TSLinkTSLink Docs

Configuration

TSLink configuration files, global settings, current fields, and state management

View as Markdown

Global Configuration

TSLink stores global settings in ~/.config/tslink/config.json. Manage it with the tslink config command:

bash
# Set a value
tslink config set control-url https://headscale.example.com

# Get a value
tslink config get control-url

# List all settings
tslink config list

# Clear a value (revert to default)
tslink config set control-url ""

Available Settings

KeyDescriptionDefault
control-urlCustom Tailscale control server URL (e.g., Headscale)Tailscale default

TSLink refuses Tailscale API-derived node auth keys for a non-Tailscale control server with credential_control_url_mismatch. Selecting a Headscale URL does not prove enrollment or HTTPS compatibility; end-to-end Headscale validation remains pending.

The control-url can also be overridden per-service in registry.json, or per-session with tslink serve --control-url <url> in a deliberate manual setup. For managed daemon mode, prefer persisted config and follow Restarting After Configuration Changes; starting a second serve is not a restart, and one-off flags may not survive background process restarts.

Remote MCP Control Plane Keys

The optional mcp block turns on the tailnet-only remote MCP control plane served by tslink serve --mcp. It is edited by hand: Use tslink config set for supported control-url/access-log keys; edit MCP bindings directly.

KeyDescriptionDefault
mcp.enabledServe the control plane without passing --mcp; the flag and the key each turn it onfalse
mcp.allowLogin emails and/or tag: entries authorized to call the endpoint. Legacy owner entries; use these or valid explicit bindings to enable the control planenone
mcp.allow_elevated_invitesOwner opt-in for MCP user invitations with a role other than member, or device invitations with exit-node use; otherwise they fail with mcp_elevated_invite_refusedfalse
mcp.node_nameHostname of the dedicated control-plane tsnet nodetslink-mcp
mcp.events_keepaliveNamed /events SSE keepalive interval, a Go duration from 5s to 5m20s
json
{
  "mcp": {
    "enabled": true,
    "allow": ["you@example.com", "tag:ops"],
    "node_name": "tslink-mcp"
  }
}

Every principal in mcp.allow gets powerful service control; elevated invitations additionally require the owner's mcp.allow_elevated_invites opt-in. Treat the list as a high-privilege access list. Boundaries, client reachability, and the tslink mcp stdio alternative are documented under Remote MCP Control Plane.

Priority Order

Settings are resolved in this order (highest to lowest):

  1. Per-service — control_url field in registry.json service entry
  2. CLI flag — tslink serve --control-url
  3. Global config — tslink config set control-url
  4. Default — Tailscale hosted control server

Tag Configuration

Default zero-credential (Tier 1) enrollment creates user-owned, untagged nodes. Stored credentials can enable configured node tags; remote ACL policy management requires its own explicit opt-in and permissions.

Default Tag

In credential-backed mode, adding a service without --tags selects the default tag. With the unchanged default, these are alternative registrations:

bash
# Credential-backed alternatives with default tag:tsmain; run one
tslink add webapp --proxy localhost:3000
tslink add webapp --proxy localhost:3000 --tags tag:tsmain

The default tag is tag:tsmain in credential-backed mode; zero-credential nodes remain user-owned and untagged even if the registry stores tags. A Tier 1 tag edit retains its existing user enrollment. Ordinary tag-owner ensure is enabled only by explicit --manage-acl on login / serve. Available API-token or OAuth-client operations depend on the required scopes; otherwise manage remote tags yourself.

Changing the Default Tag

Use tslink tags set-default to change the default tag. The value is stored in GlobalConfig.DefaultTag in ~/.config/tslink/config.json.

bash
# Set a custom default tag
tslink tags set-default tag:myteam

# Verify
tslink tags list

ACL Mutation (default-off, --manage-acl)#

Ordinary tag-owner ensure is default-off: tslink login --manage-acl ensures only the configured default tag; tslink serve --manage-acl ensures ordinary tags actually used by valid registry services, including the configured default only when a service uses it. An empty registry or services using only custom tags do not implicitly add the default tag. The credential must have the required policy scopes. Updates use ETags and do not blindly retry conflicts.

Explicitly public Funnel is a separate flow. For a proxy acknowledged with --funnel --public, TSLink can automatically provision the shared tagOwners / nodeAttrs Funnel grant by default, without another --manage-acl; this still needs suitable credentials and policy permissions. Service --no-auto-provision / no_auto_provision disables that service's provisioning; tslink serve --no-auto-provision disables it for every service in the process. Use tslink install --no-auto-provision to retain that choice in the installed background service.

tslink cleanup --manage-acl only reports whether this machine still uses the shared Funnel grant. It never deletes that shared grant and cannot infer other machines' usage from a local registry.

The tslink tags command group provides full tag lifecycle management:

bash
# List services and their configured tags (Tier 1 does not advertise them)
tslink tags list

# Print tags from the Tailscale API
tslink tags pull

# Add a tag to a specific service
tslink tags add webapp tag:staging

# Replace a specific service's tags with one tag
tslink tags set webapp tag:web

# Change the default tag
tslink tags set-default tag:myteam

