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.
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.
2. Assumptions and preflight
-
GitLab ID-token behavior verified 2026-09-12; explicit
id_tokensis Free/Premium/Ultimate. -
CI_JOB_JWT/CI_JOB_JWT_V2are 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:vaultis 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 andCI_COMMIT_SHAfor real CI, or fixture SHA for local mode -
compiled configuration / exact
id_tokensaudience 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?
A successful exchange proves only the positive path. Negative tests prove that trust conditions actually exclude identities that should not receive credentials.
If a real provider exchange succeeds but the external marker is absent, what failed?
Authentication/authorization may have succeeded, but the external action or target verification failed. Inspect the provider API/deployment layer, not the token request first.
What should appear in the evidence packet instead of the raw temporary credential?
Role/policy identity, allowed scope, issue/expiry metadata, provider request ID/status, and external-action evidence—not the bearer secret.
Why is the trust-policy hash important?
It binds the provider decision to the exact authorization configuration reviewed at the time of the test.
What is the key production operating-model improvement from Chapter 27?
Long-lived deployment secrets can be replaced, where supported, by explicit job identity plus provider-side least-privilege federation with positive and negative authorization evidence.
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.
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.
- OIDC authentication using ID tokens — official reference.
- Connect to cloud services — official reference.
- CI/CD YAML — id_tokens — official reference.
- AWS OIDC tutorial — official reference.
- Azure OIDC tutorial — official reference.
- Google Cloud workload identity federation — official reference.
- Use HashiCorp Vault secrets — official reference.
- Vault authentication tutorial — official reference.
- External secrets in CI/CD — official reference.
- GitLab deprecations and removals — official reference.
- OpenID Connect Core 1.0 — official reference.
- RFC 7519 — JSON Web Token — official reference.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.