Protected Environments, Deployment Approvals, Freeze Windows, Manual Gates, and Separation of Duties: Concepts, Architecture, and Mental Model
Separate build success from deployment authorization: model protected environments, approvers, manual gates, freeze policy, deployment execution, and external health as independently observable states.
Learning objectives
- Explain why a successful build or even a successful deployment job is not itself authorization to change production.
- Distinguish protected-environment authorization, deployment approval, a blocking manual job, a deploy freeze, protected variables, and external target health.
- Describe the current tier boundary: deploy freezes and normal manual gates are Free; protected environments and deployment approvals are Premium/Ultimate.
- Inspect deployment candidate identity, authorization state, deployment record, and external health without changing any state.
- Design separation of duties so the artifact producer, deployment approver, deployer, and administrator are not accidentally treated as one trust role.
1. The practical problem: green is not the same as authorized
A pipeline can compile correctly, tests can pass, and an artifact can have a verified digest, yet a production change can still be unauthorized. Chapter 19 separated job success, GitLab deployment history, and external target health. This chapter adds another state boundary: who was allowed to authorize the change, under which policy, and with what audit evidence?
The safe model is deliberately conservative. Build and test jobs create evidence. Authorization policy decides whether a deployment candidate may cross a trust boundary. The deployment job performs the side effect. A health check then proves what happened outside GitLab. None of those states should be inferred from another.
2. Terms before configuration
| Term | Meaning here | Do not confuse it with |
|---|---|---|
| Protected environment | A GitLab environment with restricted deploy access; current docs place this in Premium/Ultimate. | A protected branch or a protected CI/CD variable. |
| Allowed to deploy | Users, groups, or roles permitted to deploy to a protected environment. | Approvers; approval and execution can be separate roles. |
| Deployment approval | A Premium/Ultimate authorization record required before a protected-environment deployment may proceed. | MR approval; it controls deployment, not source merging. |
| Blocking manual job | A job requiring a human action while preventing later stages from proceeding. | Deployment approval; a manual job can exist without protected-environment approval rules. |
| Deploy freeze |
A scheduled policy interval exposed to pipelines through
CI_DEPLOY_FREEZE.
|
A universal external lock; other systems can still change the target unless governed too. |
| Separation of duties | Different people/roles control source, approval, deployment, and override powers. | Simply having two buttons or two jobs owned by the same authority. |
3. State model: write down every boundary before changing it
| Layer | Evidence to capture | Why it matters |
|---|---|---|
| Source/revision |
CI_PIPELINE_SOURCE, ref,
CI_COMMIT_SHA, artifact digest
|
Identifies the exact candidate. |
| Compiled pipeline | Merged YAML, job inclusion, manual/freeze rules | Shows what policy GitLab actually compiled. |
| Authorization | Protected environment, allowed deployers, approver rules, approval history | Shows who could authorize and who did. |
| Job/runner | Pipeline/job IDs, actor, runner/executor, timestamps | Shows who caused execution and where it ran. |
| Deployment record | Deployment ID/status, environment, SHA, user | Shows GitLab's deployment record. |
| External target | Target ID, candidate digest/version, independent health result | Shows what really changed. |
| Governance | Freeze interval, exception ticket/reason, admin override evidence | Explains exceptional policy decisions. |
A useful audit packet ties these rows together. If any link is missing, the claim “authorized production deployment succeeded” is incomplete.
4. Mental model: candidate → authorization → execution → health → audit
First a verified build produces a deployment candidate. GitLab compiles the deployment job and determines whether the target environment is protected and whether a freeze/manual rule applies. An approval policy may block execution until eligible approvers act. A deployer then starts or unblocks the deployment job. The job changes the target. Only an independent health check can establish post-deploy health. Finally, the pipeline, approval, deployment, and health identifiers form the audit record.
flowchart TD
A[Verified artifact + SHA] --> B[Compiled deployment candidate]
B --> C{Freeze/manual policy}
C --> D{Protected environment authorization}
D --> E[Approval record]
E --> F[Authorized deploy action]
F --> G[GitLab deployment record]
G --> H[External target change]
H --> I[Independent health verification]
I --> J[Audit packet + exception history]
The arrows are causal dependencies, not decorations. For example, the approval record must point to the same candidate/deployment that is later health-checked; otherwise a reviewer can accidentally approve one revision while another is deployed.
5. Protected environments: who may execute the production side effect
Current GitLab documentation lists protected environments as Premium/Ultimate. A Maintainer or Owner can protect an environment and configure deploy access. Users not in the Allowed to deploy set cannot update or deploy to that protected environment. GitLab administrators retain broad authority, which means administrator actions belong in the audit model rather than being treated as invisible exceptions.
Protected environments are not the same as protected branches. A branch rule controls source changes; an environment rule controls deployment access. Mature delivery uses both when both source integrity and deployment authority matter.
6. Deployment approvals: authorization record, not rollout proof
Deployment approvals are also Premium/Ultimate. Required approvals block deployments to the protected environment until approval rules are satisfied. Current docs state that the pipeline triggerer cannot approve their own deployment by default; an explicit project option can allow self-approval. Administrators can approve or reject deployments, so admin intervention must be recorded as a governance event.
GitLab exposes approval details in the environment/deployment UI and
through the Deployments API. The deployment can have a
blocked status, and Premium/Ultimate responses can
include approval summaries. After authorization, execution is still
a separate event: approving does not prove the script ran or that
the target became healthy.
7. Manual gates: useful, but model their blocking semantics precisely
Manual jobs exist on all tiers. Their pipeline behavior depends on
allow_failure and where when: manual is
declared. A top-level manual job defaults to optional behavior,
while when: manual inside rules defaults
to blocking behavior. To make intent unambiguous, use
allow_failure: false for a blocking gate.
deploy_production:
stage: deploy
environment:
name: production
script:
- ./ci/deploy-verified.sh "$CANDIDATE_DIGEST"
when: manual
allow_failure: false
manual_confirmation: "Deploy the verified candidate to simulated production?"
manual_confirmation adds an explicit confirmation step,
but it does not create independent authorization. If the same user
can prepare, approve, and run the job, there is no true separation
of duties.
8. Deploy freezes: policy input at pipeline creation
Deploy freeze periods are available on Free, Premium, and Ultimate.
During an active freeze GitLab makes the pre-pipeline variable
CI_DEPLOY_FREEZE=true available. That lets
workflow, includes, and job rules decide
whether a deployment job should be omitted, blocked, or converted to
a controlled exception path.
deploy_production:
stage: deploy
script: ./ci/deploy-verified.sh "$CANDIDATE_DIGEST"
environment:
name: production
rules:
- if: '$CI_DEPLOY_FREEZE'
when: manual
allow_failure: false
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
allow_failure: false
- when: never
A freeze is not external target health and not an air gap. It is a delivery policy signal. Any emergency exception should record who authorized it, why, which exact SHA/digest was affected, and how the target was verified afterward.
9. Inspect first: prove policy state without mutating it
Before playing, approving, retrying, or changing settings, capture evidence. In an authorized GitLab project, start with the pipeline graph and merged configuration; then inspect Operate → Environments for deployment status and approval details. Read freeze periods before editing them. If API access is authorized, prefer GET requests and avoid logging tokens.
# Read-only examples. Supply an authorized token through the client/environment; do not print it.
curl --fail --silent --show-error --header "PRIVATE-TOKEN: $GITLAB_READ_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/freeze_periods"
curl --fail --silent --show-error --header "PRIVATE-TOKEN: $GITLAB_READ_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/deployments?environment=production"
The response should be stored only if its contents are appropriate for your evidence retention policy. Never dump all CI/CD variables to “debug” authorization.
10. Separation of duties: design roles around failure impact
| Role | Minimum responsibility | Should not automatically imply |
|---|---|---|
| Builder | Produce/test candidate and digest | Production deployment authority |
| Approver | Confirm candidate may proceed under policy | Ability to rewrite artifact or deployment script |
| Deployer | Execute an approved deployment | Ability to approve own request by default |
| Verifier | Measure independent target health | Power to alter the result being measured |
| Administrator | Recover/govern exceptional cases | An undocumented bypass path |
Small teams may combine roles operationally, but the course keeps the states separate so you can see exactly what assurance is lost when identities overlap.
11. Common mistakes and safer patterns
| Mistake | Why it fails | Safer pattern |
|---|---|---|
| Treat green tests as authorization | Tests answer correctness questions, not permission questions. | Create an explicit deployment authorization layer. |
| Use one broad Maintainer role for everything | Source, secret, deployment, and override powers collapse together. | Use protected environments or a separate CD project where appropriate. |
| Assume approval means success | Approval occurs before execution/health verification. | Record deployment ID and independent post-deploy health. |
| Expose production secret to pre-gate jobs | The secret crosses the trust boundary before authorization. | Scope secrets to the deployment environment/job and minimize earlier job access. |
| Bypass a freeze with no record | Emergency path becomes unauditable normal behavior. | Require bounded reason, actor, candidate digest, and after-action evidence. |
12. Summary and evidence checklist
- A verified artifact is a deployment candidate, not deployment permission.
- Protected environments and deployment approvals are Premium/Ultimate; manual jobs and freeze periods are available on Free.
- Approval, manual execution, deployment status, and external health are separate states.
- Default deployment self-approval is disabled; enabling it is an explicit reduction in separation of duties.
-
A freeze should be observable in pipeline policy through
CI_DEPLOY_FREEZE, and exceptions need their own evidence. - Capture source SHA/digest, compiled configuration, policy/approval record, job/deployment IDs, actor, and post-deploy health.
Knowledge check
A production deployment job is green. Does that prove it was properly authorized?
No. Job success is execution evidence. Authorization must be proven separately through the applicable protected-environment, approval, manual-gate, or exception record.
What is the key difference between a blocking manual job and a deployment approval?
A manual job is a pipeline execution gate. A deployment approval is an authorization rule attached to a protected-environment deployment and is Premium/Ultimate.
What does CI_DEPLOY_FREEZE tell you?
That the pipeline was created during an active deploy-freeze window. Your pipeline policy must use that signal intentionally; it does not prove the external target cannot be changed by another path.
Why is “triggerer can self-approve” a governance decision rather than a convenience toggle?
Because it collapses requester and approver identities, reducing separation of duties and changing the assurance provided by the approval record.
What additional evidence is required after an approved deployment runs?
The deployment ID/status plus an independent check of the target identity/version and health.
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.