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

  1. Zero Secret Leakage: Even if a developer accidentally runs printenv or prints build artifacts, there are no permanent AWS or database passwords stored in GitLab.
  2. Deterministic Expiration: Ephemeral tokens expire automatically after 15 minutes, neutralizing replay attacks.
  3. No Rotation Ceremonies: Security teams never have to schedule quarterly downtime to rotate static cloud passwords.
  4. Complete IAM Auditability: Cloud and Vault access logs record the exact GitLab project, user, and commit SHA responsible for every request.

Resources & Links

Has your team transitioned to OIDC in your CI/CD pipelines? Share your security architecture below!