TSLinkTSLink Docs

Managing Services

How to add, remove, and manage proxy, file, and TCP services

View as Markdown

Overview

A "service" in TSLink is a named entry that maps a hostname on your Tailscale network to a local target. Each service runs as a separate tsnet node with its own tailnet identity and hostname, so tailnet policy can control access per service. TSLink supports three service types:

TypeFlagUse Case
Proxy--proxyReverse proxy to a web server
File--dirServe a directory over HTTPS
TCP--tcpRaw TCP forwarding (databases, custom protocols)

Proxy and file services use HTTPS with a TLS certificate, such as https://<name>.<tailnet>.ts.net. Raw TCP uses a hostname and port, such as <name>.<tailnet>.ts.net:5432, without a TSLink HTTP/TLS listener. Read the actual endpoint from runtime output rather than constructing a hostname.

Proxy Services

Use --proxy to expose a local web service via reverse proxy:

bash
tslink add <name> --proxy <host:port>

Examples:

bash
# Expose a Next.js dev server
tslink add frontend --proxy localhost:3000

# Expose an API running on a custom port
tslink add api --proxy 127.0.0.1:8080

# Short form — omit host for localhost
tslink add myapp --proxy :3000

Proxy identity propagation is best-effort: TSLink strips client-provided X-Tailscale-* headers and adds identity headers only when Tailscale WhoIs resolves a user profile. Without --allow, lookup failure can still forward a request with no identity headers; with --allow, missing or unauthorized identity is denied before the backend.

Trust identity headers only across a controlled TSLink-to-backend path that prevents direct bypass. Applications requiring user identity must reject missing values and keep their required authentication and authorization. File services do not inject proxy headers. The header fields, backend Host rewrite, and reconstructed forwarding headers are documented in Reverse Proxy.

Backend errors: A non-timeout upstream failure returns 502 Bad Gateway; a timeout returns 504 Gateway Timeout. The error response's “Service unavailable” text does not mean status 503.

File Services

Use --dir to share a directory over HTTPS:

bash
tslink add docs --dir ~/Documents/shared

The directory is served read-only with a built-in file browser and directory listing enabled. Both absolute and relative paths are supported; relative paths are resolved at registration time.

The directory is served at the root of its service hostname; read its exact address with tslink url docs --wait. Configured --allow and HTTP access logging can use WhoIs, but this file handler does not inject proxy identity headers.

TCP Services

Use --tcp to expose raw TCP connections (databases, Redis, custom protocols):

bash
# Expose a PostgreSQL database
tslink add mydb --tcp localhost:5432

# Expose Redis
tslink add cache --tcp localhost:6379

TCP services perform bidirectional byte forwarding with proper TCP half-close handling — any TCP-based protocol works. The CLI stores the target port from --tcp host:port, and the tailnet listener uses that port. If a manually edited registry entry omits port, runtime falls back to 443.

TCP services do not run through HTTP middleware, identity headers, or HTTP --allow. Both CLI registration and registry validation reject nonempty TCP allowed_users / --allow. Protect TCP with tailnet policy and the target service's own authentication.

Tailnet traffic is encrypted with WireGuard, but TSLink does not add application TLS to raw TCP. Configure authentication and any application/backend TLS in the target service and its clients.

Common use cases:

  • Databases: PostgreSQL, MySQL, MongoDB
  • Caches: Redis, Memcached
  • SSH tunneling: Forward SSH connections through your tailnet

After enrollment/readiness, use the actual hostname and port from tslink url <name> --wait from a device permitted by tailnet policy. These client examples use placeholders for that hostname:

bash
# PostgreSQL
psql -h <hostname-returned-for-mydb> -p 5432

# SSH
ssh -p 22 <hostname-returned-for-an-ssh-service>

Advanced Flags

These flags can be combined with any service type unless noted otherwise.

Ephemeral Nodes

Request an ephemeral node for a temporary service:

bash
tslink add devserver --proxy :8080 --ephemeral

Tailscale's control plane cleans it up after inactivity; a brief disconnect is not proof of removal. The local service registration remains until explicitly removed. See ephemeral node behavior.

ACL Tags

In stored-credential mode, configure the service's node tags, with existing tag ownership or explicitly authorized ordinary ACL provisioning:

bash
tslink add api --proxy :8080 --tags tag:api,tag:prod

Default credential-free (Tier 1) nodes stay user-owned and untagged even if this command saves tags in the registry. Saving tags does not change remote policy. API-token mode derives auth material with effective service tags; client-secret operations are narrower. Ordinary tag-owner ensure requires explicit --manage-acl on login / serve and sufficient policy permissions. See Tag Configuration for default tags, retained Tier 1 enrollment, and the separate acknowledged Funnel provisioning flow.

Access Control

Restrict a proxy/file HTTP service to Tailscale login emails or device tags:

bash
tslink add internal --proxy :9090 --allow user@example.com,tag:admin

When --allow is configured, missing or non-matching WhoIs identity receives 403 Forbidden before the proxy backend or file content. Resolution uses a 60s source-IP cache; it is not a fresh control-plane query on every request. File services authorize/log using WhoIs but do not inject identity headers. Preserve the application's own required authorization.

