Add protected field to Terraform states to restrict writes to protected branches

Everyone can contribute. Help move this issue forward while earning points, leveling up and collecting rewards.

  • Close this issue

Problem Statement / Context

Currently, GitLab-managed Terraform states can be accessed by any CI job using the built-in job token. While write access is restricted to maintainer-level jobs, there are no restrictions on the branch the job is running on.

This means a maintainer can run terraform apply from any feature branch — including branches that were never peer-reviewed or intended to be merged — and make potentially destructive changes to production infrastructure (e.g., deleting an S3 bucket, modifying security groups, changing DNS records).

Additionally, anyone with a Personal Access Token (PAT) can run terraform apply from their local machine, completely bypassing the CI/CD pipeline and any review process.

This is analogous to the problem that protected CI/CD variables solve: sensitive values should not be exposed to jobs running on unreviewed branches. Similarly, protected Terraform states should not be writable from unreviewed branches.

Example scenario:

  1. A developer creates a feature branch with a change to a Terraform configuration
  2. Without opening an MR or getting any review, they push and trigger a CI pipeline
  3. The pipeline runs terraform apply and modifies production infrastructure
  4. The change was never reviewed, approved, or merged into the default branch

Current protection mechanisms and their gaps:

The Terraform state documentation describes the current access model: Developer+ can read states, Maintainer+ can write/lock/delete. While GitLab offers fine-grained job token permissions for cross-project access control and custom roles (Ultimate) to exclude admin_terraform_state, none of these mechanisms restrict writes based on which branch a job runs on. Administrators can disable Terraform state management entirely, but there is no per-state protection option.

Proposal

Introduce a protected boolean field on Terraform state files. When a state is marked as protected, it can only be modified (written, locked, deleted) from CI/CD pipelines running on protected branches. Reads (terraform plan) remain unrestricted.

This follows the same conceptual pattern as protected CI/CD variables, which are only exposed to jobs on protected branches.

This is a deliberate Phase 1 approach: simple, branch-based protection using a boolean flag. More granular role-based access control (per-action minimum access levels, pattern matching) can be layered on top in future iterations if needed, following the packages protection rules pattern.

🛠️ with ❤️ at Siemens

User Flow

Setting up protection:

  • A maintainer navigates to Operate > Terraform states in their project
  • Each state shows a "Protected" badge if it is currently protected
  • From the actions dropdown, a maintainer can select Protect or Unprotect
  • Protection can also be toggled via the GraphQL API

Happy path — CI job on a protected branch:

  • A CI job runs terraform apply on the main branch (which is protected)
  • The job authenticates via its CI job token as usual
  • GitLab checks: is the state protected? Yes. Is the job on a protected branch? Yes.
  • The operation proceeds normally — no change in behavior

Blocked — CI job on a non-protected branch:

  • A CI job runs terraform apply on a feature branch (not protected)
  • GitLab checks: is the state protected? Yes. Is the job on a protected branch? No.
  • The operation is rejected with HTTP 403 Forbidden
  • Error message: "This Terraform state is protected. It can only be modified from CI/CD pipelines running on protected branches."
  • terraform plan (read-only) still works on any branch

Blocked — local execution via PAT:

  • A user runs terraform apply from their laptop using a Personal Access Token
  • GitLab checks: is the state protected? Yes. Is this a CI job? No (it's a PAT).
  • The operation is rejected with HTTP 403 Forbidden
  • Same error message as above
  • terraform plan (read-only) still works via PAT

Approach: Why a Boolean Field?

We considered several patterns used across GitLab for protecting resources:

Pattern Used By Complexity Dimension
Boolean flag + branch check Protected CI variables Low Where (branch)
Separate rules table with minimum access levels Protected packages/containers Medium Who (role)
Separate model with access levels + approval rules Protected environments High Who + approval

We propose the Boolean flag pattern because:

  1. The core problem is about where, not who. The existing role-based permissions (Developer = read, Maintainer = write) are sufficient. The missing piece is restricting which branches can trigger writes.
  2. Terraform states are few per project (often just one default state), unlike packages where pattern matching across hundreds of names is valuable.
  3. Simplicity. A boolean flag is easy to understand, configure, and explain. It matches the mental model: "this state is protected" or "it is not."
  4. Extensibility. If users later request granular per-action access levels, a protection rules table can be added on top without breaking the boolean. The boolean becomes "is this state protected at all?" and the rules define "how."

Default Behavior and Setup Rules

  • New states created on a protected branch are automatically protected
  • New states created on a non-protected branch are not protected (can be manually protected later)
  • Existing states are migrated to protected via a background migration (matching the epic requirement)
  • If a branch becomes unprotected, states that were created on it remain protected (must be manually unprotected)
  • Protection status is visible in the Terraform states list (badge) and queryable via GraphQL

What Is NOT Restricted

  • Read access (terraform plan, GET /state/:name) — remains unrestricted regardless of protection status
  • Role-based permissions — unchanged. Developer+ can still read, Maintainer+ can still write (on allowed branches)
  • State versions API — read access to historical versions is not affected

Related Resources

  • Parent Epic: gitlab-org&15118 — "Protected" Terraform States
  • Parent Epic (Terraform state management): gitlab-org&2673 — GitLab managed terraform state
  • Pattern reference: Protected CI/CD variables documentation
  • Pattern reference: Package protection rules documentation
  • Community interest: 26 upvotes on the epic as of February 2026

Implementation Plan

The implementation follows an iterative MR sequence. Detailed implementation plans and codebase research are maintained in the development workspace.

  • Database migration to add protected boolean column to terraform_states
  • Protection check service that inspects the CI job token context (Ci::Build#protected?)
  • REST API enforcement on all mutating endpoints (write, lock, unlock, delete)
  • GraphQL field exposure and toggle mutation
  • Frontend badge and protect/unprotect action
  • Data migration for existing states
  • Auto-protect states created on protected branches
  • Usage instrumentation
  • Documentation
Edited Mar 02, 2026 by 🤖 GitLab Bot 🤖
Assignee Loading
Time tracking Loading