Git, CI/CD & GitOpsXC · CI Platform SecurityStateAccess
Terraform state access from CI — what state access enables
What you'll learn
- Explain what reading and writing Terraform state enables from a CI runner
- Distinguish read access from write access and the consequences of each
- Recognise the role of state locking and how the CI interacts with the lock
- Configure per-run scoped credentials and audit state access
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
Terraform state is the team’s record of what exists in the cloud. Every resource ID, every output value, every attribute — and every secret the configuration wrote to state — is in the state file. A CI credential that can read state can read everything the team has provisioned. A credential that can write state can drift reality by modifying the record the team trusts to be authoritative. The discipline is to treat state credentials like cloud credentials: scoped to the workflow, scoped to the action, audited at every read and every write.
What state access enables
State access is read-and-write access to the team’s record of infrastructure. The categories are not the cloud APIs directly; they are the values the state file already contains:
flowchart TB
S["State credential"]
S --> R1["Resource IDs"]
S --> R2["Attributes"]
S --> R3["Outputs"]
S --> R4["Secrets in state"]
S --> R5["Dependencies"]
R1 --> X["Exfiltration: full inventory"]
R2 --> X
R3 --> X
R4 --> X
R5 --> X
S --> W1["Write state"]
W1 --> D["Drift: state says X, reality is Y"]
A read credential is an inventory credential. The attacker who reads state has every resource ID, every attribute, every output, and every secret. The inventory is the targeting data for the next attack: which databases to read, which buckets to exfiltrate, which secrets to rotate.
A write credential is a drift credential. The attacker who writes state can make the record say something reality does not: a different instance type, a different security group, a deleted resource. The next apply reconciles to the modified record — reality drifts to match the malicious record.
Read access and write access
The two operations are different and need different controls. Read access is what every plan job requires; write access is what every apply job requires:
flowchart LR
subgraph READ["Read scope"]
R1["Get state"]
R2["List resources"]
R3["Read outputs"]
end
subgraph WRITE["Write scope"]
W1["Lock state"]
W2["Update state"]
W3["Unlock state"]
end
READ -->|"Every plan"| J1["CI runner"]
WRITE -->|"Every apply"| J2["CI runner"]
A plan job needs read; an apply job needs read plus
write. The discipline is to issue per-job credentials:
the plan job’s credential can s3:GetObject but cannot
s3:PutObject; the apply job’s credential can do both.
The plan job cannot drift production because it cannot
write; the apply job can drift, but the apply is gated
by the workflow and the protected environment.
State locking and the lock table
State locking prevents two applies from running concurrently. The lock is typically held in a DynamoDB table (AWS), a Cosmos DB container (Azure), or a Cloud SQL table (GCP). The CI acquires the lock, runs the apply, and releases the lock:
flowchart LR
A["Apply job"] -->|"Acquire lock"| L["Lock table"]
L -->|"Locked"| B["State bucket"]
B -->|"Read state"| A
A -->|"Write state"| B
A -->|"Release lock"| L
The lock table is itself a credential. A CI that holds the lock-table credential can lock a state to deny applies (denial of service), unlock a state mid-apply (race condition), or read the lock history to enumerate every apply. The discipline is the same: scope the lock-table credential to the apply job, audit every lock acquisition, and never grant the lock-table credential to a job that does not apply.
Auditing state access
Every state read and write should appear in an audit log. S3 server access logging, CloudTrail data events on the state bucket, and equivalent mechanisms on other clouds produce the trail. The CI engineer periodically audits for: reads from unknown jobs, writes from unknown jobs, and reads at unexpected times.
The audit is the operational discipline that catches state credential exfiltration after the fact. The structural fix is per-job scoping; the audit is the detection layer that closes the loop.
Production discipline
- Per-job state credentials. Plan jobs get read; apply jobs get read-and-write. Scoped to the workflow and the action.
- No long-lived state credentials on the runner. No static S3 keys, no service-account JSON files. OIDC for the bucket; short-lived tokens per job.
- State locking for every apply. The lock is acquired, the apply runs, the lock is released.
- Audit state access quarterly. Every read, write, lock acquisition. Mismatches are findings.
- State contains secrets; treat state as sensitive. Encryption at rest, in transit, scoped access, audit.
Cross-course references
- Part XC-01 (What the CI platform can touch) maps the state surface in the wider blast radius.
- Terraform for Production Sysadmins — Parts IX-XII (State) cover state storage, locking, and the audit invariant.
- Part XLIV-03 (Terraform plans as artifacts) covers the plan-as-artifact pattern that depends on state read access.
- AWS for Production Sysadmins — Part XXXI (IAM) covers the policies that gate bucket access.
Quiz
Knowledge check · 4 questions
Q1. A CI credential can read the Terraform state bucket but cannot write to it. What is the worst the credential enables?
Q2. State locking prevents a malicious apply from drifting production because the lock prevents two applies from running concurrently.
Q3. Explain why state read access is a reconnaissance primitive and state write access is a sabotage primitive.
Q4. Diagnose a state-credential compromise and prescribe the per-job scoping that closes the gap.
Team T's CI uses a single AWS access key for Terraform operations, valid for the entire account with s3:* on the state bucket and dynamodb:* on the lock table. The key was issued 18 months ago and never rotated. A fork PR opens; a malicious step runs `aws s3 cp s3://tfstate-prod/main.tfstate .` and exfiltrates the file. The state contains the production database endpoint and the database master password written via kubernetes_secret. Detection is delayed until manual S3 log review.
Passing score: 75%. Answers are checked in this browser.