Chapter 27Lesson 05~245 minutes

Checkpoint Lab — ID Tokens, OIDC Workload Identity, Cloud Federation, Vault Authentication, and Secretless Deployments

Prove a mock secretless deployment where one trusted claim set receives a short-lived least-privilege credential and attacker-like claims are denied with auditable evidence.

CheckpointSecretless deploymentAllowed/denied claimsEvidenceRecovery

Learning objectives

Checkpoint mission

  • Build a mock secretless deployment flow with no stored cloud credential.
  • Prove one trusted claim set succeeds and at least two attacker-like variants fail.
  • Prove the issued synthetic credential is short-lived and least-privilege.
  • Perform one bounded external mutation and verify it independently.
  • Produce an evidence packet that separates source/configuration, identity, provider decision, credential scope and target state.

1. Scenario and safety contract

You are releasing a synthetic marker to a disposable staging target. The provider must authorize only project 42001, protected main, environment staging, and audience https://sts.example.test. A feature branch and a wrong-audience token must fail. The returned role can perform only write:staging-marker.

No real identity material: this checkpoint uses JSON claim fixtures and generated non-secret credential IDs. Do not substitute a live GitLab ID token, real cloud key, Vault token, production role, or customer repository.

2. Assumptions and preflight

  • GitLab ID-token behavior verified 2026-09-12; explicit id_tokens is Free/Premium/Ultimate.
  • CI_JOB_JWT/CI_JOB_JWT_V2 are not used; they were removed in GitLab 17.0.
  • Python 3.11+ standard library only for the mandatory path.
  • Any real AWS/Azure/GCP/Vault integration is optional and must use a disposable account/project/namespace and least privilege.
  • Native secrets:vault is Premium/Ultimate; this lab does not depend on it.
python --version
git --version
git status --short
git rev-parse HEAD

3. Predict state changes before execution

Prediction Before Expected after Independent verification
Allowed identity No exchange Provider simulator returns allow for main/staging/exact audience. Decision JSON + all checks true.
Attacker-like branch No exchange Provider simulator denies feature branch/unprotected ref. Denial JSON + failing ref/ref_protected checks.
Wrong audience No exchange Provider simulator denies mismatched audience. Denial JSON + audience check false.
Temporary scope No credential metadata Allowed exchange returns staging-writer, one action, 300-second TTL. Exchange result without bearer secret.
External target No marker Only allowed exchange can write staging marker. Read-back content + SHA-256.

4. Create the checkpoint files

glci-oidc-checkpoint/
├── trust-policy.json
├── claims-main.json
├── claims-feature.json
├── claims-wrong-aud.json
├── mock_exchange.py
├── deploy_marker.py
├── evidence/
└── target/

5. Trust policy

{
  "issuer": "https://gitlab.example.test",
  "audience": "https://sts.example.test",
  "job_project_id": "42001",
  "ref": "main",
  "ref_type": "branch",
  "ref_protected": true,
  "environment": "staging",
  "deployment_tier": "staging",
  "role": "staging-writer",
  "actions": ["write:staging-marker"],
  "ttl_seconds": 300
}

6. Claims fixtures

Create three fixtures with the same issuer/project/SHA schema. The intended one has main, protected ref, staging environment and the exact STS audience. The feature fixture changes only ref + protection. The wrong-audience fixture changes only aud. Keep the differences minimal so each denial has one causal explanation.

{
  "iss": "https://gitlab.example.test",
  "aud": "https://sts.example.test",
  "job_project_id": "42001",
  "pipeline_source": "push",
  "pipeline_id": "8100",
  "job_id": "81001",
  "ref": "main",
  "ref_type": "branch",
  "ref_protected": true,
  "environment": "staging",
  "deployment_tier": "staging",
  "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "iat": 1789230000,
  "exp": 1789230300
}

