Skip to content

Tailscale

This document guides you through the different authentication and configuration options for Tailscale with TSDProxy. For a quick comparison of authentication methods, see Authentication Methods.

Authentication Methods

TSDProxy supports three authentication methods with Tailscale: OAuth, OAuth (manual), and AuthKey.

OAuth

Prerequisites

  1. Generate an OAuth client at https://login.tailscale.com/admin/settings/oauth.
  2. Define tags for services. Tags can be defined in the provider, applying to all services.

Important

All auth keys created from an OAuth client require tags. This is a Tailscale requirement.

Configuration

Add the OAuth client credentials to the TSDProxy configuration:

/config/tsdproxy.yaml
tailscale:
  providers:
    default:
      clientId: "your_client_id"
      clientSecret: "your_client_secret"
      tags: "tag:example" # Optional if tags are defined in each proxy

Tip

To avoid hardcoding clientId and clientSecret in the config file, you can set them via environment variables instead. See Environment Variables for details on TSDPROXY_TAILSCALE_<NAME>_CLIENTID and TSDPROXY_TAILSCALE_<NAME>_CLIENTSECRET.

Restart

Restart TSDProxy to apply the changes.

Tip

If the proxy fails to authenticate after restarting, check the error logs. Ensure the tags are correct and the OAuth client is enabled.

OAuth (Manual)

Disable AuthKey

OAuth authentication mode is enabled when no AuthKey is set in the Tailscale provider configuration:

/config/tsdproxy.yaml
tailscale:
  providers:
    default:
      authKey: ""
      authKeyFile: ""

The proxy will wait for authentication with Tailscale during startup.

Dashboard

