Well-Architected Framework
Secure GitLab CI/CD secrets with HashiCorp Vault
GitLab CI/CD can issue a signed ID token to pipeline jobs through the
id_tokens keyword. The token contains claims that identify what is running —
including the project, namespace, branch or tag reference, deployment
environment, and pipeline trigger source. This makes JWT/OIDC auth the natural
integration point for most GitLab deployments: no credential pre-provisioning,
no secrets stored in GitLab, and a Vault token scoped to the duration of the
job.
The secrets:vault keyword in GitLab CI/CD YAML handles the JWT exchange and
secret retrieval declaratively for GitLab Premium and Ultimate users. GitLab
Community Edition users achieve the same result with explicit shell commands
in the pipeline script. The underlying Vault auth mechanism is the same in
both cases.
Architecture
The pipeline-to-Vault authentication flow is based on GitLab-issued JWT tokens:
- GitLab generates a signed JWT when it dispatches the job and delivers it to the runner as part of the job payload; the runner exposes it as the environment variable declared in the
id_tokensblock - The job presents the JWT to Vault's JWT auth endpoint
- Vault fetches signing keys from the GitLab instance's JWKS endpoint (
https://gitlab.example.com/-/jwks) and validates the token signature - Vault evaluates
bound_claims—project_path,namespace_path,ref,environment, orpipeline_source— against the configured role - Vault issues a short-lived token scoped to the policies attached to the matching role
The job uses the Vault token to retrieve the secrets authorized for that role.