For claims-feature.json, set job_id=81002, ref=feature/unsafe, ref_protected=false. For claims-wrong-aud.json, set job_id=81003 and aud=https://vault.example.test.

7. Exchange implementation

import json, sys, hashlib
from pathlib import Path
claims = json.loads(Path(sys.argv[1]).read_text())
policy = json.loads(Path("trust-policy.json").read_text())
now = 1789230100
checks = {
  "issuer": claims.get("iss") == policy["issuer"],
  "audience": claims.get("aud") == policy["audience"],
  "job_project_id": str(claims.get("job_project_id")) == policy["job_project_id"],
  "ref": claims.get("ref") == policy["ref"],
  "ref_type": claims.get("ref_type") == policy["ref_type"],
  "ref_protected": claims.get("ref_protected") is policy["ref_protected"],
  "environment": claims.get("environment") == policy["environment"],
  "deployment_tier": claims.get("deployment_tier") == policy["deployment_tier"],
  "time": claims.get("iat", now + 1) <= now < claims.get("exp", 0),
}
decision = "allow" if all(checks.values()) else "deny"
policy_hash = hashlib.sha256(Path("trust-policy.json").read_bytes()).hexdigest()
out = {"decision":decision,"checks":checks,"job_id":claims.get("job_id"),"sha":claims.get("sha"),"policy_sha256":policy_hash}
if decision == "allow":
    out["credential"] = {"credential_id":"mock-checkpoint-81001","role":policy["role"],"scope":policy["actions"],"ttl_seconds":policy["ttl_seconds"]}
Path(sys.argv[2]).write_text(json.dumps(out,indent=2)+"\n")
print(json.dumps({"decision":decision,"failed_checks":[k for k,v in checks.items() if not v]}))
sys.exit(0 if decision == "allow" else 3)

8. Execute all three trust decisions

mkdir -p evidence target/staging
python mock_exchange.py claims-main.json evidence/main.json

set +e
python mock_exchange.py claims-feature.json evidence/feature.json
feature_rc=$?
python mock_exchange.py claims-wrong-aud.json evidence/wrong-aud.json
aud_rc=$?
set -e

test "$feature_rc" -eq 3
test "$aud_rc" -eq 3

Do not proceed to deployment until the intended identity is allowed and both attacker-like cases are denied. Authorization correctness is a checkpoint prerequisite, not a post-deployment observation.

9. Bounded external action

import json, hashlib
from pathlib import Path
r = json.loads(Path("evidence/main.json").read_text())
required = "write:staging-marker"
if r["decision"] != "allow" or required not in r["credential"]["scope"]:
    raise SystemExit("missing bounded authorization")
p = Path("target/staging/marker.txt")
p.write_text("source_sha=" + r["sha"] + "\nrole=" + r["credential"]["role"] + "\n")
print(hashlib.sha256(p.read_bytes()).hexdigest())

Run it once, capture the marker digest, and read the file back. Then attempt an out-of-scope operation in a separate tiny check and prove the synthetic credential scope does not include it. You are proving both positive and negative authorization.

10. Exact GitLab configuration mapping

deploy_staging:
  stage: deploy
  timeout: 10m
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_COMMIT_REF_PROTECTED == "true"'
  id_tokens:
    STS_ID_TOKEN:
      aud: https://sts.example.com
  environment:
    name: staging
    deployment_tier: staging
  script:
    - ./provider-exchange-and-deploy "$STS_ID_TOKEN"

In a real provider integration, replace the mock exchange with the provider's supported OIDC exchange. Preserve provider request/correlation IDs and temporary role/scope/expiry metadata. Never upload the token or returned credential as an artifact.

11. Required evidence packet

  • source identity: CI_PIPELINE_SOURCE, ref and CI_COMMIT_SHA for real CI, or fixture SHA for local mode
  • compiled configuration / exact id_tokens audience request
  • pipeline/job IDs and runner/tool versions when run in GitLab
  • trust-policy SHA-256 and human owner/revision note
  • selected synthetic claim sets for allowed and denied tests
  • three exchange decisions and failed-check names
  • short-lived credential role/scope/TTL metadata only
  • external marker read-back and digest
  • assumptions/limitations note explaining that the local simulator does not prove GitLab signature issuance or a real provider's IAM implementation