Access the TSDProxy dashboard (e.g., http://192.168.1.1:8080).

Authentication

Click on the proxy with “Authentication” status.

Tip

If “Ephemeral” is set to true, authentication is required at each TSDProxy restart.

AuthKey

Generate AuthKey

  1. Go to https://login.tailscale.com/admin/settings/keys.
  2. Click “Generate auth key”.
  3. Add a description.
  4. Enable “Reusable”.
  5. Add tags if needed.
  6. Click “Generate key”.

Warning

If tags are added to the key, all proxies initialized with the same AuthKey will receive the same tags. To use different tags, add a new Tailscale provider to the configuration.

Configuration

Add the AuthKey to the TSDProxy configuration:

/config/tsdproxy.yaml
tailscale:
  providers:
    default:
      authKey: "YOUR_GENERATED_KEY_HERE"
      authKeyFile: ""

Restart

Restart TSDProxy to apply the changes.

Funnel

In addition to configuring TSDProxy to enable Funnel, you need to grant permissions in the Tailscale ACL. See Troubleshooting for more details. Also read Tailscale’s Funnel documentation for requirements and limitations.

Tags

  • Tags are required for OAuth authentication.
  • Tags only work with OAuth authentication.
  • Tags can be configured in the provider or service.
  • If tags are defined in the provider, they apply to all services.
  • If tags are defined in the service, provider tags are ignored.

Prevent Duplicate Machines

When TSDProxy restarts and the data directory has been lost (e.g. non-persistent Docker volume), Tailscale creates a new machine instead of reconnecting the existing one. This results in duplicate machines in your tailnet, often with a -1 suffix.

The preventDuplicates option (default: false) tells TSDProxy to query the Tailscale API before creating a new node. If an existing device with the same hostname and matching tags is found and is offline, it is deleted first so the new node can take its place.

A boolean option:

ValueBehavior
falseDo not check for duplicate devices (default)
trueCheck and remove offline duplicates before creating a new node (requires OAuth)

Warning

This deletes devices from your tailnet. Deleting a device also removes any manual configuration associated with it, including custom ACL rules, tags assigned in the Tailscale admin console, and device-specific settings. Only enable this if you understand the implications. The safest way to prevent duplicates is to use a persistent Docker volume for the dataDir directory.

Requirements

  • OAuth authentication (clientId + clientSecret) — the Tailscale API is not available with auth keys alone.
  • Tags must be configured on the provider.

Configuration

/config/tsdproxy.yaml
tailscale:
  providers:
    default:
      clientId: "your_client_id"
      clientSecret: "your_client_secret"
      tags: "tag:example"
      preventDuplicates: true

Tip

You can omit clientId and clientSecret from the config file and set TSDPROXY_TAILSCALE_DEFAULT_CLIENTID and TSDPROXY_TAILSCALE_DEFAULT_CLIENTSECRET as environment variables instead.

Safety checks

A device is only deleted when all of these conditions are true:

  • It has the same hostname as the proxy being created
  • It has matching tags
  • It is currently offline (ConnectedToControl is false)
  • The local tsnet state file is missing (no existing identity to reuse)

Online devices are never deleted.

Certificate Concurrency

When many ephemeral containers restart at once, TSDProxy requests TLS certificates for all of them simultaneously. The Tailscale local API cannot handle this thundering herd, resulting in context deadline exceeded errors and failed certificate generation.

The maxCertConcurrency option (default: 2) limits how many certificate generation requests run in parallel. Requests that exceed the limit wait for a slot and are logged at warn level if delayed by more than one second.

/config/tsdproxy.yaml
tailscale:
  providers:
    default:
      maxCertConcurrency: 3 # allow up to 3 parallel cert requests

Tip

The default of 2 is sufficient for most deployments. Increase it only if you run 50+ containers and want faster startup at the cost of higher load on the Tailscale coordination server. Values below 1 are invalid and fall back to the default.

Identity Headers

TSDProxy resolves the Tailscale identity of each incoming request and forwards it to your backend services via HTTP headers. All identity headers are stripped from the incoming request before being set, preventing header injection attacks.

Unauthenticated requests (e.g. via Funnel) will not receive identity headers.

TSDProxy Headers

HeaderValue
x-tsdproxy-usernameTailscale login name
x-tsdproxy-displaynameTailscale display name
x-tsdproxy-profilepicurlTailscale profile picture URL

Standard Auth Headers

These headers are recognized by common reverse-proxy-aware backends (Authelia, OAuth2 Proxy, Traefik, FileBrowser, etc.):

HeaderValueUsed by
Remote-UserTailscale login nameApache, Nginx, FileBrowser
X-Forwarded-UserTailscale login nameTraefik, Authelia, many apps
X-Auth-Request-UserTailscale login nameOAuth2 Proxy
X-Forwarded-EmailTailscale login nameKeycloak, Authentik
X-Auth-Request-EmailTailscale login nameOAuth2 Proxy
X-Forwarded-Preferred-UsernameTailscale display nameOpenShift, Kubernetes

Standard Proxy Headers

HeaderValue
X-Forwarded-ForClient IP address
X-Forwarded-HostOriginal host header
X-Forwarded-ProtoOriginal protocol

Usage Example: FileBrowser

FileBrowser supports proxy authentication out of the box. Configure it to read the X-Forwarded-User header set by TSDProxy:

filebrowser --auth.method=proxy --auth.header=X-Forwarded-User

Users will be automatically logged in with their Tailscale login name.

Shared Tailscale

By default, each proxy gets its own Tailscale connection (tsnet.Server). When you enable shared mode, multiple proxies share a single Tailscale connection, which is useful when you want to conserve Tailscale machine quota or centralize DNS and TLS management.

How it works

  • All shared proxies use one tsnet.Server with SNI (Server Name Indication) routing

  • Incoming TLS connections are dispatched by domain name to the correct proxy

  • Each proxy must have a custom domain set (tsdproxy.domain label or domain in list config) because SNI routing depends on unique domain names

  • Only HTTPS ports are supported in shared mode — TCP and plain HTTP ports cannot be multiplexed by SNI and will be rejected at startup

    Note

    SNI routing inspects the TLS ClientHello to determine which domain the client is connecting to. Without TLS, there is no SNI to inspect, so multiple proxies cannot share a single listener on the same port. HTTP redirects (80/http->...) are also excluded because they would conflict when multiple proxies try to bind port 80 on the shared server. If you need TCP or redirect ports alongside shared mode, use a per-proxy Tailscale provider for those containers instead.

  • The shared server starts when the first proxy is created and stops when the last proxy is removed

Configuration

/config/tsdproxy.yaml
defaultProxyProvider: shared

dnsProviders:
  cloudflare:
    provider: cloudflare
    apiToken: "your-cloudflare-api-token"

tlsProviders:
  acme:
    provider: acme
    email: "admin@example.com"

defaultDNSProvider: cloudflare
defaultTLSProvider: acme

tailscale:
  providers:
    shared:
      clientId: "your_client_id"
      clientSecret: "your_client_secret"
      tags: "tag:shared-proxy"
      shared: true
      hostname: "shared-proxy"
  dataDir: /data/

docker:
  local:
    host: unix:///var/run/docker.sock
    defaultProxyProvider: shared

Container labels for shared proxies:

services:
  app1:
    image: nginx:alpine
    labels:
      tsdproxy.enable: "true"
      tsdproxy.name: "app1"
      tsdproxy.domain: "app1.example.com"

  app2:
    image: nginx:alpine
    labels:
      tsdproxy.enable: "true"
      tsdproxy.name: "app2"
      tsdproxy.domain: "app2.example.com"

Requirements

Tip

To keep clientId and clientSecret out of the config file, set TSDPROXY_TAILSCALE_SHARED_CLIENTID and TSDPROXY_TAILSCALE_SHARED_CLIENTSECRET as environment variables instead.

Important

Shared Tailscale mode requires a custom domain on every proxy. Without a domain, the proxy cannot be routed via SNI and will fail to start. Configure DNS and TLS providers as described in Custom Domains.

When to use shared mode

  • Fewer Tailscale machines in your tailnet
  • All domains point to a single Tailscale hostname
  • Centralized DNS and TLS management

Services Mode

Services mode uses the Tailscale VIP Services API to automatically assign FQDNs to each proxy. Unlike shared mode, no custom domains, external DNS, or TLS providers are needed — Tailscale handles everything.

How it works

  • All services share one tsnet.Server (like shared mode)
  • Each proxy is registered as a Tailscale VIP Service
  • FQDNs are auto-assigned by Tailscale (e.g. myapp.tailnet-name.ts.net)
  • No custom domain support — you cannot set tsdproxy.domain
  • No UDP support — only HTTPS, HTTP, and TCP ports
  • The shared server starts when the first service is created and stops when the last service is removed

Configuration

/config/tsdproxy.yaml
defaultProxyProvider: services

tailscale:
  providers:
    services:
      clientId: "your_client_id"
      clientSecret: "your_client_secret"
      tags: "tag:services-proxy"
      services: true
      hostname: "shared-services"
      autoApproveDevices: true
  dataDir: /data/

docker:
  local:
    host: unix:///var/run/docker.sock
    defaultProxyProvider: services

Container labels for services mode proxies:

services:
  app1:
    image: nginx:alpine
    labels:
      tsdproxy.enable: "true"
      tsdproxy.name: "app1"

  app2:
    image: nginx:alpine
    labels:
      tsdproxy.enable: "true"
      tsdproxy.name: "app2"

Requirements

Important

Services mode requires OAuth credentials (clientId + clientSecret). Auth keys alone do not provide access to the VIP Services API. A hostname must also be set — this is the shared Tailscale machine name.

Tip

Set autoApproveDevices: true to automatically approve new device registrations. Without this, new devices may require manual approval in the Tailscale admin console, which will block the proxy from starting.

Constraints

  • No custom domains — FQDNs are auto-assigned by Tailscale from the tailnet name
  • No UDP — VIP Services do not support UDP traffic
  • HTTPS, HTTP, and TCP only — all other protocols are rejected at startup
  • Mutually exclusive with shared — a provider cannot use both shared: true and services: true

Auto-remove conflicting devices

When switching from per-proxy or shared mode to services mode, existing Tailscale devices may share hostnames with the VIP services being created. This causes the Tailscale API to return a 409 "name is in use but is not a service" error, preventing the proxy from starting.

The autoRemoveConflicts option (default: false) enables automatic removal of conflicting devices when this error is encountered. After removing the device, TSDProxy retries the VIP service creation.

/config/tsdproxy.yaml
tailscale:
  providers:
    default:
      clientId: "your_client_id"
      clientSecret: "your_client_secret"
      tags: "tag:example"
      services: true
      hostname: "shared-services"
      autoRemoveConflicts: true

Warning

This deletes devices from your tailnet. When a 409 conflict is detected, TSDProxy will delete the conflicting device regardless of whether it is online or offline, and regardless of its tags. Only enable this if you understand the implications.

Tip

This option requires OAuth credentials (clientId + clientSecret) to access the Tailscale device API.

When to use services mode

  • You want fewer Tailscale machines without managing external DNS
  • Auto-assigned .ts.net FQDNs are acceptable for your use case
  • You don’t need UDP or custom domains

Proxy Provider Resolution

  1. Per-proxy label (tsdproxy.proxyprovider)
  2. Target provider default (defaultProxyProvider)
  3. Global default (top-level defaultProxyProvider)
  4. First available provider

Proxy Lifecycle

StateDescription
InitializingBeing created
StartingConnecting to Tailscale
AuthenticatingWaiting for auth (visit the auth URL)
AwaitingApprovalRegistered, waiting for admin approval in Tailscale
AuthFailedAuthentication failed (invalid key, bad tags, etc.)
DeviceConflictHostname collision with an existing Tailscale device
ReconcilingCleaning up stale devices before starting
RunningActive
StoppingShutting down
StoppedRemoved
PausedTemporarily disabled
ErrorFatal error

Note

The AwaitingApproval status appears when a node registers with Tailscale but an admin needs to approve it in the Tailscale admin console. This is separate from Authenticating, which means the node has no credentials at all and needs the user to visit an authentication URL.

Note

The AuthFailed status indicates a permanent authentication failure (invalid auth key, mismatched tags, or expired credentials). The proxy will not retry automatically unless authRetry is configured. Check the logs for the specific error.

Note

The DeviceConflict status means another Tailscale device with the same hostname already exists and is online. TSDProxy will not delete online devices. Either remove the conflicting device manually from the Tailscale admin console, or enable preventDuplicates for automatic cleanup of offline duplicates.

Last updated on