Runner hosting and auth method selection
The right auth method for your GitLab deployment depends on where runners
execute, not on your GitLab tier. JWT/OIDC auth works for all runner types
because GitLab issues a JWT regardless of where the runner runs. However,
JWT requires Vault to reach your GitLab instance's JWKS endpoint at runtime —
if Vault cannot reach https://your-gitlab.example.com/-/jwks, JWT auth is not
viable regardless of runner placement. When runners execute on infrastructure
you control, additional auth methods become available that can provide stronger
identity guarantees or eliminate the dependency on GitLab's token issuer entirely.
- GitLab.com shared runners — JWT/OIDC is the only viable option. GitLab.com shared runners run on Google Cloud infrastructure, but the platform identity is not assignable or accessible to individual tenants, so there is no instance identity you can configure Vault to trust.
- Self-hosted runners on Kubernetes — Kubernetes auth is available in addition to JWT. The runner pod's service account token is verified directly by the cluster API, independent of GitLab.
- Self-hosted runners on AWS, Azure, or GCP — Cloud provider auth methods (AWS IAM, Azure Managed Identity, GCP IAM) are available. The runner instance or container already has a platform identity that Vault can validate against the cloud provider's API — no credential to provision or rotate.
- Self-hosted runners on bare metal or on-premises networks — AppRole with response wrapping, or TLS certificate auth for persistent runner fleets. These are the primary auth options in this environment, not fallbacks — no cloud or cluster identity is available, and Vault may not be able to reach a self-hosted GitLab JWKS endpoint depending on network segmentation.
Authentication options
Use JWT/OIDC auth for runners on GitLab.com shared infrastructure and for self-hosted runners where GitLab-issued workload identity is the most practical identity source. For self-hosted runners, choose the auth method that best matches the underlying infrastructure.
The following table summarizes when each auth method fits best.
| Auth method | Security posture | Credential lifecycle | Best suited for |
|---|---|---|---|
| JWT / OIDC | High; per-job identity bound to GitLab project metadata | Token issued per job by GitLab, no rotation needed | GitLab.com shared runners; any self-hosted runner with access to GitLab's JWKS endpoint |
| Cloud provider auth methods | High; identity is the platform infrastructure itself | Managed by cloud provider; no rotation required | Self-hosted runners on EC2, Azure VMs/ACI, or GCE instances with an assigned instance identity |
| Kubernetes | Highest for in-cluster | Short-lived projected service account token, auto-rotated by Kubernetes | Self-hosted runners deployed as pods in a Kubernetes cluster |
| TLS Certificate | High; mutual authentication | Certificate with defined expiry; requires rotation process | Long-lived, persistent runner fleets where mTLS is part of the existing security posture |
| AppRole | Moderate; requires secure secret ID delivery | Secret ID is single-use with response wrapping; RoleID is long-lived | Bare metal or on-premises runners where no cloud or cluster identity is available |
JWT auth is the right choice for most GitLab CI/CD pipelines, whether using GitLab.com shared runners or self-hosted runners that can reach GitLab's JWKS endpoint.
GitLab's JWT contains the following claims that Vault can use as binding conditions:
| Claim | Example value | Use for |
|---|---|---|
sub | project_path:org/repo:ref_type:branch:ref:main | Primary identity — encodes project path and ref context |
namespace_path | org or group/subgroup | Restrict to a specific group or namespace |
project_path | org/repo | Restrict to a specific project |
ref | main | Restrict to a specific branch or tag |
ref_type | branch | Distinguish branch-triggered jobs from tag-triggered jobs |
environment | production | Restrict to jobs targeting a specific deployment environment |
pipeline_source | push | Restrict to jobs triggered by a specific event type |
The pipeline_source claim is particularly useful for production access
controls. It lets you restrict a Vault role to jobs triggered by a push
event, preventing scheduled pipelines, manually triggered jobs, or pipeline API
triggers from authenticating to production-scoped roles.
Auth mount configuration
Before configuring roles, enable the JWT auth method and point it at your
GitLab instance's JWKS endpoint. Set bound_issuer to restrict the mount to
tokens issued by your GitLab instance:
vault write auth/jwt/config \
jwks_url="https://gitlab.example.com/-/jwks" \
bound_issuer="https://gitlab.example.com"
The auth mount configuration tells Vault where to fetch signing keys for your GitLab instance and which issuer to trust. Vault can then validate GitLab-issued job tokens before it evaluates role constraints.
For GitLab.com, use https://gitlab.com as the issuer and JWKS base URL.
Role configuration
The Vault role's bound_claims are where you translate these into access
controls. An unconstrained role that accepts any JWT from your GitLab instance
is equivalent to giving every project access to those secrets. Always bind
roles to specific projects and contexts:
# Vault role scoped to a specific project, environment, and trigger source
# Without bound_claims, any pipeline in any project can authenticate
vault write auth/jwt/role/gitlab-production-deploy \
role_type=jwt \
bound_audiences="https://gitlab.example.com" \
bound_claims_type=glob \
bound_claims='{
"project_path": "your-group/your-project",
"ref": "main",
"ref_type": "branch",
"environment": "production",
"pipeline_source": "push"
}' \
user_claim=project_path \
token_policies=gitlab-production-deploy-policy \
ttl=15m
The role binds Vault access to a specific project, branch, environment, and trigger type. Vault issues a short-lived token only when the GitLab job presents the expected identity and pipeline context.
Workflow configuration
How you reference secrets in the job differs between GitLab Premium/Ultimate
and Community Edition, but the Vault auth mechanism is the same in both cases.
Use secrets:vault when you need static KV secret injection, or use the Vault
CLI after the token exchange when you need dynamic secrets.
GitLab Premium and Ultimate provide the secrets:vault keyword, which handles
JWT exchange and secret injection declaratively. The job authenticates to Vault
automatically using the id_tokens block and injects the secret value directly
into the named environment variable.
deploy_production:
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.example.com
secrets:
# Injects the value of the 'password' field into DATABASE_PASSWORD
DATABASE_PASSWORD:
vault: ci/production/db/password@secret # path: secret/data/ci/production/db, field: password
file: false
API_KEY:
vault: ci/production/api/key@secret
file: false
script:
- deploy.sh
The secrets:vault configuration lets GitLab exchange the job token and inject
the requested KV values without extra CLI logic in the job script. This pattern
fits static secret retrieval in Premium and Ultimate pipelines.
For dynamic secrets, use the Vault CLI directly in the job script after
authenticating with the id_tokens block:
deploy_production:
image: hashicorp/vault:1.19
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.example.com
script:
# Exchange the GitLab JWT for a Vault token
- export VAULT_TOKEN=$(vault write -field=token auth/jwt/login
role=gitlab-production-deploy
jwt=$VAULT_ID_TOKEN)
# Request short-lived dynamic AWS credentials for this job
- AWS_CREDS=$(vault read aws/creds/deploy-role -format=json)
- export AWS_ACCESS_KEY_ID=$(echo $AWS_CREDS | jq -r '.data.access_key')
- export AWS_SECRET_ACCESS_KEY=$(echo $AWS_CREDS | jq -r '.data.secret_key')
- export AWS_SESSION_TOKEN=$(echo $AWS_CREDS | jq -r '.data.security_token')
- deploy.sh
The job exchanges the GitLab token for a Vault token and then requests dynamic AWS credentials for the deployment. This pattern is useful when the pipeline needs leased credentials rather than static KV values.
Policy
The Vault policy for any role should follow least privilege. A pipeline that
deploys to production should not be able to read staging secrets, and a staging
pipeline should not be able to read production secrets. Because GitLab JWT claims can encode both the environment and the
pipeline_source, you can enforce this on the Vault role rather than relying
solely on GitLab's access controls.
# Vault policy: scope to exactly the paths this pipeline needs
path "secret/data/ci/production/db" {
capabilities = ["read"]
}
path "secret/data/ci/production/api" {
capabilities = ["read"]
}
# Deny staging paths explicitly if path structure overlaps
path "secret/data/ci/staging/*" {
capabilities = ["deny"]
}
The policy limits the job to the exact production secret paths it needs. Explicit deny rules are useful when path structures overlap and you want to prevent accidental access to staging data.
Security considerations
There are several GitLab-specific security considerations to align with your organizational risk tolerance and operational constraints:
pipeline_sourcerestricts access by trigger type. Binding a production role topipeline_source: pushprevents scheduled pipelines, manually triggered jobs, and pipeline API triggers from authenticating to that role. The distinction matters when scheduled or manually triggered pipelines run in a different security context than deployment pipelines but share the same project and branch.- GitLab protected environments. When binding a Vault role to the
environmentclaim, that binding is only as strong as GitLab's enforcement of who can deploy to that environment. Configure GitLab environment protection rules to restrict which branches and users can target protected environments — otherwise the Vault role constraint can be bypassed by any pipeline that can target the environment. - GitLab protected CI/CD variables. When using AppRole, the
VAULT_ROLE_IDand wrapping token should be stored as protected and masked CI/CD variables in GitLab, scoped to the specific environment or branch that needs them. Unprotected variables are available to all branches including feature branches and forks, which expands the blast radius if a pipeline is compromised. - Secret masking in CE pipelines. The
secrets:vaultkeyword in Premium and Ultimate automatically masks retrieved secret values in job logs. Community Edition pipelines using the Vault CLI directly do not get this behavior from the GitLab integration layer. If you use CLI-based retrieval in those pipelines, you need to handle log exposure and output discipline explicitly.
For a list of additional security considerations, refer to the CI/CD security considerations page.
Integrate HCP Vault Radar
Before migrating to Vault-based secrets management, Vault Radar can scan existing GitLab CI/CD pipeline definitions and connected repositories for credentials stored as hardcoded values or as GitLab CI/CD variables that should be moving to Vault. This is particularly important in organizations that have accumulated years of pipelines with credentials duplicated across projects, groups, and environments.
After migration, Vault Radar can monitor repositories continuously for new secret introductions — catching cases where developers hardcode credentials that should be going through Vault, or where a pipeline references a variable that was deprecated as part of the migration.
Refer to the HCP Vault Radar quick start tutorials to learn about HCP Vault Radar.
HashiCorp resources
Vault
- Read the JWT/OIDC auth method documentation to configure Vault to validate GitLab CI/CD JWT tokens.
- Read the Kubernetes auth method documentation to authenticate runner pods using projected service account tokens.
- Read the AWS auth method documentation to authenticate self-hosted runners running on AWS compute.
- Read the Azure auth method documentation to authenticate self-hosted runners running on Azure compute.
- Read the GCP auth method documentation to authenticate self-hosted runners running on Google Cloud.
- Read the AppRole auth method documentation to configure bootstrap credentials for isolated environments.
- Read the TLS certificate auth method documentation to configure mTLS-based authentication for persistent runners.
- Read the Response wrapping documentation to learn how to deliver single-use secret IDs securely.
- Read the AWS dynamic secrets engine documentation to generate short-lived AWS credentials for pipeline jobs.
- Follow the Dynamic secrets for database credential management tutorial to generate short-lived database credentials from Vault.
Terraform
- Read the Vault provider for Terraform documentation to manage Vault configuration as code.
- Watch Codify your JWT/OIDC Vault auth method with Terraform to learn how to manage Vault auth configuration with Terraform.
External resources
- Read Use HashiCorp Vault secrets in GitLab CI/CD for the official GitLab documentation on Vault integration.
- Read Authenticating and reading secrets with HashiCorp Vault for a step-by-step GitLab example of Vault authentication.
- Read Using external secrets in CI for GitLab's overview of external secrets management options.
- Read the GitLab CI/CD JWT token claims reference to understand the claims available for Vault role binding.
- Watch GitLab Unfiltered — How to integrate GitLab CI with HashiCorp Vault for a video walkthrough of the GitLab and Vault integration.
- Read Terraform code for JWT auth between HCP Vault Dedicated and GitLab for example Terraform code to configure JWT auth between GitLab and HCP Vault.
Next steps
In this section of managing CI/CD secrets, you learned how to bind GitLab CI/CD workload identity to Vault auth methods and how to select the right auth method based on where your runners are deployed. Secure GitLab CI/CD secrets with HashiCorp Vault is part of the Secure systems pillar.