Terraform Security Review for Infrastructure as Code
Terraform review asks whether infrastructure changes are predictable, attributable, and protected from secret exposure. HashiCorp states that state and plan files can contain sensitive resource attributes. sensitive = true redacts normal CLI and UI display, but does not prevent value storage in state or plans. Treat state, saved plans, logs, and backend access as production-sensitive material.
Review package
Collect root and module source, terraform.lock.hcl, backend configuration, workspace or environment map, CI identity configuration, terraform validate output, reviewed plan artifact, apply record, and exception register. Tie every record to commit, Terraform version, provider lock selections, workspace, timestamp, owner, and pass/fail decision.
| Surface | Evidence criteria | Pass condition | Owner |
|---|---|---|---|
| State backend | encryption, access list, audit logs, recovery path | least privilege and protected remote state | platform owner |
| Sensitive values | variable/output declarations and state exposure review | no secret default; handling matches storage need | module owner |
| Providers | required source/version and lockfile diff | pinned, reviewed selections | code owner |
| Plan | binary plan plus rendered review or trusted CI record | reviewer checks intended change before apply | change owner |
| Execution | CI identity and apply log | attributable identity; approved path only | release owner |
Protect state before writing resources
Local terraform.tfstate is plaintext. Exclude it and plan files from version control, but do not stop there: use remote state with encryption at rest, TLS in transit, narrow read/write roles, audit logging, backup and recovery testing. Separate environments and limit who can read state; read capability can expose credentials, database passwords, endpoints, and resource metadata.
HashiCorp documents that sensitive redacts display but still stores value in state and plan files. For temporary values, supported Terraform versions can use ephemeral = true in supported contexts or provider write-only arguments; verify actual provider behavior before relying on either. Do not expose outputs with terraform output -raw or -json in shared logs. Rotate any secret already committed to state, a plan artifact, CI log, or source; deleting source line does not remove historic copies.
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
variable "database_password" {
type = string
sensitive = true
ephemeral = true # use only where Terraform/provider support permits
}
This sample does not make a backend secure and is not a complete production configuration. Evidence must show backend policy, encryption setting, authorized principals, and an access-log export.
Pin inputs, review plans
Pin provider source and version constraint, commit reviewed terraform.lock.hcl, and inspect lockfile changes in pull request. Version pinning limits unexpected resolution changes; it does not make provider code safe. Review module source, revision, input validation, output exposure, and resource defaults. Floating module references, broad * IAM actions, public ingress, disabled encryption, and destructive lifecycle changes need explicit finding or justified exception.
Run terraform fmt -check and terraform validate. Generate plan in controlled environment with intended workspace and inputs. Preserve a digest or protected CI artifact, then have named reviewer compare creates, updates, destroys, access changes, public exposure, encryption changes, and data-loss risk against approved request. Apply same saved plan only when environment and credentials match review assumptions; regenerate and re-review after source, lockfile, variable, or target changes.
Evidence, exceptions, remediation
Pass needs completed artifact, not statement of intent. Example remediation record: provider lockfile pull request merged; plan shows no unintended destroy; state-role policy tested to deny unauthorized read; security owner records result. Fail when plan is reviewed after apply, state role is shared, or secret is stored in default variable and no migration exists.
Exception needs affected module/resource, threat, reason, compensating control, business owner, approver, expiry, remediation ticket, and retest date. Expiry without renewal returns fail. Deliver scope, source revision, state-access review, provider/module register, plan review record, findings, exceptions, remediation owners, and residual limits. Terraform review cannot prove deployed cloud has no drift; compare runtime configuration separately. Source review can assess agreed modules.
Worked case: destructive plan gate
Run plan against isolated test state after changing one resource property that requires replacement. Expected output identifies replacement before apply, lists affected dependencies, and contains no unreviewed resource address outside intended scope. Save plan artifact only where it is protected from secret disclosure, then run policy and format validation against same revision. Reviewer compares planned changes with change request and rejects unknown drift rather than applying it to make plan clean.
Test missing variable, deleted remote object, provider upgrade, state lock contention, and import of pre-existing resource. Closure evidence includes commit revision, sanitized plan summary, policy result, reviewer decision, applied output for test environment, and post-apply refresh showing intended state. An emergency bypass requires named approver, narrow target, expiry, and retrospective plan review. IaC review closes after observed state and declared configuration agree, not when syntax passes.
Before final apply, compare resource addresses, replacement actions, and provider versions with approved change. Expected output contains stated scope only. Keep review artifact sanitized and link any unexpected drift to separate remediation, not this deployment.