# Remove an ACL tag owner rule globally after local safety checks
tslink tags delete-remote tag:deprecated --force --manage-acl

Registry File

TSLink stores all service definitions in:

Code
~/.config/tslink/registry.json

This file is the single source of truth for your registered services. Use tslink add and tslink remove to manage it, or edit it manually for advanced configuration.

Full Registry Format

Ordinary services-only writes retain schema version 1. People-enabled registries use version 2 with top-level people and per-service people_scoped. Older binaries refuse unsupported fields rather than discarding grants; stop the new daemon and restore a separately backed-up version 1 registry only if you intentionally discard people authorization. The services-only example below remains valid.

json
{
  "schema_version": 1,
  "services": [
    {
      "name": "webapp",
      "type": "proxy",
      "target": "http://localhost:3000",
      "path": "",
      "port": 0,
      "ephemeral": false,
      "tags": ["tag:web"],
      "allowed_users": ["user@example.com"],
      "funnel": false,
      "public_ack": false,
      "control_url": "",
      "created_at": "2026-03-01T00:00:00Z"
    },
    {
      "name": "shared-files",
      "type": "file",
      "path": "/Users/you/Documents/shared",
      "created_at": "2026-03-01T00:01:00Z"
    },
    {
      "name": "mydb",
      "type": "tcp",
      "target": "localhost:5432",
      "port": 5432,
      "created_at": "2026-03-01T00:02:00Z"
    }
  ]
}

Service Fields

