Skip to content
Latchkey

Vault JWT/OIDC Auth from GitHub Actions

GitHub Actions can log in to Vault with its built-in OIDC token, so no static Vault secret needs to live in the repo.

GitHub issues a short-lived OIDC JWT to each workflow run. Vault JWT auth verifies it against GitHub OIDC and issues a token, with bound_claims restricting which repos and refs can log in.

What it does

Vault JWT auth validates a GitHub Actions OIDC token signed by https://token.actions.githubusercontent.com. A role binds claims like sub, repository, and ref so only the intended workflows authenticate, then issues a token with the role policies. The login uses jwt=<the OIDC token> and a role.

Vault setup

Terminal
vault auth enable jwt
vault write auth/jwt/config \
  oidc_discovery_url="https://token.actions.githubusercontent.com"
vault write auth/jwt/role/github-ci \
  role_type="jwt" user_claim="actor" \
  bound_audiences="https://github.com/my-org" \
  bound_claims='{"repository":"my-org/my-repo","ref":"refs/heads/main"}' \
  token_policies="ci" token_ttl=15m

Workflow login

.github/workflows/ci.yml
permissions:
  id-token: write
  contents: read
steps:
  - name: Get OIDC token and login to Vault
    run: |
      JWT=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
        "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://github.com/my-org" \
        | jq -r '.value')
      export VAULT_TOKEN=$(vault write -field=token \
        auth/jwt/login role=github-ci jwt="$JWT")

In CI

The workflow must request permissions: id-token: write, or the OIDC token endpoint is not available. The audience you request from GitHub must match bound_audiences on the role. The official hashicorp/vault-action wraps this exchange, but the curl shows exactly what it does under the hood. No long-lived secret is stored anywhere.

Common errors in CI

"Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable" means the job is missing id-token: write permission. "error validating token: error validating claims: ... claim 'repository' does not match" means bound_claims rejected the run (wrong repo or ref). "no known key successfully validated the token signature" usually means a misconfigured oidc_discovery_url or bound_audiences mismatch with the requested audience.

Related guides

Run this faster and cheaper on Latchkey managed runners. Start free →