Chapter 11 of 16 · CI user · GitLab pipeline

GitLab CI with ID Tokens

Exchange a job-scoped GitLab OIDC ID token for narrow Vault access without storing a reusable CI credential.

Chapter contract

Persona: CI user · GitLab pipelinePhase: Applications and pipelinesEstimated time: 35 minutes

Prerequisites:

A GitLab project design and a platform-supplied Vault JWT role; no live pipeline is required.

User-visible outcome:

A protected GitLab job can request one file-delivered field through an audience-bound ID token.

Learning objectives

Consumer flow

GitLab ID tokenstep 1Vault JWT rolestep 2Job Vault tokenstep 3File-type secretstep 4

Ownership boundary

OwnerConsumer-facing responsibility
You ownOwns job identity, protected refs, logs, artifacts, cancellation, and cleanup.
Platform team ownsDefines the exact value or file contract consumed by the job.
Vault ownsOwns JWT trust, claim bindings, policy, token TTL, and audit.

Core concepts

ID token

A job-scoped signed JWT requested with an audience.

Bound audience

The recipient value Vault validates.

Bound claims

Project, namespace, ref, or protection constraints.

File variable

A temporary file whose variable value is its path.

Commands and configuration

vault-read:
  id_tokens:
    VAULT_ID_TOKEN:
      aud: https://vault.example.internal
  secrets:
    LOCKBOX_CONFIG:
      vault: orders-api/feature_mode@lockbox-kv
      token: $VAULT_ID_TOKEN
  script:
    - test -s "$LOCKBOX_CONFIG"
    - test "$(wc -l < "$LOCKBOX_CONFIG")" -eq 1

Platform-team supplied context: Vault JWT role

{
  "role_type": "jwt",
  "policies": ["lockbox-gitlab"],
  "token_explicit_max_ttl": 60,
  "user_claim": "user_email",
  "bound_audiences": ["https://vault.example.internal"],
  "bound_claims": {
    "project_id": "42",
    "ref_protected": "true"
  }
}

Secret-safety stop

Stop: Do not decode or print the ID token in CI logs. Use job metadata and Vault audit correlation.

Failure drill

SymptomJWT login reports audience or claim failure.
Likely causesAudience differs, issuer metadata changed, ref unprotected, or project claims do not match.
Inspect safelyCompare audience, project path, ref, protection, job time, and role name.
Do not printID token, Vault token, secret file, claims payload, or runner environment.

Break it / fix it

Break it: Change the job ID-token audience so it no longer matches the Vault role.

Show diagnosis and fix

Fix it: Restore the exact audience and re-check project, ref, protection, issuer, auth path, and mount.

Escalate with time, endpoint, auth path, role, mount, namespace when relevant, status code, request ID, and sanitized error class. Never attach a credential or response body.

Try it

Bootstrap boundary

Platform-supplied external configuration is represented by reserved example inputs; this lab applies nothing.

Cost:

$0; offline validation with no external resource changes

Starting state:

Use the tracked example and offline validator; real GitLab, AWS, and Kubernetes configuration remains platform-owned.

User actions:

Review the provided CI example and run its offline checker for audience, file delivery, logging, artifacts, and legacy token use.

cd learn-vault/labs/gitlab-ci
python3 check_example.py
sed -n '1,80p' .gitlab-ci.example.yml
git grep -n 'CI_JOB_JWT' -- . ':!learn-vault/labs/gitlab-ci/check_example.py' && exit 1 || true

Success criteria:

Complete every item in the Verify section using metadata-only evidence; no secret value appears in terminal output or tracked files.

Troubleshooting:

Use the Failure drill and Break it / fix it evidence fields. Stop before broadening policy, weakening identity or TLS checks, or copying secret-bearing diagnostics.

Verify

  1. The example requests an explicit Vault audience.
  2. The secret is delivered as a file and is not an artifact.
  3. The checker contacts neither GitLab nor Vault.
Evidence rule: retain only status, policy names, TTLs, versions, timestamps, fingerprints, and error classes. Never retain secret values.

Cleanup

No external resources exist. Delete any copied job output before sharing logs.

Project Lockbox increment

The release job can read one Lockbox path from protected refs, with no reusable Vault variable.

Quick check

Why declare aud?

Vault must know the token was minted for it.

Why bind project and ref?

Another valid GitLab job must not match this role.

What is default delivery?

A temporary file path.

Upload it for debugging?

No. Artifacts outlive the job and broaden access.

Gotchas

Recap and next

CI exchanges its existing job identity for short-lived access constrained to the exact trusted pipeline context.

Primary references

Continue →