GitLab CI With HashiCorp Vault: Dynamic Secrets Done Right
Stop pasting static credentials into CI variables. GitLab's native Vault integration uses JWT auth to fetch short-lived secrets at job runtime. Here's the setup.
- #gitlab
- #cicd
- #vault
- #secrets
- #security
- #devops
Most teams’ secrets story in GitLab CI is the same: a pile of masked CI/CD variables holding database passwords, cloud keys, and API tokens that were pasted in eighteen months ago and have never rotated. Every job that runs gets all of them in its environment. It works, until the day a malicious dependency or a leaky echo exfiltrates one, and now you’re rotating a credential nobody remembers the blast radius of.
The better model is dynamic, short-lived secrets fetched at job runtime from HashiCorp Vault, authenticated with the job’s own identity. GitLab has native support for this, and once it’s wired up, your CI variables stop being a secret graveyard. Here’s how I set it up.
Why Vault over static CI variables
Static CI variables have three structural problems: they’re long-lived (so a leak is a standing liability), they’re broadly scoped (every job sees them), and they’re invisible to audit (you can’t tell who used which secret when). Vault flips all three. Secrets are minted fresh per job, scoped to exactly what that job’s identity is allowed, and every fetch is logged in Vault’s audit trail.
The glue is JWT authentication. Every GitLab CI job is issued a signed CI_JOB_JWT (or the newer ID-token) that encodes claims about the job: project path, branch, environment, whether it’s protected. Vault verifies that token against GitLab’s public keys and grants access based on those claims. No shared secret travels between the two systems.
Configure Vault to trust GitLab
On the Vault side, enable the JWT auth method and point it at your GitLab instance’s OIDC discovery endpoint:
vault auth enable jwt
vault write auth/jwt/config \
oidc_discovery_url="https://gitlab.com" \
bound_issuer="https://gitlab.com"
Then create a role that maps job claims to a policy. This is where you scope access — only protected main builds of a specific project get the production database role:
vault write auth/jwt/role/prod-db - <<EOF
{
"role_type": "jwt",
"user_claim": "user_email",
"bound_claims_type": "glob",
"bound_claims": {
"project_id": "42",
"ref": "main",
"ref_protected": "true"
},
"policies": ["prod-db-read"],
"ttl": "10m"
}
EOF
The bound_claims block is the security boundary. A feature branch can’t assume this role because its JWT carries ref_protected: false. A different project can’t, because its project_id doesn’t match. Spend your effort here — a loose bound_claims is the whole game.
Fetch secrets in the job
With the role in place, GitLab’s secrets: keyword does the fetch declaratively. Define an ID token aud-scoped to Vault and pull the secret:
deploy:
stage: deploy
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.example.com
secrets:
DATABASE_URL:
vault:
engine:
name: kv-v2
path: secret
path: ops/prod/db
field: url
token: $VAULT_ID_TOKEN
script:
- ./deploy.sh # DATABASE_URL is in the environment, freshly fetched
GitLab authenticates with VAULT_ID_TOKEN, fetches ops/prod/db, and injects the url field as the DATABASE_URL environment variable for this job only. When the job ends, the lease can be revoked. Nothing long-lived was ever stored in GitLab.
Dynamic secrets are the real prize
KV secrets are a fine start, but Vault’s dynamic secret engines are where this gets genuinely safer. Instead of fetching a stored password, Vault generates a credential on demand with a short TTL — a database user that exists for ten minutes, a cloud credential that self-destructs after the job.
migrate:
id_tokens:
VAULT_ID_TOKEN: { aud: https://vault.example.com }
secrets:
DB_USER:
vault:
engine: { name: database, path: database }
path: creds/migrator
field: username
token: $VAULT_ID_TOKEN
DB_PASS:
vault:
engine: { name: database, path: database }
path: creds/migrator
field: password
token: $VAULT_ID_TOKEN
script:
- ./run-migrations.sh
Vault creates a throwaway database user scoped to the migrator role, hands it to the job, and revokes it when the lease expires. Even if that credential leaks in a log, it’s worthless minutes later and was never able to do more than run migrations.
The mistakes that undermine it
I’ve seen Vault integrations that were strictly less safe than the static variables they replaced. Avoid these:
- Don’t echo secrets. Fetching short-lived secrets is pointless if the next line is
echo $DATABASE_URLinto a log. Treat the fetched value like radioactive material. - Don’t make TTLs long “to be safe.” A 24-hour TTL on a per-job secret defeats the purpose. Make it just longer than the job needs — minutes, not hours.
- Don’t skip
ref_protected. Without it, any contributor opening a feature-branch MR can craft a pipeline that assumes production roles. Bind to protected refs for anything sensitive. - Don’t forget to enable Vault audit logging. The audit trail is half the value; turn it on before you migrate real secrets.
Where to go from here
Native GitLab-Vault integration replaces a static secrets graveyard with short-lived, job-scoped, fully audited credentials authenticated by the job’s own identity. Configure tight bound_claims, prefer dynamic secret engines over stored KV where you can, keep TTLs short, and never log what you fetch. The result is a CI environment where a leaked secret is a minor, time-boxed inconvenience instead of a fire drill.
For more on hardening GitLab pipelines, see our GitLab CI/CD guides. And when reviewing changes to Vault roles or secrets: blocks, our AI code review assistant helps catch loose claim bindings and accidental secret logging.
Vault configuration and claim names vary across GitLab and Vault versions. Validate the integration in a non-production project before migrating real secrets.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.