Tailscale: OAuth Client Detail

Level 3 (Detail) — OAuth client flow, key minting, and lifecycle.

Concept

TazLab uses two Tailscale OAuth clients to generate short-lived auth keys dynamically, avoiding persistent keys in Terraform state. Both are defined in ephemeral-castle/tailscale/main.tf.

Bootstrap OAuth Client

Used for device authentication (joining TazPod and operator machines to the tailnet).

resource "tailscale_oauth_client" "bootstrap" {
  description = "tazlab-bootstrap"
  scopes      = ["auth_keys", "devices"]
  tags        = ["tag:tazpod"]
}
  • Tags tag:tazpod are automatically assigned to devices joining with keys from this client
  • Scopes auth_keys + devices allow key creation and device management

Kubernetes Operator OAuth Client

Used by the Tailscale Kubernetes Operator to create proxy devices for Ingress and LoadBalancer exposure.

resource "tailscale_oauth_client" "k8s_operator" {
  description = "tazlab-k8s-operator"
  scopes      = ["devices", "auth_keys", "services"]
  tags        = ["tag:k8s-operator"]
}
  • Scope services is required to avoid bug #19471 (operator misclassifies 404 on /vip-services as InvalidOAuth)
  • Without services scope, the operator cannot create Ingress or LoadBalancer proxy devices

Credential Storage

Client credentials are stored as plain files in ~/secrets/:

FileContent
~/secrets/tailscale-oauth-client-idBootstrap OAuth client ID
~/secrets/tailscale-oauth-client-secretBootstrap OAuth client secret
~/secrets/tailscale-operator-client-idOperator OAuth client ID
~/secrets/tailscale-operator-client-secretOperator OAuth client secret

These are provisioned by the ephemeral-castle creation flow and persist in the operator’s encrypted vault. Operator credentials are also pushed to Vault at secret/tazlab-k8s/static/infra/tailscale/.

Key Minting Flow

File: ~/.local/bin/tazpod-tailscale-upmint_authkey() function

OAuth credentials → POST /oauth/token → access_token (1h)
                                         │
                                         ▼
                                  POST /tailnet/-/keys → auth_key (1h)
                                         │
                                         ▼
                                  tailscale up --authkey=<key>
  1. Reads OAuth client ID + secret from ~/secrets/
  2. POST to https://api.tailscale.com/api/v2/oauth/token with grant_type=client_credentials + scope=auth_keys
  3. Extract access_token from response
  4. POST to https://api.tailscale.com/api/v2/tailnet/-/keys with:
    • reusable: true, ephemeral: true, preauthorized: true
    • tags: ["tag:tazpod"]
    • expirySeconds: 3600 (1 hour)
  5. Extract key from response
  6. Export as TS_AUTHKEY environment variable

Fallback

If OAuth client credentials are missing, the script falls back to TAILSCALE_API_KEY + TAILSCALE_TAILNET for direct API key minting.

Key Properties

PropertyValue
Reusabletrue (one key can join multiple devices)
Ephemeraltrue (devices are removed on disconnect)
Preauthorizedtrue (no manual approval needed)
Expiry3600 seconds (1 hour)
Tagstag:tazpod (bootstrap), tag:k8s-operator (operator)

See Also