This is an HTTP request filter. It is not available for raw TCP services.

People grants add per-request WhoIs checks and deadlines for private HTTP/files. Managed logins use their people grants ahead of legacy allow rules; revocation keeps a deny record. The legacy-only allow check and identity headers have the cache behavior above; people authorization is not cached. See share with a person.

Tailscale Funnel

Expose a proxy service publicly on the internet via Tailscale Funnel:

bash
tslink add public-site --proxy localhost:3000 --funnel --public

This open Funnel publication is only available for proxy services. It requires explicit --public acknowledgement, makes the service URL publicly accessible without requiring Tailscale on the client, and is not covered by TSLink identity enforcement. Permission to publish does not authorize internet visitors; keep required visitor authentication and authorization in the backend application.

Custom Domains (Roadmap / experimental)

Custom-domain TLS and ACME are not implemented. The --domain / --acme-email flags and domain / acme_email registry fields have been removed; old registry keys are rejected with unknown_config_key even when empty. Use the default <service>.<tailnet>.ts.net hostname for shipped flows.

Per-Service Control URL

Override the global control server for a specific service (useful when mixing Tailscale and Headscale nodes):

bash
tslink add headscale-app --proxy localhost:3000 --control-url https://headscale.example.com

Or set it directly in registry.json:

json
{
  "name": "headscale-app",
  "type": "proxy",
  "target": "http://localhost:3000",
  "control_url": "https://headscale.example.com"
}

Configuration priority: per-service flag/config > CLI flag on serve > global config (tslink config) > Tailscale default.

Middleware (Roadmap / experimental)

Configurable rate limiting, Basic Auth, IP allow lists, and CORS are unavailable; the removed middleware key is rejected with unknown_config_key at registry load, even when empty. See Experimental & Roadmap. It is not a protection layer for a running share.

Docker Auto-Discovery (Roadmap / experimental)

Docker discovery is not implemented; the current TSLink has no discovery package or accepted labels. Register services explicitly with tslink add until this feature is wired and integration-tested.

Naming Conventions

Service names become part of the URL:

Code
https://<service-name>.<tailnet-name>.ts.net

Rules:

  • Lowercase letters, numbers, and hyphens only
  • Must start and end with a letter or number
  • Must be unique within your TSLink instance

Good names: webapp, api-v2, staging-server, docs

Listing Services

View all registered services:

bash
tslink list

Shows service name, type, target, and configured flags for each entry.

Removing a Service

Remove a service by name:

bash
tslink remove frontend

Local removal deletes the registry entry; a running gateway stops the service after detecting that change through hot reload. Remote tailnet cleanup is attempted only when TSLink can prove exact ownership of a matching device; otherwise matching candidates are reported as protected and cleanup is skipped. Local success does not prove remote device deletion.

Hot Reload

TSLink watches registry.json for changes using fsnotify. When you add or remove a service while the gateway is running:

  1. The registry file is updated automatically.
  2. The file watcher detects the change.
  3. New services start receiving traffic once enrollment, approval, and readiness are complete; removed services stop.

This means you can manage services without interrupting other active services.

What takes effect through hot reload:

  • Adding a new service (tslink add)
  • Removing a service (tslink remove)
  • Structural changes that runtime detects, such as target/path/port, tags, ephemeral mode, or per-service control URL updates, by restarting the affected node

Runtime restart is distinct from enrollment reset: tag edits retain Tier 1 user enrollment. See Hot Reload for the field and identity contract.

What requires a gateway restart:

  • Changing global configuration (e.g., control-url)
  • Updating authentication credentials or swapping credential modes (tslink login)
  • Changing the legacy authkey file

Use the platform-aware restart sequence for these changes. A managed installation needs verified shutdown and restoration through its manager; a manual stop / serve sequence is only for a setup without autostart registration.

On startup, TSLink also attempts ownership-safe stale tailnet node cleanup for previous runs. Candidates without exact ownership proof are reported as protected/skipped.

Multiple Services

Register as many services as you need. Each gets its own hostname:

bash
tslink add app --proxy localhost:3000
tslink add api --proxy localhost:8080
tslink add docs --dir ~/docs
tslink add mydb --tcp localhost:5432

Default add already ensures the background gateway is running. Inspect each result; if it reports needs_login, open its auth_url and complete the tailnet's enrollment/approval requirements. Then run tslink url <name> --wait for each service and inspect tslink status --json. The same gateway serves all ready services; do not run serve again after default add.

For an explicitly manual workflow, use --no-daemon-install on registrations and start serve only when no gateway or managed installation is present. See daemon mode for that workflow and the verified shutdown prerequisite.

Guest and private management workflows

The Funnel registration above is an open publication. Guest links instead start with a private HTTP proxy app and explicitly enable public Funnel with a mandatory guest gate and optional PIN. Each link expires; private people/allow checks remain independent. New public lifetime is at least 1h, defaults to 24h and has a default 7d maximum; never is refused. Durations describes owner-configured limits and explicit regrant.

Use the private per-host portal and requests to help eligible tailnet members find or request private HTTP/file apps. Use access history to inspect retained access, not as a completeness guarantee.

Table of Contents