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!
