Review the plan, not only the diff, and review it as four separate questions: who can obtain each identity, what each identity can do, what becomes reachable from the network, and where secrets end up. Encode the recurring rules as policy, for example Rego run with conftest against terraform show -json, and fail the pipeline on violations. Apply exactly the approved plan, then test the deployed access, and keep secrets out of state with ephemeral values and write-only arguments.
- Trust, permission, network exposure and artifact handling are independent findings; fixing one says nothing about the others.
- Policy as code turns the review into a pipeline gate: a 45-line Rego policy caught all three access defects in R17 and passed R18.
sensitive = trueonly hides a value in the terminal; in a test it still appeared in clear text inplan.jsonand the saved plan file.- Terraform 1.11 write-only arguments, fed by ephemeral variables, keep a secret out of plan and state entirely.
- An approved plan is evidence only for itself: a later plan from a different lock file needs its own review.
| Setting | Value | Source |
|---|---|---|
| Policy input | terraform show -json plan.bin for the exact plan that will be applied | Terraform: JSON output format |
| Pipeline gate | conftest test --policy policy plan.json; any failure stops the apply | conftest documentation |
| Secrets passed to resources | Ephemeral variables into write-only arguments (*_wo with *_wo_version) | HashiCorp: managing sensitive Terraform data |
| Plan and state files | Treated as secret-bearing: encrypted storage, restricted readers, bounded retention | HashiCorp: managing sensitive Terraform data |
- Run
terraform show -json plan.bin > plan.jsonandconftest test --policy policy plan.jsonin CI and confirm a failing rule stops the apply. - Search the saved plan and
plan.jsonfor a canary secret and expect no match where write-only arguments are used. - Apply only the plan file that was reviewed (
terraform apply plan.bin), and record its hash with the approval. - After apply, test the role from production and staging, one allowed write, a forbidden read and a neighboring prefix.
- Check that the database port is not reachable from outside the VPC.
- Applies to
- Terraform 1.16.5 tested; ephemeral values need 1.10 and write-only arguments 1.11 or later
- conftest 0.70.0 (OPA Rego v1 syntax)
- AWS provider 6.68.0
Infrastructure change review. A clean source diff cannot establish deployed state or all effective permissions.
- Part of
- Workflow: Fix a finding
Learning goals and prerequisites
What you will learn
- Translate a feature requirement into a precise access tuple
- Separate a plan approval from deployed-access evidence
- Identify secrets that remain exposed in state artifacts
Before you start
Engineers who write or approve Terraform changes, and the platform teams who run the pipeline that applies them
- Read an infrastructure diff and identify identities, resources, and network exposure.
Refresh first: Review cloud permissions and role trust together
Go to the guided practiceUse this checklist for cloud infrastructure changes; the state-handling notes specifically concern Terraform. Provider features and resource behavior vary, so inspect the plan using the exact tool and provider versions your deployment supports. Review both the configuration and the proposed effective resources.
Start with the intended capability. Which service needs access to which resource, for what operation, from which identity and network path? A syntactically valid configuration can still grant more authority or exposure than the feature requires.
The feature is deliberately small: a production report worker writes a finished CSV to one tenant prefix. It does not browse the bucket or read other tenants’ exports. Represent this requirement as principal, operation, resource and conditions. A role named report-writer is only a label; the role’s effective actions and the identities that can acquire it determine its authority. Start with the requirement before reading the proposed implementation so the implementation does not quietly redefine the need.
In the supplied plan, s3:* on exports/* grants many operations across every prefix. Replacing it with PutObject on exports/north/* removes the documented read and neighboring-tenant paths. That is only the permissions half of the review. If a staging runner may assume the production role, staging still gains the narrower production capability. Review and repair the role acquisition condition separately. An environment string in a variable is not an enforcement condition unless it reaches the identity provider and trust policy correctly.
The network change is independent again. Opening a database security group to 0.0.0.0/0 is not required for writing a CSV to object storage. Ask for the dependency that needs the ingress; if none exists, remove it. A successful upload test cannot excuse unrelated database exposure. Finally, a sensitive Terraform variable may be redacted in terminal output while its value remains in plan/state. Treat those artifacts as a distinct secret-bearing data flow with an owner, retention and access controls.
Close the review in two stages. Before deployment, accept only the intended plan revision and lockfile, with the excess changes removed. After deployment, establish the actual principal and test one allowed write plus forbidden read, neighboring prefix and staging assumption. A plan can be correct while a separate resource policy or manual change still grants access; only the combined evidence supports the operational conclusion.
The Terraform below is the R17 proposal. It shows three of the four defects in code: a trust condition that accepts every environment through StringLike, s3:* on every export object, and PostgreSQL open to the internet. The fourth, a plan file readable by anyone with build-log access that carries a database password, is a pipeline and state problem covered below.
resource "aws_iam_role" "report_worker" {
name = "report-worker"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRoleWithWebIdentity"
Principal = { Federated = "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com" }
Condition = {
StringEquals = { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" }
StringLike = { "token.actions.githubusercontent.com:sub" = "repo:company/reports:environment:*" }
}
}]
})
}
resource "aws_iam_role_policy" "report_worker" {
role = aws_iam_role.report_worker.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{ Effect = "Allow", Action = "s3:*", Resource = "arn:aws:s3:::exports/*" }]
})
}
resource "aws_vpc_security_group_ingress_rule" "db" {
security_group_id = "sg-0123456789abcdef0"
ip_protocol = "tcp"
from_port = 5432
to_port = 5432
cidr_ipv4 = "0.0.0.0/0"
}- Identity: prefer appropriate workload roles and temporary credentials; inspect trust relationships as carefully as permission policies, as the cloud IAM trust guide at /domains/runtime/cloud-posture/cloud-iam-trust/ shows.
- Permission: examine wildcard actions/resources, cross-account access, policy conditions, administrative privileges, and who can change the controls themselves.
- Network: explain every new public endpoint or broad ingress rule. Confirm whether private naming or an internal load balancer actually enforces the required boundary.
- Data: identify storage exposure, encryption/key access, backup behavior, deletion protection, and where audit evidence goes.
- Operations: identify the deployer, the approved plan, drift handling, rollback constraints, and the owner of any deliberate exception.
Rules that every change must meet belong in a policy the pipeline runs on the plan JSON, so a reviewer spends time on the judgment calls. The Rego policy below, run with conftest, checks the three access questions: wildcard actions in permission policies, a GitHub OIDC subject matched with anything other than StringEquals, and ports other than 80 and 443 opened to 0.0.0.0/0 or ::/0. On the plan for R17 it reported three failures, one per defect; on the plan for R18 all three rules passed.
The same approach works with other engines: HCP Terraform and Terraform Enterprise run Sentinel or OPA policies on every plan, and scanners such as Checkov and Trivy ship hundreds of ready-made checks. A policy only sees what is in the plan, so the trust path through the pipeline itself and existing resources outside the configuration still need review.
# Review rules for a Terraform plan: terraform show -json plan.bin > plan.json
package main
changes contains rc if {
some rc in input.resource_changes
some action in rc.change.actions
action in {"create", "update"}
}
# 1. Permission policies must not allow wildcard actions
deny contains msg if {
some rc in changes
rc.type in {"aws_iam_role_policy", "aws_iam_policy"}
some statement in as_array(json.unmarshal(rc.change.after.policy).Statement)
statement.Effect == "Allow"
some action in as_array(statement.Action)
contains(action, "*")
msg := sprintf("%s: Allow with wildcard action %q", [rc.address, action])
}
# 2. A GitHub OIDC trust must pin one exact subject with StringEquals
deny contains msg if {
some rc in changes
rc.type == "aws_iam_role"
some statement in as_array(json.unmarshal(rc.change.after.assume_role_policy).Statement)
some operator, conditions in statement.Condition
some key, value in conditions
endswith(key, ":sub")
operator != "StringEquals"
msg := sprintf("%s: %s on %s matches %v; use StringEquals", [rc.address, operator, key, value])
}
# 3. Nothing but the public web ports may be opened to the whole internet
deny contains msg if {
some rc in changes
rc.type == "aws_vpc_security_group_ingress_rule"
some cidr in {rc.change.after.cidr_ipv4, rc.change.after.cidr_ipv6}
cidr in {"0.0.0.0/0", "::/0"}
not rc.change.after.from_port in {80, 443}
msg := sprintf("%s: port %v open to %s", [rc.address, rc.change.after.from_port, cidr])
}
as_array(x) := x if is_array(x)
as_array(x) := [x] if not is_array(x)Terraform stores sensitive values in state and plan files; sensitive = true only redacts them from terminal and UI output. In a test with Terraform 1.16.5, a password passed through a sensitive variable printed as "(sensitive value)" in the plan output but appeared in clear text in terraform show -json and in the saved plan file. Since Terraform 1.10, variables can be ephemeral, and since 1.11 resources can take write-only arguments: the same password passed through an ephemeral variable to value_wo appeared in neither file. Use them where the provider supports them, and restrict access to, encrypt and limit the retention of state and plans everywhere else.
Do not paste complete plans into public tickets or unconstrained logging systems. Review the deployment pipeline’s artifact visibility and the identities able to read previous state versions.
# Terraform 1.11+: an ephemeral variable passed to a write-only argument never reaches
# the plan or state. A plain `sensitive = true` variable is only hidden in the terminal.
variable "db_password" {
type = string
sensitive = true
ephemeral = true
}
resource "aws_ssm_parameter" "db_password" {
name = "/reports/db-password"
type = "SecureString"
value_wo = var.db_password
value_wo_version = 1 # increase to write a new value
}In an authorized test environment, prove that the intended workload can perform its required action while an unrelated identity cannot. Check actual public reachability and effective policies after deployment; configuration review alone cannot establish the final state. The policy and drift guide at /domains/runtime/cloud-posture/iac-policy-drift/ covers checks after apply. Retain the approved plan, deployed revision, access-test results, and exception decisions. Repeat important checks when policies, providers, or deployment identities change.
A wildcard replacement is not automatically least privilege. Bucket-level and object-level actions use different resource forms in AWS; copy the service’s documented action/resource relationships, not one ARN shape for every action. Multipart uploads or a particular client may need additional narrowly scoped operations. Establish that need through the supported client’s actual calls, rather than restoring s3:* after the first failure.
A clean infrastructure scan does not establish that a secret disappeared from older state versions or downloaded plans. Revoke any exposed credential and review readers and retained copies. Do not attempt to sanitize the problem by merely toggling sensitive. Conversely, never publish raw state as the evidence for a security review; record redacted identifiers and controlled artifact references.
These outcomes belong to the worked example; the policy checks the plan-level rows, and the access rows need tests against the deployed account.
| Case | Expected result | What it establishes |
|---|---|---|
Production PutObject north/new.csv | Allow | Required upload still works |
Production GetObject north/new.csv | Deny under the stated role contract | Write-only role is actually narrow |
Production PutObject south/new.csv | Deny | Tenant prefix is enforced |
| Staging acquires production role | Deny | Trust policy is separately restricted |
| Unrelated source connects to database | Deny | Unrequested ingress is removed |
Guided practice
Review change R17 using only the supplied requirement and plan. Produce separate decisions for identity acquisition, object operations, network exposure and artifact handling.
Supplied scenario material
R17 is the Terraform shown above; its plan file is readable by everyone with build-log access and carries a database password from a variable marked sensitive. Revision R18 removes database ingress, allows only production identity P, and grants PutObject on exports/north/*. Plan P18 was approved. Pipeline later creates plan P19 from a different provider lockfile and deploys P19. Observed tests: P writes north/a.csv; P cannot read it; staging assumes the role successfully. A shared log artifact contains the old plan with a database credential.1. Identify independent defects
List each original capability with its missing business justification. Do not combine trust and permission into one finding.
Expected result and reasoning
The four defects are staging-to-production acquisition, excessive object actions/prefixes, unrelated public database ingress, and broad access to secret-bearing plan artifacts. Each has a separate control and acceptance test.
2. Review the claimed repair
Compare R18 and the observed staging test. Decide whether a narrow permission list closes the finding.
Expected result and reasoning
R18 appears to repair the documented intent, but staging still assumes the deployed role. Either another trust path remains or P19 differs. The acquisition finding stays open even though the production upload succeeds.
3. Bind approval to execution
Explain the significance of approving P18 and deploying P19. Name the evidence needed next.
Expected result and reasoning
P18 approval is not approval of P19. Compare the exact plans and provider lockfiles, identify the deployed trust policy and all permission attachments, and rerun the intended principal/action/resource tests. Do not label P19 compliant from a P18 screenshot.
4. Handle retained credentials
Decide what changes after discovering the old plan credential. Separate future artifact policy from response.
Expected result and reasoning
Restrict future plan access and retention, but also treat the already exposed credential as compromised: rotate/revoke it, review permitted readers and access history, and remove retained copies where policy allows. Redaction in the next build does not invalidate an old credential.
The review uses the supplied plans and observations; the Terraform, Rego policy and state test on this page show the same findings with real tools.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
Reference information
- SDLC phase
- ReleaseMaintenance
- ASVS 5.0.0 requirements
- v5.0.0-13.2.2 · v5.0.0-13.3.1 · v5.0.0-13.3.2