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
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=15mWorkflow login
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.