Checkpoint Lab — Protected Environments, Deployment Approvals, Freeze Windows, Manual Gates, and Separation of Duties
Stage a simulated production deployment through approval and freeze controls, prove authorization and health separately, exercise a documented emergency exception, and package auditable evidence.
Learning objectives
- Stage a simulated production deployment using exact candidate identity, an independent approval, an allowed deployer, and freeze-aware policy.
- Predict at least two state changes before execution and verify each independently afterward.
- Demonstrate a normal blocked/approved path and a documented emergency exception without broadening privileges.
- Prove authorization and post-deploy health as separate evidence claims.
- Produce a concise evidence packet and clean up only the disposable target.
1. Checkpoint scenario and success criteria
You are promoting a synthetic candidate to a local “production”
directory. The builder is builder-carol, the approver
is approver-alice, and the deployer is
deployer-bob. A freeze may be active. Your result is
accepted only if the exact candidate digest, authorization record,
deploy actor, simulated deployment record, and independent health
check agree.
2. Preflight and current assumptions
| Check | Expected |
|---|---|
| Working directory | Disposable repository you own |
| GitLab tier | Free is sufficient for mandatory path; Premium/Ultimate controls are simulated |
| Tools |
POSIX shell, sha256sum, awk,
grep
|
| Runner | Optional; local execution is a faithful simulation |
| Target |
.lab/ch21-checkpoint/target-production only
|
| Verification date | 2026-09-12 |
If you also run the YAML on GitLab, record the GitLab/Runner
versions, executor, pipeline ID, job IDs,
CI_PIPELINE_SOURCE, ref, and
CI_COMMIT_SHA.
3. Predict before changing state
- Prediction A: an ineligible actor attempting deployment will be denied and the target directory will remain absent.
- Prediction B: after independent approval plus allowed deployer execution, the target digest will equal the candidate digest.
- Prediction C: when the freeze flag is active, the normal path will not proceed unless a separate exception record exists.
Write these predictions into your evidence packet before running the commands. The point is to compare expected and observed state, not merely to obtain a green result.
4. Build the candidate and policy files
set -eu
root=.lab/ch21-checkpoint
mkdir -p "$root"/{candidate,evidence,policy}
printf '%s
' 'version=21.checkpoint' 'payload=synthetic-only' > "$root/candidate/app.txt"
sha256sum "$root/candidate/app.txt" | tee "$root/evidence/candidate.sha256"
cat > "$root/policy/authorization.env" <<'EOF'
ALLOWED_DEPLOYER=deployer-bob
REQUIRED_APPROVER=approver-alice
PIPELINE_TRIGGERER=builder-carol
SELF_APPROVAL_ALLOWED=false
EOF
printf '%s
' 'prediction_A=deny_unlisted_actor' 'prediction_B=digest_matches_after_authorized_deploy' 'prediction_C=freeze_requires_exception' > "$root/evidence/predictions.env"
5. Implement the synthetic authorization contract
cat > .lab/ch21-checkpoint/authorize.sh <<'EOF'
#!/usr/bin/env sh
set -eu
root=.lab/ch21-checkpoint
. "$root/policy/authorization.env"
actor="${1:?actor}"
action="${2:?action}"
case "$action" in
approve)
[ "$actor" = "$REQUIRED_APPROVER" ] || { echo "DENY_APPROVER $actor"; exit 41; }
[ "$SELF_APPROVAL_ALLOWED" = true ] || [ "$actor" != "$PIPELINE_TRIGGERER" ] || exit 42
printf 'approval=APPROVED
approved_by=%s
' "$actor" > "$root/evidence/approval.env" ;;
deploy)
[ "$actor" = "$ALLOWED_DEPLOYER" ] || { echo "DENY_DEPLOYER $actor"; exit 43; }
test -f "$root/evidence/approval.env" || { echo "DENY_NO_APPROVAL"; exit 44; } ;;
*) exit 64 ;;
esac
EOF
chmod +x .lab/ch21-checkpoint/authorize.sh
6. Prove the denied actor path first
root=.lab/ch21-checkpoint
rm -rf "$root/target-production"
set +e
"$root/authorize.sh" intruder-mallory deploy > "$root/evidence/denial.log" 2>&1
rc=$?
set -e
test "$rc" -eq 43
test ! -e "$root/target-production"
printf 'denial_exit=%s
target_absent=true
' "$rc" >> "$root/evidence/denial.log"
cat "$root/evidence/denial.log"
This independently verifies Prediction A. Do not discard the denial log after the happy path succeeds.
7. Exercise freeze policy before approval
root=.lab/ch21-checkpoint
export CI_DEPLOY_FREEZE=true
if [ "${CI_DEPLOY_FREEZE:-}" = true ]; then
printf '%s
' 'freeze=true' 'normal_path=BLOCKED' > "$root/evidence/freeze.env"
fi
cat "$root/evidence/freeze.env"
For real GitLab use, configure a deploy freeze so the pre-pipeline variable is supplied by GitLab, then implement a rule that blocks/changes the deployment path. Here the variable is simulated only to exercise decision logic.
8. Independent approval and emergency exception record
root=.lab/ch21-checkpoint
"$root/authorize.sh" builder-carol approve || test $? -eq 41
"$root/authorize.sh" approver-alice approve
cat >> "$root/evidence/freeze.env" <<'EOF'
exception_id=LAB-CH21-EMERGENCY-001
exception_authorized_by=approver-alice
exception_reason=checkpoint controlled recovery
EOF
cat "$root/evidence/approval.env"
cat "$root/evidence/freeze.env"
The exception does not erase the freeze. It adds a bounded, attributable decision alongside it. This is the governance pattern the lab is testing.
9. Deploy only the approved, verified candidate
root=.lab/ch21-checkpoint
"$root/authorize.sh" deployer-bob deploy
candidate_digest=$(awk '{print $1}' "$root/evidence/candidate.sha256")
current_digest=$(sha256sum "$root/candidate/app.txt" | awk '{print $1}')
test "$candidate_digest" = "$current_digest"
mkdir -p "$root/target-production"
cp "$root/candidate/app.txt" "$root/target-production/app.txt"
printf 'deployment_id=SIM-21-001
deployed_by=deployer-bob
candidate_digest=%s
' "$candidate_digest" > "$root/evidence/deployment.env"
cat "$root/evidence/deployment.env"
10. Prove external state independently
root=.lab/ch21-checkpoint
expected=$(awk -F= '/candidate_digest/{print $2}' "$root/evidence/deployment.env")
actual=$(sha256sum "$root/target-production/app.txt" | awk '{print $1}')
test "$expected" = "$actual"
grep -q '^version=21.checkpoint$' "$root/target-production/app.txt"
printf 'health=PASS
target_digest=%s
verified_by=checkpoint-verifier
' "$actual" > "$root/evidence/health.env"
cat "$root/evidence/health.env"
This independently verifies Prediction B. The target evidence is separate from the approval file and deployment receipt.
11. Equivalent GitLab Free control skeleton
stages: [verify, authorize, deploy, verify_target]
verify_candidate:
stage: verify
script: sha256sum dist/app.tar.gz
artifacts:
paths: [dist/app.tar.gz, dist/app.tar.gz.sha256]
production_authorization:
stage: authorize
needs: [verify_candidate]
rules:
- if: '$CI_DEPLOY_FREEZE'
when: manual
allow_failure: false
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
allow_failure: false
- when: never
manual_confirmation: "Authorize this exact candidate for simulated production?"
deploy_production:
stage: deploy
needs: [verify_candidate, production_authorization]
resource_group: production
environment:
name: production
script: ./ci/deploy-verified.sh
verify_production:
stage: verify_target
needs: [deploy_production]
script: ./ci/check-production-health.sh
On Premium/Ultimate, add protected-environment Allowed to deploy and deployment-approval rules rather than pretending this manual job is equivalent to those features.
12. Required evidence packet
| Evidence | Required content |
|---|---|
| Source/pipeline | Pipeline source/ref/SHA or explicit local-simulation note |
| Candidate | SHA-256 digest and candidate path |
| Authorization | Allowed deployer/approver policy, denial log, approval record |
| Freeze | Freeze state plus exception ID/actor/reason if exercised |
| Execution | Job/deployment IDs or simulated deployment ID and deployer |
| Runner/tooling | Runner/executor/version when used; local tool assumptions otherwise |
| Target | Independent target digest/content and health result |
| Limitations | Paid features simulated; no production credentials/resources used |
If you use real Premium/Ultimate controls, add protected-environment configuration and deployment approval summary from the UI/API.
13. Cleanup/rollback
root=.lab/ch21-checkpoint
case "$root" in .lab/ch21-checkpoint) : ;; *) exit 90 ;; esac
rm -rf "$root/target-production"
test ! -e "$root/target-production"
# Review/copy the evidence packet before removing the rest.
# rm -rf "$root"
For a real deployment, rollback must restore a known candidate/artifact and be authorized under the same or an explicitly documented emergency policy. Never “rollback” by rebuilding source and assuming identical bytes.
14. What this chapter adds to a production operating model
You now have four separate claims for a production change: candidate identity, authorization, deployment execution, and external health. Freeze and exception policy adds governance context around those claims. This separation is what makes approval evidence meaningful rather than ceremonial.
Chapter 22 moves from authorization to Tags, Releases, Release CLI, Changelogs, Evidence, Asset Links, and Release-Orchestration Pipelines: version/tag identity, release objects, changelog/evidence, and asset links must preserve the same candidate provenance you just learned to authorize.
Knowledge check
What does the denied-actor test prove?
That the authorization boundary is enforced, not merely documented. The target remaining absent is independent evidence of no side effect.
Why does the checkpoint keep the freeze record when exercising an emergency exception?
Because the exception supplements the policy state; deleting or unsetting the freeze would erase why exceptional authority was needed.
Which two files prove authorization and target health separately?
The approval/authorization evidence proves permission; the health evidence proves the resulting external target state. Neither substitutes for the other.
Why verify the digest again immediately before copying to the target?
To detect candidate drift between build/approval and deployment so the deployed bytes remain tied to the approved candidate.
What should change when moving this checkpoint to Premium/Ultimate?
Use actual protected-environment Allowed to deploy and deployment-approval rules, retain the Free freeze/manual logic where useful, and capture real GitLab approval/deployment IDs—without removing independent target verification.
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. The mandatory learning path uses only disposable/local simulation plus GitLab Free features. Protected environments and deployment approvals are treated as Premium/Ultimate and are simulated unless the learner already has an authorized eligible project.
- Protected environments — official reference.
- Deployment approvals — official reference.
- Deployment safety — official reference.
- Job control — official reference.
- CI/CD YAML reference — official reference.
- Predefined variables — official reference.
- Freeze Periods API — official reference.
- Deployments API — 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.