Chapter 21Lesson 02~215 minutes

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.

Hands-onManual gateDenied actorFreeze simulationHealth 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.
Safety guard: every mutation below must resolve under .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?

Why is the denied intruder run retained?

Why is the freeze exception written to evidence instead of unsetting CI_DEPLOY_FREEZE?

Which observation proves post-deploy health?

Would a second approval repair a digest mismatch?

Next lesson

Configuration, design choices, and tradeoffs

Compare manual gates with environment approvals, self-approval with true separation of duties, hard freezes with emergency exceptions, and broad deploy roles with narrow authorization.

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.

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.