Configuration
Config precedence
Section titled “Config precedence”Configuration is resolved in this order (highest to lowest):
- CLI flags — explicit flags on the command line
- Environment variables —
TS_*prefix - YAML config file — specified with
--config - Managed profile credential — auth key only, when the profile references one
- Built-in defaults — hardcoded fallbacks
Browser exception (ADR-014): browser does not accept an environment-only
credential. With browser --profile, --auth-key-file remains the highest
precedence source; otherwise the managed profile credential is used and
TS_AUTHKEY is ignored with an explicit warning.
Environment variables
Section titled “Environment variables”Required
Section titled “Required”| Variable | Description | Example |
|---|---|---|
TS_AUTHKEY |
Auth key for connect. Tailscale: tskey-auth-<id>-<secret>. Headscale: hskey-*. Plaintext in the environment and readable by child processes. On a shared or hardened host, connect --auth-key-file can leave it unset; init --auth-key-file avoids command-line exposure but writes TS_AUTHKEY to the generated .env (or the sibling .env in YAML mode). Browser mode intentionally ignores this variable; use browser --auth-key-file or a managed profile credential. |
tskey-auth-KEYID-SECRET |
TS_TARGET |
Host address on the mesh network | 100.82.151.104:3389 |
Optional
Section titled “Optional”| Variable | Default | Description |
|---|---|---|
TS_LOCAL_ADDR |
127.0.0.1:33389 |
Local bind address. Auto-derived in auto mode when unset. |
TS_CONTROL_URL |
(Tailscale default) | Custom control plane URL for self-hosted Headscale. Required for hskey- keys — they are rejected without it (a tskey- key routes to Tailscale SaaS by default). Must be an http(s):// URL. |
TS_BOOTSTRAP_SSH |
(disabled) | OpenSSH endpoint (user@host[:port]) used to reach a custom control plane through dynamic SOCKS forwarding. Requires TS_CONTROL_URL. |
TS_BOOTSTRAP_SOCKS_ADDR |
127.0.0.1:1055 |
Loopback-only SOCKS listener for SSH bootstrap. It is valid only when TS_BOOTSTRAP_SSH is set. |
TS_HOSTNAME |
ts-bridge |
Node name in the admin console. Auto-generated per run in auto mode. |
TS_STATE_DIR |
(per-user) | Directory for node state (holds the private node identity). Default is a fixed per-user dir: Windows %LOCALAPPDATA%\ts-bridge\state, macOS ~/Library/Application Support/ts-bridge/state, Linux $XDG_STATE_HOME/ts-bridge/state (→ ~/.local/state/ts-bridge/state). Created with 0700 permissions. Ephemeral temp dir in auto mode with an instance. A relative override is warned about. |
TS_AUTO_INSTANCE |
true |
Auto mode toggle. Set false to disable auto behavior. |
TS_MANUAL_MODE |
false |
Force legacy persistent mode. Takes precedence over TS_AUTO_INSTANCE. |
TS_INSTANCE_NAME |
(empty) | Stable instance alias for deterministic local port selection. |
TS_PORT_RANGE |
33389-34388 |
Port range for auto mode (START-END). |
TS_TIMEOUT |
30s |
Timeout for tsnet initialization (control-plane handshake). Go duration format. |
TS_DIAL_TIMEOUT |
5s |
Per-connection target dial timeout. Keeps stuck dials from holding a slot across retries. Go duration format. (v1.8.0+) |
TS_DRAIN_TIMEOUT |
15s |
Timeout for graceful drain of active connections on shutdown. Go duration format. |
TS_MAX_CONNECTIONS |
1000 |
Maximum concurrent connections before rejecting new ones. |
TS_IDLE_TIMEOUT |
(disabled) | Close connections after this period of no traffic. Go duration format (e.g. 30m). Default 0 disables. (v1.6.0+) |
TS_DIAL_RETRIES |
3 |
Maximum retries for transient target dial failures. 0 disables. (v1.7.0+) |
TS_DIAL_BACKOFF_BASE |
1s |
Base backoff for dial retries (multiplied by 2^attempt, plus jitter). Go duration format. (v1.7.0+) |
TS_DIAL_BACKOFF_MAX |
30s |
Cap on backoff per retry attempt. Must be ≥ TS_DIAL_BACKOFF_BASE. (v1.7.0+) |
TS_HEALTH_ADDR |
(disabled) | Address for health/metrics HTTP server. |
TS_VERBOSE |
false |
Enable debug logging. Also available as -v flag. |
TS_LOG_FORMAT |
text |
Log output format (text or json). |
Host Configuration
Section titled “Host Configuration”The ts-bridge host commands resolve their settings as flags > environment (TS_HOST_*,
including ./.env) > host file (--config) > per-OS defaults. ts-bridge host init --config <path>
writes the host file; the variables below override it per run.
version: 1host: services: - kind: rdp # rdp on Windows and Linux; ssh/vnc as their OS steps land port: 3389 firewall: # Windows only rule: Tailscale-RDP-Ingress power: # Windows only prevent_sleep: true control_url: https://headscale.example.com # optional: the descriptor's control planeUnknown keys warn and are ignored; an empty key, or an entry the OS has no step for, is an
error before any step runs. host.control_url is read from the file only.
| Variable | Default | Description |
|---|---|---|
TS_HOST_RDP_PORT |
3389 |
RDP listening port on the host. |
TS_HOST_FIREWALL_RULE |
Tailscale-RDP-Ingress |
Name of the firewall rule. |
TS_HOST_NO_SLEEP |
false |
Disable sleep mode on the host (Windows). |
Host serve (host.mesh)
Section titled “Host serve (host.mesh)”ts-bridge host serve reads the host.mesh block of the host file (--config); no
environment variable sets it. Serve also ignores the TS_* variables and the YAML that tune
connect: its dial, idle and drain timeouts, connection limit and bootstrap backoff are the
built-in defaults. Unknown keys warn and are ignored; a key given with no or an
empty value is an error.
| Key | Required | Description |
|---|---|---|
state_dir |
yes | Absolute path holding the node identity. Windows: owned by SYSTEM or Administrators, no other SID in its DACL, no link or junction on the path. Linux: 0700, owned by root. Created protected when absent. |
auth_key_file |
first start | Absolute path to a tagged auth key, read only while state_dir holds no node. Must not be readable by other accounts. |
hostname |
no | Node name; defaults to the OS hostname in lower case. |
bootstrap_ssh |
no | user@host[:port] to reach host.control_url through OpenSSH, as connect --bootstrap-ssh. Needs host.control_url. |
bootstrap_socks_addr |
no | Loopback SOCKS address for that bootstrap; default 127.0.0.1:1055. |
health_addr |
no | Loopback address for /health/ready, which host check --json reads for published. |
host.control_url is required by serve. Setup and recipes: the
host serve runbook.
Boolean parsing
Section titled “Boolean parsing”TS_VERBOSE, TS_AUTO_INSTANCE, TS_MANUAL_MODE accept: 1, true, yes, on (case-insensitive). All other values are treated as false.
YAML config file
Section titled “YAML config file”YAML config is optional and supports non-sensitive settings only. The auth key must not be stored in YAML files.
Named profiles may store a non-secret managed credential reference:
profiles: office: target: "acemagic-office:45000" credential: "office"Store the key with ts-bridge auth set --profile office. The managed
credential path and value are never exported through a tsb:// descriptor.
Example
Section titled “Example”target: "100.82.151.104:3389"local_addr: "127.0.0.1:33389"hostname: "office-laptop"health_addr: "127.0.0.1:9090"max_connections: 1000idle_timeout: "30m"log_format: "json"
# Optional: reach a blocked Headscale control plane through OpenSSH.control_url: "https://vpn.example.com"bootstrap_ssh: "deployer@bastion.example.com"bootstrap_socks_addr: "127.0.0.1:1055"Loading
Section titled “Loading”# Load from YAML, auth key from envts-bridge connect --config ts-bridge.yaml
# Auth key from file (secure)ts-bridge connect --config ts-bridge.yaml --auth-key-file /run/secrets/authkey
# Secure non-interactive initializationts-bridge init --auth-key-file /run/secrets/authkey --target 100.82.151.104:3389Headscale configuration
Section titled “Headscale configuration”For self-hosted Headscale, set TS_CONTROL_URL and use hskey-* auth keys:
TS_AUTHKEY=hskey-auth-xxxxxTS_TARGET=100.64.0.5:3389TS_CONTROL_URL=https://vpn.example.comKeep that file at 600, or leave TS_AUTHKEY out of it entirely and start with
ts-bridge connect --auth-key-file /run/secrets/authkey --control-url https://vpn.example.com.
The init wizard supports both Tailscale and Headscale — it detects the key prefix. For an
hskey-* key it requires a control plane (--control-url, or --profile for a named profile)
and refuses to write a configuration that connect would then reject; the pairing is enforced
on write by the same validator the runtime applies on read.
Re-running init --format yaml in a directory that already holds a .env rewrites that file’s
TS_TARGET and TS_CONTROL_URL from the flags it was given. That layer outranks the YAML file in
the merge chain (flags > env > yaml > defaults), so a stale value there would otherwise be the
one connect uses — which is why init writes the values it validated instead of preserving them,
and validates a preserved control URL when no flag was given.
SSH bootstrap for filtered networks
Section titled “SSH bootstrap for filtered networks”When the local network blocks or sinkholes the Headscale hostname, but SSH to a bastion remains available:
ts-bridge connect \ --control-url https://vpn.example.com \ --bootstrap-ssh deployer@bastion.example.com \ --auth-key-file /run/secrets/headscale-authkey \ --target mesh-host:3389The system ssh binary opens a loopback dynamic SOCKS proxy. Only requests to
the configured control-plane hostname are selected for that proxy; unrelated
HTTP traffic keeps its existing proxy behavior. DNS resolution happens through
the SSH server while the original URL hostname is retained for TLS SNI and
certificate checks. The feature never edits the hosts file or disables
certificate or SSH host-key verification.