Protected Environments, Deployment Approvals, Freeze Windows, Manual Gates, and Separation of Duties: Guided Hands-On Workflow and Core Operations
Build a disposable authorization simulation with a verified artifact, allowed deployer and independent approver, a denied actor, freeze logic, blocking manual gate, deployment record, and independent target-health proof.
Learning objectives
- Create a disposable deployment candidate and record its SHA-256 digest before any authorization step.
- Model allowed deployers and an independent approver with synthetic identities; deny one unauthorized actor.
- Use a Free-compatible blocking manual deployment job and freeze-aware rules while faithfully simulating protected-environment approval policy.
- Prove that approval, deployment execution, and external health are three different observations.
- Clean up only the bounded local target and retain a concise evidence packet.
1. Lab boundary and assumptions
This lab is intentionally runnable without production access. The
“target” is a directory under
.lab/ch21/target-production. Synthetic actors are names
in policy files; they are not GitLab accounts. The Premium/Ultimate
protected-environment/approval behavior is simulated, while the YAML
illustrates real Free-tier manual/freeze controls.
| Assumption | Value |
|---|---|
| GitLab behavior verified | 2026-09-12 |
| Runner/executor | Any ordinary disposable runner or local shell simulation; no privileged runner required. |
| Credentials | None for the mandatory local path. Optional API inspection uses an authorized read token without printing it. |
| Target | Repository-local disposable directory only. |
| Paid features | Simulated; never required to finish the lab. |
.lab/ch21/. If your path check fails, stop
instead of “fixing” it with broader deletion.
2. Create synthetic source, candidate, and policy state
set -eu
mkdir -p .lab/ch21/{candidate,target-production,evidence,policy}
printf '%s
' 'version=21.1' 'message=synthetic production candidate' > .lab/ch21/candidate/app.txt
sha256sum .lab/ch21/candidate/app.txt | tee .lab/ch21/evidence/candidate.sha256
cat > .lab/ch21/policy/authorization.env <<'EOF'
ALLOWED_DEPLOYER=deployer-bob
REQUIRED_APPROVER=approver-alice
PIPELINE_TRIGGERER=builder-carol
SELF_APPROVAL_ALLOWED=false
EOF
cat .lab/ch21/policy/authorization.env
The digest becomes the immutable-ish candidate identity for the lab. The synthetic policy keeps requester, approver, and deployer distinct.
3. Add a bounded authorization simulator
cat > .lab/ch21/authorize.sh <<'EOF'
#!/usr/bin/env sh
set -eu
. .lab/ch21/policy/authorization.env
actor="${1:?actor required}"
action="${2:?action required}"
case "$action" in
approve)
[ "$actor" = "$REQUIRED_APPROVER" ] || { echo "DENY approver=$actor"; exit 42; }
[ "$SELF_APPROVAL_ALLOWED" = true ] || [ "$actor" != "$PIPELINE_TRIGGERER" ] || { echo "DENY self-approval"; exit 43; }
printf 'approved_by=%s
' "$actor" > .lab/ch21/evidence/approval.env
;;
deploy)
[ "$actor" = "$ALLOWED_DEPLOYER" ] || { echo "DENY deployer=$actor"; exit 44; }
test -f .lab/ch21/evidence/approval.env || { echo "DENY missing approval"; exit 45; }
;;
*) exit 64 ;;
esac
EOF
chmod +x .lab/ch21/authorize.sh
./.lab/ch21/authorize.sh intruder-mallory deploy || test $? -eq 44
The denied request is expected evidence. Do not remove it from the exercise: authorization denials are often more informative than successful paths.
4. Free-tier pipeline gate: freeze-aware and explicitly blocking
stages: [verify, deploy, verify_target]
verify_candidate:
stage: verify
script:
- sha256sum .lab/ch21/candidate/app.txt
artifacts:
paths: [.lab/ch21/candidate/app.txt, .lab/ch21/evidence/candidate.sha256]
production_gate:
stage: deploy
needs: [verify_candidate]
environment:
name: production
script:
- echo "authorization gate acknowledged for $CI_COMMIT_SHA"
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 the simulated production attempt for this exact pipeline?"
deploy_simulated:
stage: deploy
needs: [verify_candidate, production_gate]
resource_group: simulated-production
script:
- ./ci/deploy-simulated.sh
environment:
name: production
The manual gate is not a replacement for Premium deployment approvals. It provides a runnable Free-tier gate and demonstrates how freeze policy can alter job behavior.
5. Deploy exact bytes and write a deployment receipt
mkdir -p ci
cat > ci/deploy-simulated.sh <<'EOF'
#!/usr/bin/env sh
set -eu
root=.lab/ch21
expected=$(awk '{print $1}' "$root/evidence/candidate.sha256")
actual=$(sha256sum "$root/candidate/app.txt" | awk '{print $1}')
[ "$expected" = "$actual" ] || { echo "candidate digest mismatch"; exit 50; }
./.lab/ch21/authorize.sh deployer-bob deploy
mkdir -p "$root/target-production"
cp "$root/candidate/app.txt" "$root/target-production/app.txt"
printf 'candidate_digest=%s
deployed_by=%s
' "$actual" 'deployer-bob' > "$root/evidence/deployment.env"
EOF
chmod +x ci/deploy-simulated.sh
ci/deploy-simulated.sh
cat .lab/ch21/evidence/deployment.env
The script refuses to deploy if the candidate changed after hashing. That is the “build once, authorize, then deploy the verified bytes” principle from earlier artifact chapters.
6. Simulate independent approval before deployment
rm -f .lab/ch21/evidence/approval.env
./.lab/ch21/authorize.sh builder-carol approve || test $? -eq 42
./.lab/ch21/authorize.sh approver-alice approve
cat .lab/ch21/evidence/approval.env
./.lab/ch21/authorize.sh deployer-bob deploy
The triggerer cannot approve because only
approver-alice is eligible. This mirrors the default
self-approval boundary without requiring a paid GitLab project.
7. Simulate a freeze and a documented exception
export CI_DEPLOY_FREEZE=true
if [ "${CI_DEPLOY_FREEZE:-}" = true ]; then
printf '%s
' 'freeze_active=true' 'decision=BLOCK' | tee .lab/ch21/evidence/freeze.env
fi
# Emergency path is evidence, not a silent unset:
printf '%s
' 'exception_id=LAB-EX-21' 'authorized_by=approver-alice' 'reason=synthetic recovery exercise' >> .lab/ch21/evidence/freeze.env
cat .lab/ch21/evidence/freeze.env
In a real project, use GitLab freeze-period configuration and
CI_DEPLOY_FREEZE. Do not teach operators to simply
unset the variable. An exception should be a separate, reviewable
policy decision.
8. Approval is not rollout success: verify target health independently
expected=$(awk -F= '/candidate_digest/{print $2}' .lab/ch21/evidence/deployment.env)
actual=$(sha256sum .lab/ch21/target-production/app.txt | awk '{print $1}')
printf 'target_digest=%s
' "$actual" | tee .lab/ch21/evidence/health.env
test "$expected" = "$actual"
grep -q '^version=21.1$' .lab/ch21/target-production/app.txt
printf '%s
' 'health=PASS' >> .lab/ch21/evidence/health.env
cat .lab/ch21/evidence/health.env
The health check reads the target after deployment. It does not rely on the deployment script saying “success.”
9. Optional GitLab inspection: read IDs and blocked state without mutating policy
On an authorized project, record pipeline/job IDs first. For
Premium/Ultimate, a protected deployment can appear with
status=blocked; approval details can be inspected in
the environment UI or deployment API. Keep tokens out of traces.
# Optional read-only example
curl --fail --silent --show-error --header "PRIVATE-TOKEN: $GITLAB_READ_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/deployments?environment=production" > .lab/ch21/evidence/deployments.json
10. Before/after evidence packet
| Evidence | Before | After |
|---|---|---|
| Candidate | SHA-256 of synthetic file | Same digest present in target |
| Authorization | No approval file; denied intruder | Independent approval + allowed deployer |
| Freeze | Policy can be active | Exception has ID/actor/reason when exercised |
| Deployment | No target file | Deployment receipt + target identity |
| Health | Not applicable | Independent digest/content check PASS |
If a real GitLab pipeline is used, add CI_PIPELINE_ID,
deployment job ID, deployment ID, CI_COMMIT_SHA, actor,
runner version/executor, and merged configuration evidence.
11. Challenge: choose the layer before changing anything
A deployment is correctly approved by Alice, but Bob’s deploy job writes a candidate whose digest differs from the approved candidate. Which layer is broken?
Answer after reasoning: authorization can still be correct; the failure is artifact/dataflow integrity. Fix the candidate handoff/digest verification. Adding another approver does not solve the wrong-byte problem.
12. Cleanup and rollback
case "$PWD" in
*/your-disposable-project|*/gitlab-ci-cd-lab) : ;;
*) echo "Run cleanup only inside your disposable lab repository"; exit 70 ;;
esac
rm -rf .lab/ch21/target-production
# Keep evidence for review, then remove the whole lab only when finished:
test ! -e .lab/ch21/target-production
# rm -rf .lab/ch21
The guard is intentionally narrow. Adapt the accepted repository path to your disposable lab rather than weakening the check.
Knowledge check
Why does the lab hash the candidate before approval?
So the approval can be tied to exact bytes. Otherwise the meaning of “approved candidate” can drift before deployment.
Why is the denied intruder run retained?
It proves the authorization boundary actually rejects an ineligible actor instead of only documenting the happy path.
Why is the freeze exception written to evidence instead of
unsetting CI_DEPLOY_FREEZE?
An exception is a governance event. Silently removing the signal destroys the audit trail and normalizes bypass.
Which observation proves post-deploy health?
The independent read of the target digest/content, not the approval record and not the deploy job exit code alone.
Would a second approval repair a digest mismatch?
No. That is a dataflow/artifact-integrity failure, so the fix belongs in the artifact transfer/verification layer.
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.