Skip to content
Latchkey

Vault "token is expired" / "permission denied" from an expired token in CI

The token CI is using has passed its TTL. Vault rejects expired tokens with "token is expired" (or a plain "permission denied" once the token is fully gone), so the read fails partway through a long job.

What this error means

A call that worked earlier in the job now fails with "Code: 403 ... * token is expired", often on a long-running or retried pipeline.

vault
Error making API request.

URL: GET https://vault.example.com/v1/secret/data/ci/app
Code: 403. Errors:

* token is expired

Common causes

The token TTL is shorter than the job

A login-issued token with a small token_ttl expires before a long build finishes, so later reads fail.

The token was not renewed and is not renewable

The pipeline held one token for the whole run without renewing it, and once past TTL it is invalid.

How to fix it

Raise the role token TTL

  1. Set a token_ttl on the auth role that comfortably exceeds the longest job.
  2. Keep a token_max_ttl cap for safety.
  3. Re-run so the freshly issued token lives long enough.
Terminal
vault write auth/approle/role/ci \
  token_ttl=30m token_max_ttl=1h

Fetch secrets once, early

Read all needed secrets right after login into masked env vars, so a later expiry cannot break the job.

.github/workflows/ci.yml
- uses: hashicorp/vault-action@v3
  with:
    url: ${{ secrets.VAULT_ADDR }}
    method: approle
    roleId: ${{ secrets.VAULT_ROLE_ID }}
    secretId: ${{ secrets.VAULT_SECRET_ID }}
    secrets: secret/data/ci/app dbUrl | DB_URL

How to prevent it

  • Size token_ttl to exceed your slowest pipeline.
  • Fetch secrets once at the start rather than repeatedly late in the job.
  • Do not depend on a single short-lived token surviving a long run.

Related guides

Tired of flaky CI? Latchkey auto-heals failed jobs and retries them for you. Start free →