---
title: "Identity tokens"
description: "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](https://tailscale.com/kb/1276/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.

```console
$ 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):

```json
{
  "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:

```console
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:

```console
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.
