Zero-Secret Pipelines: Hardening GitLab CI/CD with OIDC & HashiCorp Vault
Every security team's worst nightmare begins with a developer committing a secret to Git, or an attacker compromising a project's CI/CD variable settings.
For years, connecting GitLab CI/CD to external infrastructure (AWS, Google Cloud, Azure, or internal databases) meant creating static, long-lived credentials:
- An AWS IAM Access Key and Secret Key pasted into GitLab CI variables.
- A database password stored in plaintext inside runner environments.
- Tokens that rarely get rotated and live indefinitely.
If a rogue merge request or malicious dependency executes env inside a build script, your entire cloud infrastructure is compromised.
The modern industry gold standard for CI/CD security is Zero Stored Secrets using OpenID Connect (OIDC).
In this practical guide, I will show you how to configure GitLab CI/CD’s native id_tokens to dynamically authenticate against HashiCorp Vault or cloud providers, minting short-lived (15-minute) credentials on demand with zero static secrets stored in GitLab.
1. The Architecture: Static Secrets vs. OIDC Federated Trust
+-------------------------------------------------------------+
| TRADITIONAL STATIC MODEL |
| |
| GitLab CI Runner reads: |
| $AWS_ACCESS_KEY_ID = "AKIA..." (Static, never rotated) |
| $AWS_SECRET_ACCESS_KEY = "wJal..." (Massive leak risk!) |
+-------------------------------------------------------------+
vs.
+-------------------------------------------------------------+
| ZERO-SECRET OIDC FEDERATION |
| |
| 1. GitLab Runner generates cryptographically signed JWT |
| token (`id_tokens: GITLAB_OIDC_TOKEN`) |
| 2. Token contains claims: project_id, branch, user_login |
| 3. HashiCorp Vault / Cloud IAM verifies GitLab's OIDC pubkey|
| 4. Vault issues temporary 15-minute token scoped to branch |
| 5. Zero credentials stored in GitLab repository settings! |
+-------------------------------------------------------------+
2. Step 1: Configuring HashiCorp Vault for GitLab OIDC
Inside your HashiCorp Vault cluster, enable the JWT/OIDC authentication engine and establish trust with GitLab’s public identity provider:
# 1. Enable the JWT auth method
vault auth enable jwt
# 2. Configure Vault to trust GitLab's OpenID Connect issuer
vault write auth/jwt/config \
oidc_discovery_url="https://gitlab.com" \
bound_issuer="https://gitlab.com"
Next, define a Vault role that restricts access based on strict GitLab JWT claims (e.g., only commits on the protected main branch of project my-org/core-infra):
vault write auth/jwt/role/gitlab-production-deployer \
role_type="jwt" \
bound_audiences="https://vault.mycompany.com" \
user_claim="user_email" \
bound_claims_type="glob" \
bound_claims='{
"project_path": "my-org/core-infra",
"ref": "main",
"ref_type": "branch"
}' \
policies="production-database-read" \
ttl="15m"
If an attacker tries to run a pipeline from an unapproved fork or feature branch, Vault automatically rejects the token!
3. Step 2: Minting OIDC Tokens in .gitlab-ci.yml
In your GitLab CI configuration, declare id_tokens with an audience matching your Vault configuration. GitLab automatically signs this JSON Web Token using its private key:
stages:
- deploy
deploy_production:
stage: deploy
image: hashicorp/vault:latest
environment: production
id_tokens:
# GitLab generates a cryptographically signed OIDC JWT
VAULT_ID_TOKEN:
aud: https://vault.mycompany.com
variables:
VAULT_ADDR: "https://vault.mycompany.com:8200"
rules:
- if: $CI_COMMIT_BRANCH == "main"
script:
- |
echo "Authenticating against HashiCorp Vault with ephemeral OIDC JWT..."
# 1. Exchange the GitLab OIDC token for a 15-minute Vault Client Token
VAULT_AUTH_RESPONSE=$(vault write -format=json auth/jwt/login \
role="gitlab-production-deployer" \
jwt="$VAULT_ID_TOKEN")
export VAULT_TOKEN=$(echo $VAULT_AUTH_RESPONSE | jq -r '.auth.client_token')
# 2. Fetch the dynamic database secret from Vault
DB_SECRET=$(vault kv get -format=json secret/production/database)
DB_PASSWORD=$(echo $DB_SECRET | jq -r '.data.data.password')
echo "Executing deployment with ephemeral secret..."
./scripts/deploy.sh --db-pass "$DB_PASSWORD"
4. Key OIDC Claims Available for Role Hardening
GitLab embeds rich contextual metadata directly into every OIDC token:
| JWT Claim | Example Value | Security Use Case |
|---|---|---|
iss |
https://gitlab.com |
Verifies the token originated from GitLab. |
project_path |
my-org/core-infra |
Restricts secrets to exact repository paths. |
ref |
main |
Ensures production secrets never leak to feature branches. |
environment |
production |
Binds access to protected GitLab environments. |
user_login |
hrlpavan |
Enforces identity auditing for human triggers. |
ci_config_sha |
a1b2c3d4... |
Pins access to specific audited CI configurations. |
5. Why OIDC Replaces Static API Keys Forever
-
Zero Secret Leakage: Even if a developer accidentally runs
printenvor prints build artifacts, there are no permanent AWS or database passwords stored in GitLab. - Deterministic Expiration: Ephemeral tokens expire automatically after 15 minutes, neutralizing replay attacks.
- No Rotation Ceremonies: Security teams never have to schedule quarterly downtime to rotate static cloud passwords.
- Complete IAM Auditability: Cloud and Vault access logs record the exact GitLab project, user, and commit SHA responsible for every request.
Resources & Links
- GitLab Profile: gitlab.com/hrlpavan
- GitLab Contributor Profile: contributors.gitlab.com/users/hrlpavan
- GitHub: @hrlpavan
Has your team transitioned to OIDC in your CI/CD pipelines? Share your security architecture below!
Top comments (0)