Configuration
TSLink configuration files, global settings, current fields, and state management
Global Configuration
TSLink stores global settings in ~/.config/tslink/config.json. Manage it with the tslink config command:
# 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
| Key | Description | Default |
|---|---|---|
control-url | Custom 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.
| Key | Description | Default |
|---|---|---|
mcp.enabled | Serve the control plane without passing --mcp; the flag and the key each turn it on | false |
mcp.allow | Login emails and/or tag: entries authorized to call the endpoint. Legacy owner entries; use these or valid explicit bindings to enable the control plane | none |
mcp.allow_elevated_invites | Owner 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_refused | false |
mcp.node_name | Hostname of the dedicated control-plane tsnet node | tslink-mcp |
mcp.events_keepalive | Named /events SSE keepalive interval, a Go duration from 5s to 5m | 20s |
{
"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):
- Per-service —
control_urlfield inregistry.jsonservice entry - CLI flag —
tslink serve --control-url - Global config —
tslink config set control-url - 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:
# Credential-backed alternatives with default tag:tsmain; run one
tslink add webapp --proxy localhost:3000
tslink add webapp --proxy localhost:3000 --tags tag:tsmainThe 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.
# Set a custom default tag
tslink tags set-default tag:myteam
# Verify
tslink tags listACL 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.
Managing Tags with tslink tags#
The tslink tags command group provides full tag lifecycle management:
# 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-aclRegistry File
TSLink stores all service definitions in:
~/.config/tslink/registry.jsonThis 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.
{
"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
| Field | Type | Description |
|---|---|---|
name | string | Service hostname (lowercase, hyphens, alphanumeric) |
type | string | proxy, file, or tcp |
target | string | Proxy/TCP target (e.g., http://localhost:3000) |
path | string | Absolute path for file services |
port | int | TCP port number |
ephemeral | bool | Request an ephemeral node; control-plane cleanup follows inactivity, not immediate disconnect; local registration remains until removed |
tags | string[] | Configured Tailscale node tags in credential-backed mode; Tier 1 remains untagged |
allowed_users | string[] | Proxy/file HTTP allowed identities (emails or tag:xxx); nonempty values are rejected for TCP |
funnel | bool | Enable Tailscale Funnel (proxy only, public exposure) |
public_ack | bool | Required acknowledgement when funnel is true |
funnel_expires_at | string | Required for Funnel: RFC3339 deadline; legacy persisted never is preserved, but new public never is refused; omission is rejected with funnel_expiry_required |
no_auto_provision | bool | Disable this service's Funnel policy provisioning |
file | string | Optional single filename inside a file service's path |
control_url | string | Per-service control server override |
created_at | string | ISO 8601 creation timestamp |
people_scoped | bool | Private HTTP/file app requires people grants or explicit legacy allow rules; preserved after grants are removed |
health | object | Backend path/status/body assertion, timeout and interval; see health checks |
request_limits | object | Upload size, header/body-read/idle windows; no request-rate middleware |
preserve_host | bool | Proxy 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:
| Credential | Prefix | Behavior |
|---|---|---|
| API access token | tskey-api-* | Expires periodically. Current most complete path for Tailscale REST tag/device operations and auth material derivation. |
| OAuth client secret | tskey-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:
- System keychain (primary) — macOS Keychain, Linux secret service, Windows Credential Manager. The keychain service name is
"tslink". - Conditional file fallback —
~/.config/tslink/apikeyor~/.config/tslink/clientsecret, permission0600. 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:
- The watcher detects the filesystem event.
- A file lock (mutex) is acquired to prevent race conditions with concurrent writes.
- The new registry is loaded and validated.
- The new state is diffed against the running state.
- Added services are started; removed services are stopped.
- 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 selectedfile - Service
port - Service
tags,allowed_users,ephemeral,funnel,public_ack, orno_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/:
| Path | Purpose |
|---|---|
config.json | Global settings (control URL, etc.) |
registry.json | Service definitions |
tslink.pid | Daemon process ID tracking |
apikey | API key file (conditional macOS/Linux fallback) |
clientsecret | OAuth client secret file (conditional macOS/Linux fallback) |
authkey | Legacy 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.
(
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.