FieldTypeDescription
namestringService hostname (lowercase, hyphens, alphanumeric)
typestringproxy, file, or tcp
targetstringProxy/TCP target (e.g., http://localhost:3000)
pathstringAbsolute path for file services
portintTCP port number
ephemeralboolRequest an ephemeral node; control-plane cleanup follows inactivity, not immediate disconnect; local registration remains until removed
tagsstring[]Configured Tailscale node tags in credential-backed mode; Tier 1 remains untagged
allowed_usersstring[]Proxy/file HTTP allowed identities (emails or tag:xxx); nonempty values are rejected for TCP
funnelboolEnable Tailscale Funnel (proxy only, public exposure)
public_ackboolRequired acknowledgement when funnel is true
funnel_expires_atstringRequired for Funnel: RFC3339 deadline; legacy persisted never is preserved, but new public never is refused; omission is rejected with funnel_expiry_required
no_auto_provisionboolDisable this service's Funnel policy provisioning
filestringOptional single filename inside a file service's path
control_urlstringPer-service control server override
created_atstringISO 8601 creation timestamp
people_scopedboolPrivate HTTP/file app requires people grants or explicit legacy allow rules; preserved after grants are removed
healthobjectBackend path/status/body assertion, timeout and interval; see health checks
request_limitsobjectUpload size, header/body-read/idle windows; no request-rate middleware
preserve_hostboolProxy forwards its canonical external Host when enabled; Origin remains unchanged

Roadmap / Experimental Configuration

The middleware, domain, and acme_email registry keys have been removed. They are rejected with unknown_config_key, even with empty values. Strict mutation commands refuse these entries; the daemon skips invalid service entries and continues healthy services. A reload that makes a public Funnel entry invalid closes its public listener. Docker discovery and Prometheus instrumentation are not implemented. For details, see Experimental & Roadmap.

Credential Storage

Default Tier 1 enrollment uses browser login without stored API credentials. For credential-backed operation, TSLink supports these two stored credential types:

CredentialPrefixBehavior
API access tokentskey-api-*Expires periodically. Current most complete path for Tailscale REST tag/device operations and auth material derivation.
OAuth client secrettskey-client-*Does not expire. Used directly by tsnet for authentication, but current REST tag/device automation paths are narrower. Validate before unattended use.

Storage Priority

Credentials are stored and resolved in this order:

  1. System keychain (primary) — macOS Keychain, Linux secret service, Windows Credential Manager. The keychain service name is "tslink".
  2. Conditional file fallback — ~/.config/tslink/apikey or ~/.config/tslink/clientsecret, permission 0600. macOS/Linux only, after proving an old keychain value absent or deleted; unreachable/uncertain keychains fail closed. Windows disables this fallback.

Legacy Support

For backward compatibility, TSLink also checks ~/.config/tslink/authkey. This file is used only if no API key or OAuth client secret exists. It is not recommended for new installations.

Credential Migration

If a file-based API access token exists and the system keychain is available, tslink serve migrates that API token into the keychain and removes the file after keychain storage succeeds. OAuth client-secret fallback files are not auto-migrated by serve; pipe the secret to tslink login --client-secret-stdin to store one through the normal keychain-first path.

Auth Key Derivation

When using an API access token (tskey-api-*), TSLink derives fresh auth material as each service starts. The request is scoped to that service's configured tags, ephemeral setting, and service-specific description. Auth keys are not persisted to disk.

Hot Reload

TSLink uses fsnotify to watch the registry file for changes. When the file is modified:

  1. The watcher detects the filesystem event.
  2. A file lock (mutex) is acquired to prevent race conditions with concurrent writes.
  3. The new registry is loaded and validated.
  4. The new state is diffed against the running state.
  5. Added services are started; removed services are stopped.
  6. Existing unchanged services continue running uninterrupted.

What Requires a Node Restart

Changes to the following fields recreate the affected service runtime through hot reload, briefly interrupting that service:

  • Service type (proxy, file, tcp)
  • Service target, path, or selected file
  • Service port
  • Service tags, allowed_users, ephemeral, funnel, public_ack, or no_auto_provision
  • Effective control_url

Runtime recreation and enrollment reset are different. Tag edits retain Tier 1 enrollment; effective tag changes in credential-backed mode, ephemeral changes, or an effective control-server change can reset node identity. Other unchanged services continue running. Invalid service entries are skipped; other healthy entries continue. A reload that invalidates a public Funnel entry closes its listener instead of retaining public exposure.

State Directory

TSLink stores its runtime state in ~/.config/tslink/:

PathPurpose
config.jsonGlobal settings (control URL, etc.)
registry.jsonService definitions
tslink.pidDaemon process ID tracking
apikeyAPI key file (conditional macOS/Linux fallback)
clientsecretOAuth client secret file (conditional macOS/Linux fallback)
authkeyLegacy auth key (backward compatibility)
nodes/Per-service tsnet state (WireGuard keys, node state)
logs/Daemon and access logs

The state directory is created automatically on first use. The nodes/ directory is managed by the embedded tsnet library. Roadmap/experimental custom-domain ACME work is not part of the shipped state-directory contract.

Resetting State

Remove managed automatic startup before resetting state. On macOS, stopping a LaunchAgent alone lets KeepAlive restart it; an unsuccessful uninstall must halt the reset, including when a launchd domain cannot be inspected. Do not use --force to bypass that proof. Stop any remaining manual gateway, then verify that it is stopped and no automatic-start/restart registration remains before clearing credentials or state.

This Bash example resets the default macOS/Linux directory and uses Python 3 to validate the status JSON. It runs in a subshell and stops on any failed command, malformed/missing status field, running gateway, or active supervision. For a custom configuration directory, use its matching service registration and state path instead; this example refuses a different TSLINK_CONFIG_DIR. Do not start another gateway while resetting.

bash
(
  set -euo pipefail
  [ -z "${TSLINK_CONFIG_DIR:-}" ] || [ "$TSLINK_CONFIG_DIR" = "$HOME/.config/tslink" ]

  # 1. Remove managed startup, then stop any remaining manual gateway
  tslink uninstall
  tslink stop

  # 2. Require a stopped gateway and inactive supervision before continuing
  tslink status --json | python3 -c '
import json, sys
result = json.load(sys.stdin)
data = result.get("data", {})
supervision = data.get("supervision", {})
if not (result.get("ok") is True and data.get("daemon_running") is False
        and all(supervision.get(key) is False
                for key in ("installed", "autostart", "restart_on_exit"))):
    sys.exit("Reset halted: gateway or supervision is active or unverified")
'

  # 3. Clear credentials, then remove local registry, node state, and logs
  tslink logout
  rm -rf -- "$HOME/.config/tslink"
)

On Windows, use the same uninstall → stop → status --json prerequisites before logout or local state deletion; stop uses process termination rather than graceful shutdown. The Bash/Python example above is for macOS/Linux.

After a successful reset, start a new share and approve its node; stored-credential tslink login remains optional. Verify and clean up remote devices. A local reset does not delete remote nodes from your tailnet. Open the Tailscale admin console and manually check for and remove any leftover TSLink devices.

tslink logout alone clears credentials while preserving the directory; the final removal additionally deletes the registry and node state. rm -rf on its own does not clear a keychain credential and does not prove remote cleanup — it is not equivalent to logout or to admin-console device removal.

Verify remotely. None of these steps delete tailnet devices automatically — TSLink only removes a device when it can prove exact ownership, so remote cleanup is protected/manual. After a reset, open the Tailscale admin console and remove any leftover *.ts.net devices this machine registered.

Access management settings

mcp.bindings adds viewer/app-operator/people-manager/owner principals, explicit apps or viewer inventory, positive operator/manager max_duration, and optional fixed binding expiry. Legacy mcp.allow remains owner authority; duplicate principals fail closed. Restart after editing MCP configuration. See MCP scopes.

Use tslink portal enable --owner you@example.com for the independent private portal; optional admins can open all private HTTP/file apps. Read its actual status URL. Per-service requestable is false by default and explicitly discloses a private app name for requests. See portal and requests. People/guest/request/portal state shares the atomic registry; use compatible TSLink readers and preserve backups before downgrade.

durations.public_max sets the guest/public maximum (default 7d, relative value at least 1h). New people/Funnel default to 24h; changing the policy does not rewrite deadlines. See duration exceptions.

The access_log block manages enabled/path_mode/retention_days/max_bytes/queue_size; service access_log_path_mode overrides inherited path recording. Global off is a hard opt-out. tslink config set access-log-path-mode prefix and other access-log keys are supported. Defaults and gaps are in access history. Global changes require restart; app path settings hot reload.

Table of Contents