sha256sum trust-policy.json claims-*.json evidence/*.json target/staging/marker.txt
python --version
git rev-parse HEAD

12. Verification checklist

  • Main/staging/exact audience is allowed.
  • Feature/unprotected ref is denied.
  • Wrong audience is denied.
  • No raw live token or provider secret exists in logs/artifacts.
  • Credential metadata shows only the staging-writer role, one action and 300-second TTL.
  • External marker contains the expected source SHA and independent SHA-256.
  • Trust-policy hash is captured.
  • Original denial evidence remains after the successful action.

13. Cleanup / rollback

rm -rf target
# Keep evidence/ through review, then remove the disposable lab directory/project deliberately.
# For a real optional provider exercise, revoke/delete only the exact lab role/provider binding after preserving evidence.
# Never use wildcard account/role/Vault cleanup.

14. What this checkpoint proves—and does not prove

It proves that a documented trust invariant can be evaluated independently of repository rules, that intended claims allow one narrow action, and that attacker-like claims fail. It does not prove GitLab actually signed the synthetic JSON, that a real cloud provider implements the same claim mapping, or that a production IAM role is correctly scoped. Those require provider-side verification in an authorized disposable environment.

Knowledge check

Why must the checkpoint include denied claim sets?

If a real provider exchange succeeds but the external marker is absent, what failed?

What should appear in the evidence packet instead of the raw temporary credential?

Why is the trust-policy hash important?

What is the key production operating-model improvement from Chapter 27?

15. What Chapter 27 adds to the secure operating model

You can now treat workload identity as auditable state: exact job/source/configuration requests an audience-specific assertion; an external policy validates stable and contextual claims; a short-lived least-privilege credential is issued; and the resulting side effect is independently verified. “Secretless” no longer means “trust everything”—it means replacing stored long-lived provider secrets with short-lived identity exchange while preserving explicit authorization boundaries.

Next chapter

Kubernetes Agent, Cluster Access, GitOps-Oriented Delivery, Kubernetes Deployments, and Environment Integration

Chapter 28 applies the same identity discipline to Kubernetes: explicit GitLab Agent/context authorization, namespaces, immutable image identity, deployment manifests, environment records, rollout health and GitOps boundaries.

Version and compatibility note

GitLab and GitLab Runner evolve continuously. Treat version-sensitive YAML, runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations as assumptions to verify against the current official GitLab documentation before production use. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for any reproducible lab or incident record.

Official references and version notes

Documentation verification date: 2026-09-12. GitLab documents explicit id_tokens as available on Free, Premium, and Ultimate across GitLab.com, Self-Managed, and Dedicated. CI_JOB_JWT/CI_JOB_JWT_V2 were removed in GitLab 17.0; use explicit ID tokens instead. ID tokens are RS256-signed. Their expiry is the job timeout when one is specified, otherwise five minutes. Current custom claims include project/namespace identity, job identity, ref/ref protection, pipeline source, runner identity, source SHA, CI configuration identity, and—when the job declares an environment—environment name/protection/tier/action. GitLab recommends stable IDs such as project/namespace IDs in trust policy conditions where the target provider supports them. Native secrets:vault integration is Premium/Ultimate, while ID-token federation itself is Free. The checkpoint intentionally simulates the cryptographic verification step. For a real optional exercise, use provider-native federation documentation, preserve only non-secret exchange metadata, and remove the exact disposable trust/role after verification.

Current assumptions used in this chapter: Version-sensitive YAML, Runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations must be verified against the exact GitLab, GitLab Runner, tool, and external-system versions used in production. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for reproducible work.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.