Skip to content
SRE & DevOpsDeep Dive Published Updated 8 min readViews unavailable

GitLab Protected Environments: Deployment Access and Approval Boundaries

Protect production deployments in GitLab with explicit environment declarations, authorized deployers, approval rules, serialized jobs, and audit-ready controls.

GitLab protected environments add an authorization boundary around deployment targets such as production. They control who can deploy and, where the project’s GitLab tier and configuration support it, can require deployment approvals. They do not secure an environment merely because a job is named production, and they do not serialize two concurrent deployment jobs by themselves. The CI job must declare the environment correctly, access lists and approval rules must be maintained, and concurrency controls must be configured separately.

Treat the pipeline definition and environment settings as one security system. A user who can alter .gitlab-ci.yml, modify protected-environment rules, access deployment credentials, or run arbitrary jobs on a privileged runner may be able to bypass the intent of an approval gate. Review who can change each component, whether configuration is inherited from a parent group, and whether the environment name or deployment tier used by the job matches the protected object.

Understand the three decisions

Allowed-to-deploy rules determine who may run a deployment job for the protected environment. Approval rules determine who must approve a deployment before it can proceed when deployment approvals are configured. CI job rules determine whether the job is created and whether it is manual or automatic. These are distinct controls: a manual job is not a protected environment, and an approval rule does not automatically prevent a pipeline from executing unrelated privileged jobs.

Environment names are part of the match. A job that omits the environment keyword or uses a different environment name may not be associated with the protected environment, and deployment-only access will not apply as expected. Dynamic names such as production/$CI_COMMIT_REF_SLUG can also create distinct environments instead of targeting the exact protected production environment. Use stable names for critical targets and test the effective environment in a non-production project before adopting a pattern.

Group-level protection can use deployment tiers to apply a rule across projects with different environment names. Project and group policies can combine; a job may need to satisfy both. Know which rule is inherited, who can edit it, and whether a child project can weaken any setting. Protected environments are unavailable if CI/CD is disabled for the project.

Declare the deployment target explicitly

The pipeline job should identify the environment and its deployment tier. The resource_group key solves a different problem: it prevents concurrent jobs in the same resource group from modifying the same target at once. It does not limit which users can deploy, require approval, or verify the artifact. Use both access controls and serialization where needed.

stages:
  - test
  - deploy

deploy-production:
  stage: deploy
  script:
    - ./scripts/deploy.sh "$RELEASE_DIGEST"
  environment:
    name: production
    deployment_tier: production
  resource_group: production
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      when: manual

This example assumes the project defines RELEASE_DIGEST through a trusted build-and-promotion path and that deploy.sh validates it. A variable name alone does not prove a digest is immutable or approved. Configure the protected environment in GitLab settings, restrict the production deployment group, and ensure the deploy job does not receive production credentials in earlier untrusted jobs. The script should fail closed if the release identifier is missing or malformed.

The manual job is a human-triggered action; depending on the configuration, an eligible user may still be able to run it without a separate approval workflow. If deployment approval is required, configure an approval rule on the protected environment and verify the UI status for a real pipeline. Do not infer that a button labeled “play” represents the required independent approval controls.

Configure access and approval rules least-privilege

Use dedicated operator groups for production deployments and approval groups for independent review. Grant access at the narrowest scope available. A role-based setting such as allowing all developers may be appropriate in a lower environment but usually defeats a production separation-of-duties design. GitLab documents that the exact features and access options depend on offering and tier; confirm the current subscription before relying on approval rules.

Keep deployers and approvers separate when policy requires independent control. Verify that the approver group is actually shared with the project and that group inheritance is configured as intended. Membership changes should be reviewed in the identity-management process. Remove a user from both direct and inherited access paths when revoking deployment authority.

Approvals authorize a deployment decision, but they do not prove that the approved artifact is the one eventually deployed. Bind approval evidence to a pipeline, commit, environment, and immutable artifact digest. If a job can be retried with changed variables or a different artifact after approval, the workflow may no longer preserve the same decision context. Make release inputs immutable and visible in the job log without exposing secrets.

Serialize deployment jobs and handle stale pipelines

GitLab pipelines can run concurrently. If two pipelines deploy to the same environment, a newer version can be overwritten by an older delayed job, or both changes can interleave. A resource_group ensures only one job in that group runs at a time. It does not inherently guarantee that the newest pipeline runs last or that an old job cannot deploy after a newer one; choose a process mode, cancellation policy, and stale-pipeline rule appropriate to your GitLab version and release process.

Use a dedicated resource-group name for the real production target, not a variable whose value changes by branch unless branches deploy to genuinely separate environments. Prevent outdated pipelines from deploying when a newer commit supersedes them. Before enabling automatic deployments, test cancellation during an active deploy, a retry after partial failure, and two pipelines queued for the same target. The deploy script should be idempotent or have a documented compensating action.

Database migrations and infrastructure changes may not be safely serialized by the application job alone. Model their lock and ordering explicitly. A resource group serializes jobs that use the same key, but it does not coordinate operators working outside GitLab, a second project using another key, or a cloud console action. If multiple pipelines or projects can modify the same system, establish a shared deployment lock or an ownership contract.

Protect the pipeline definition and credentials

Production credentials should be scoped to protected branches and environments and exposed only to the approved deployment job. Avoid making secrets available to test jobs that run fork code. Use OIDC federation or another short-lived credential mechanism when the target cloud supports it, and constrain the trust policy to the expected project, branch, environment, and audience. A protected environment cannot compensate for a cloud role that trusts every job in every branch.

Protect the .gitlab-ci.yml and included pipeline configuration with branch protections, code ownership, and review. A developer who can alter the pipeline may be able to add a job that exfiltrates an environment variable or calls a deployment API by another route. Include shared includes, component versions, runner tags, and deployment scripts in the review boundary. Pin third-party components and images according to the organization’s policy.

Runners are part of the deployment security model. Keep production runners isolated from untrusted build work, use protected runner configuration when supported, and restrict network paths. A successful protected-environment check does not stop arbitrary code running earlier in a job if it already has access to the same credentials or cluster. Make credentials available only after approval and immediately before the deployment action.

Audit and troubleshoot effective protection

For each release, record the environment name, deployment tier, pipeline ID, commit, artifact digest, deployer, approvers, job result, and target version. Use GitLab audit events and deployment records for historical evidence, and verify that the organization retains them for the required period. If an approval rule is changed or deleted, understand how prior approval details are represented and whether the audit event provides the necessary history.

When an authorized deployer receives a permission error, inspect the protected-environment Allowed to deploy list, inherited group policy, user role, exact environment name, job’s environment declaration, and whether CI/CD is enabled. A job with no environment keyword falls back to ordinary CI job permissions and will not receive deployment-only access. When an unauthorized user can trigger a deploy, verify that the job really targets the protected environment and that a duplicate path or separate project is not bypassing the control.

When a deployment executes without the expected approval, check the environment’s approval rules and project tier, whether the job was created against that environment, whether the configuration changed after the rule was set, and whether another deployment path exists. Do not patch the issue by making all jobs manual or allowing fewer users without first identifying the bypass. Reproduce the exact pipeline with a test user and a non-production environment.

Protected environments are effective when identity, target naming, pipeline code, approval evidence, artifact identity, runner isolation, and serialization agree. Periodically test both allowed and denied users, and treat any change to the environment policy or deployment job as a production control change rather than a YAML cleanup.

Related:

Sources:

Comments