Tailscale Workload Identity Federation

Put a VM on your Tailscale network, or let it call the Tailscale API, without copying a Tailscale auth key or OAuth secret onto it. Instead, the VM asks exe.dev for a short-lived token that proves which VM it is, and Tailscale accepts that token because you told it to trust exe.dev.

You need two browser tabs: the exe.dev Integrations page and Tailscale's Trust credentials page. You must be allowed to add trust credentials in your Tailscale network.

The integration only hands out the token. Once a VM has joined, it stays on your Tailscale network until it logs out or you remove it in Tailscale, even if you detach or delete the integration. The steps below register VMs as ephemeral, so Tailscale removes them on its own when they go away.

1. Start the integration in exe.dev

On the Integrations page, add an Identity Federation integration and choose Tailscale. Give it a name, such as tailscale-wif, and attach it to the VM or tag that should use it.

The dialog shows an Issuer and a Subject. You will paste both into Tailscale in the next step, and Tailscale will give you a Client ID to paste back. You can click Run now and add the Client ID later by editing the integration.

2. Tell Tailscale to trust exe.dev

In Tailscale, open Trust credentials → Add, select OpenID Connect, and fill in:

Tailscale field What to enter
Issuer Choose Custom issuer and paste the exe.dev Issuer
Subject Paste the exe.dev Subject exactly as shown. Don't use *.
Audience Leave it; Tailscale generates one from the Client ID
Scopes Turn on Auth Keys write. Only add more if the VM needs to call the Tailscale API.
Tags Appears once you turn on a scope that can add devices. Pick the tags the VM should get, such as tag:exe.

If you'd rather use the Tailscale API:

curl -fsS -X POST https://api.tailscale.com/api/v2/tailnet/-/keys \
  -H "Authorization: Bearer $TS_API_TOKEN" \
  -H 'Content-Type: application/json' \
  -d "{
    \"keyType\": \"federated\",
    \"description\": \"exe.dev\",
    \"issuer\": \"$EXE_WIF_ISSUER\",
    \"subject\": \"$EXE_WIF_SUBJECT\",
    \"scopes\": [\"auth_keys\"],
    \"tags\": [\"tag:exe\"]
  }"

Copy the Client ID Tailscale shows. It isn't a secret.

3. Enter the Client ID and save

Back in exe.dev, open the integration if you already saved it, and paste the Client ID. The Audience fills in as api.tailscale.com/<Client ID>, matching what Tailscale generated. Optionally enter the Tags you picked in Tailscale so the VM can read them later. Click Run or Save.

Or from the CLI. The audience is api.tailscale.com/<Client ID>, but the command prints the Issuer and Subject only after it creates the integration, so you don't have the Client ID yet. Pass a stand-in, then replace it once Tailscale has given you one:

ssh exe.dev integrations add wif --name tailscale-wif \
  --audience api.tailscale.com/pending --consumer tailscale --attach vm:example-vm

ssh exe.dev integrations edit tailscale-wif \
  --audience api.tailscale.com/CLIENT_ID \
  --metadata=client_id=CLIENT_ID --metadata=tags=tag:exe

Add --team to add for a team integration. edit replaces every --metadata value, so pass all of them each time.

Use it from the VM

On an attached VM, the integration is reachable at https://<name>.int.exe.xyz (https://<name>.team.exe.xyz for a team integration). /metadata returns the Client ID and tags you entered. /token returns a fresh token.

Join your Tailscale network

Install Tailscale on the VM, then run this. The token is only needed once; the VM stays joined across reboots.

EXE_WIF_URL=https://tailscale-wif.int.exe.xyz
META="$(curl -fsS "$EXE_WIF_URL/metadata")"
CLIENT_ID="$(echo "$META" | jq -er .client_id)"
TAGS="$(echo "$META" | jq -r '.tags // "tag:exe"')"
ID_TOKEN="$(curl -fsS "$EXE_WIF_URL/token" | jq -er .token)"

sudo tailscale up \
  --client-id="${CLIENT_ID}?ephemeral=true&preauthorized=true" \
  --id-token="$ID_TOKEN" \
  --advertise-tags="$TAGS"

unset ID_TOKEN

ephemeral=true tells Tailscale to remove the VM from your network once it goes offline, so a deleted VM doesn't leave a stale entry behind. Use ephemeral=false if the VM should stay listed while it's off. The tags you advertise must be ones you picked in step 2.

To leave the network, run sudo tailscale logout on the VM.

Call the Tailscale API

If you gave the credential API scopes in step 2, trade the exe.dev token for a Tailscale API token:

EXE_WIF_URL=https://tailscale-wif.int.exe.xyz
CLIENT_ID="$(curl -fsS "$EXE_WIF_URL/metadata" | jq -er .client_id)"
JWT="$(curl -fsS "$EXE_WIF_URL/token" | jq -er .token)"

TS_API_TOKEN="$(
  curl -fsS -X POST https://api.tailscale.com/api/v2/oauth/token-exchange \
    -d "client_id=$CLIENT_ID" -d "jwt=$JWT" | jq -er .access_token
)"

curl -fsS https://api.tailscale.com/api/v2/tailnet/-/devices \
  -H "Authorization: Bearer $TS_API_TOKEN"

unset JWT TS_API_TOKEN

The Tailscale Terraform provider can do this for you: set TAILSCALE_OAUTH_CLIENT_ID to the Client ID and TAILSCALE_IDENTITY_TOKEN to file: followed by the path of a file holding a fresh exe.dev token.

About the tokens

  • Get a new token from /token each time you join or call the API. They last 5 minutes by default.
  • Tailscale API tokens are short-lived too. Get a new one before the old one runs out.
  • Treat both like passwords. Don't print, log, or commit them.

Troubleshooting

  • /token returns 403. The integration isn't attached to this VM.
  • Tailscale rejects the token. Check that the Issuer and Subject in Tailscale match the integration exactly, that the integration's Client ID is the one Tailscale shows for this credential, and that --advertise-tags only lists tags you picked in Tailscale. Tailscale's list of token exchange errors explains each one.
  • tailscale up asks for --audience. That option only works on cloud platforms Tailscale can fetch tokens from itself. On exe.dev, pass --id-token as shown above.

Keep it narrow

  • Make one exe.dev integration per Tailscale trust credential.
  • Attach it only to the VMs or tag that need it.
  • Use the exact Subject in Tailscale. A * would let any integration under the same Issuer in.
  • Only turn on Auth Keys unless the VM must call the API, and never OAuth Keys, Federated Keys, or All.

Tailscale references: