Skip to content
slopscale
Esc
↑↓navigate↵open⌘Jpreview
On this page

Identity tokens

Issue signed identity tokens with tailscale id-token, so machines log in to AWS, Google Cloud, Vault or other OpenID verifiers without a stored secret.

A machine can ask the server for a signed token that says which machine it is, the way Tailscale’s workload identity does. A service that trusts the server’s signing key can then let the machine in without a password: a secrets store, a cloud account’s OpenID federation, an internal API.

$ TAILSCALE_USE_WIP_CODE=1 tailscale id-token https://vault.example.com
eyJhbGciOiJFUzI1NiIsImtpZCI6...

The command is behind the client’s work-in-progress switch, as it is on Tailscale. Programs use the local API, GET /localapi/v0/id-token?aud=…, which the client forwards to the server over its control connection; the answer is {"id_token": "…"}.

What the token says

The token is a JSON Web Token signed with an ES256 key the server holds. It carries the standard claims and Tailscale’s:

Claim Value
iss The server URL
sub The machine’s MagicDNS name, laptop.ts.example.com
aud The audience the machine asked for
iat, nbf, exp Issued now, valid at once, for five minutes
jti A random token identifier
key The machine’s node key
addresses Its tailnet addresses
nid, node Its machine ID and name
domain The tailnet’s domain, the server’s host name
tags <domain>:tag:<name> for each tag, on a tagged machine
user, uid <domain>:<login> and the user’s ID, on a user-owned machine

A verifier finds the key through OpenID discovery at the server URL: /.well-known/openid-configuration names the key set, served at /.well-known/jwks.json. Anything that verifies an OpenID Connect ID token can verify these, with the server URL as the issuer and the audience as the client ID.

The signing key is made the first time a token is asked for and stored in the database, so every server on the same database signs with the same key and the key survives a restart. There is no key rotation; a verifier caches the key set for as long as it likes.

Checks

The server signs only for the machine that asks: the node key in the request must be the one bound to the connection, and the machine must be approved and not suspended. An expired key, a machine waiting for approval or a suspended one gets a 403.

Federating with a cloud

A machine’s identity token lets it assume a cloud identity without a stored credential, the way Tailscale’s workload identity federation does. The server acts as an OpenID Connect issuer: the issuer is the server URL, tokens are signed with ES256, discovery is at /.well-known/openid-configuration, and keys are at /.well-known/jwks.json. Cloud providers verify the signature and evaluate the claims listed above to issue short-lived credentials.

AWS

Register the server once as an OpenID Connect provider: aws iam create-open-id-connect-provider --url https://control.example.com --client-id-list sts.amazonaws.com. The client ID is the audience the machines will ask for; any string works as long as the trust policy names it.

A role trust policy allows sts:AssumeRoleWithWebIdentity, sets the Federated principal to the provider ARN, and checks "control.example.com:aud": "sts.amazonaws.com" and "control.example.com:sub": "laptop.ts.example.com" (StringEquals, or StringLike with *.ts.example.com for a fleet):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/control.example.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "control.example.com:aud": "sts.amazonaws.com",
          "control.example.com:sub": "laptop.ts.example.com"
        }
      }
    }
  ]
}

On the machine, assume the role with aws sts assume-role-with-web-identity --role-arn ... --role-session-name laptop --web-identity-token "$(tailscale id-token sts.amazonaws.com)" (with TAILSCALE_USE_WIP_CODE=1 as above), or write the token to a file and set AWS_ROLE_ARN and AWS_WEB_IDENTITY_TOKEN_FILE, or use credential_process. The token lasts five minutes, so a helper that refreshes the file is needed for long jobs.

AWS fetches the JWKS itself, so the server must be reachable from the internet over HTTPS with a publicly trusted certificate.

Google Cloud

Create a workload identity pool and an OIDC provider: gcloud iam workload-identity-pools create tailnet --location=global and gcloud iam workload-identity-pools providers create-oidc slopscale --location=global --workload-identity-pool=tailnet --issuer-uri=https://control.example.com --allowed-audiences=gcp --attribute-mapping="google.subject=assertion.sub,attribute.node=assertion.node,attribute.domain=assertion.domain". The tags claim is a list, so map single-valued claims only; --jwk-json-path uploads the key set when the server is not reachable from Google.

Grant roles/iam.workloadIdentityUser on a service account to principal://iam.googleapis.com/projects/NUMBER/locations/global/workloadIdentityPools/tailnet/subject/laptop.ts.example.com (or principalSet://.../attribute.domain/ts.example.com for every machine).

gcloud iam workload-identity-pools create-cred-config ... --executable-command='tailscale id-token gcp' --executable-output-file=... is the documented way to plug an executable in. The executable must print the JSON Google expects ({"version":1,"success":true,"token_type":"urn:ietf:params:oauth:token-type:jwt","id_token":"...","expiration_time":...}), so a two-line wrapper script is needed:

token=$(tailscale id-token gcp)
printf '{"version":1,"success":true,"token_type":"urn:ietf:params:oauth:token-type:jwt","id_token":"%s"}\n' "$token"

HashiCorp Vault

Enable the JWT auth method and configure it against the server’s discovery URL:

vault auth enable jwt
vault write auth/jwt/config oidc_discovery_url="https://control.example.com" bound_issuer="https://control.example.com"
vault write auth/jwt/role/machines role_type=jwt bound_audiences=vault user_claim=sub \
  bound_claims_type=glob bound_claims='{"sub":"*.ts.example.com"}' policies=default ttl=1h
vault write auth/jwt/login role=machines jwt="$(tailscale id-token vault)"

The bound_claims field can also pin tags (a list claim; Vault matches any element) so only tag:ci machines may log in.

The token names the machine, not a person. A stolen token is worth five minutes and only what the cloud role allows.

Last updated on September 27, 2026

Was this page helpful?