Terraform and IaC security: a technical engineering guide
Secure Terraform from module to apply: provider supply chain, state and plan data, CI identity, policy as code, drift, detection and testing.
- AUTHOR
- Karol Rapacz / Pentester (OSCP, PNPT)
- PUBLISHED
- 24 April 2026
- READING TIME
- 21 min read
- TOPIC
- DevSecOps and Cloud
Terraform and Infrastructure as Code security extend far beyond scanning .tf files. Terraform downloads executable provider plugins, resolves modules, processes variables, generates a plan, stores resource mappings in state and invokes APIs under the runner identity. Compromise of any one of those elements can alter infrastructure, disclose secrets or bypass manually configured cloud controls.
The essential principle is: the IaC pipeline is a privileged management system, and its plan, state, providers and runners belong inside the security boundary. A credible assessment traces the complete route from commit to resource and the telemetry path back. Dynamic tests belong in an isolated account or project and should create harmless, tagged resources. Do not run an unreviewed production apply merely to demonstrate an issue already proven by the plan and policy decision.
Terraform threat model
A practical model contains at least nine elements:
| Element | Security question |
|---|---|
| Repository | who can change code, ownership, workflows and review requirements? |
| Modules | where do they originate, which version resolves and is the source immutable? |
| Providers | which executable plugin runs and how is its integrity established? |
| Variables | where do sensitive values originate and who can retrieve them? |
| Plan | who created it, for which commit, and can it change after approval? |
| State | where is it stored and who can read, change, lock or recover it? |
| Runner | which identity, network, filesystem and tools are available to the process? |
| Backend | how do encryption, locking, versioning and tenant separation work? |
| Cloud | what is the maximum outcome available to the plan or apply identity? |
Add the pull-request system, registries, CI cache, artefact store, policy engine, webhooks and platform administrators to the graph. Developer error is only one threat. A compromised account, malicious dependency update, replaced runner, exposed state or unmanaged cloud drift can produce the same business impact.
What happens during init, plan and apply
During terraform init, Terraform initialises the backend, downloads modules and installs providers. This is the point at which the workflow meets its external software supply chain. terraform plan refreshes known state, compares configuration with provider objects and proposes actions. A saved plan can then be supplied to terraform apply, which executes operations through provider APIs.
A secure workflow binds four things together:
- the exact code commit;
- the resolved module and provider set;
- one approved plan artefact;
- the identity and environment performing apply.
If a pipeline regenerates the plan after approval or resolves different dependencies, reviewers did not approve what will be deployed. The terraform plan documentation distinguishes speculative plans from saved plans intended for automation. Treat the saved plan as a sensitive executable artefact, not an ordinary text log.
Providers execute code with runner authority
A Terraform provider is an executable plugin. It communicates with Terraform but runs in the runner environment and uses credentials and network access available there. Provider trust therefore covers source, version, integrity and the update process.
.terraform.lock.hcl records selected provider versions and package hashes. The dependency lock-file documentation explains how Terraform verifies a downloaded package against the recorded checksums. Commit the lock file and review its changes. A provider upgrade should be a visible dependency event with release-note review and tests.
An important boundary is that the lock file currently tracks provider dependencies, not remote module versions. Constrain a module through source and version or an immutable commit governed by your organisation. A moving branch, mutable tag or uncontrolled Git source means that the same configuration commit can resolve different code later.
A private mirror or registry improves availability and allows an allowlist, but it does not by itself prove provenance. Apply SBOM, signing and VEX practices to tooling executed inside the deployment pipeline.
Terraform state is a high-value security asset
State maps Terraform addresses to provider objects and retains attributes required for later operations. It can contain identifiers, endpoints, connection metadata, generated passwords and other sensitive values. Marking a variable or output as sensitive reduces display in CLI and UI; it does not automatically remove the underlying value from state.
HashiCorp documents the protection of sensitive data in state. A production backend needs:
- encryption in transit and at rest under a governed key;
- precise read, write, lock and historical-version permissions;
- versioning and protection against accidental deletion;
- reliable concurrency locking;
- separate state for environments and trust boundaries;
- central logs for reads and changes;
- prohibition on publishing state as a CI artefact or ticket attachment;
- a recovery procedure that preserves access controls.
Do not store state in Git. Removing a committed file does not remove it from history, caches, backups and forks. An incident requires rotation of exposed values, not only repository cleanup.
Plans and diagnostic logs can also expose secrets
A plan includes proposed values required to execute changes. UI redaction is not a guarantee that a saved plan file excludes sensitive data. Access to the artefact should reflect the infrastructure authority it represents. Apply encryption, short retention and a one-run binding.
Terraform and provider debug logs may contain requests, responses or variable values. Do not enable verbose logging globally on a shared runner. Diagnostic bundles require restricted access, automated redaction and deliberate retention.
Retrieve credentials at execution time from a controlled system, prefer short-lived identity and avoid passing a secret value through Terraform where the service can consume a reference instead. A mature Vault, KMS and secrets-rotation process should define ownership and incident handling.
Runner identity and blast radius
A planning identity often needs broad read access, while apply needs write permissions. This does not justify one global administrator. Separate roles by environment, stack and responsibility. Prefer OIDC federation and short-lived sessions over static cloud keys stored in CI secrets.
Also establish:
- who may change workflow definitions and runner selection;
- whether untrusted pull-request code receives credentials;
- whether the runner is ephemeral and wiped after each job;
- whether outbound access permits arbitrary executable downloads;
- whether
.terraformcache crosses trust boundaries; - whether plan and apply use different roles;
- whether production apply needs approval independent of the change author;
- whether break-glass access is logged and expires.
The execution role should have an organisation guardrail, boundary or provider equivalent that prevents outcomes Terraform must never reach. That constraint protects against a malicious module and a provider bug as well as user error. A broader cloud penetration test should prove the guardrail outside the pipeline’s happy path.
Policy as code blocks unsafe outcomes before apply
Policy as code evaluates configuration or the plan against organisational requirements. Useful rules reject public storage, unencrypted databases, global ingress, prohibited Regions, disabled audit logs, privileged workloads and resources missing ownership tags.
Strong rules are outcome-oriented and include:
- control identifier and accountable owner;
- a clear failure message with a safe pattern;
- positive and negative tests;
- version control and peer review;
- a time-limited exception route;
- false-positive and remediation-time metrics;
- a monitoring phase before a new rule becomes mandatory.
HCP Terraform provides policy enforcement through Sentinel and OPA integrations. The product is less important than the invariant: the policy engine must evaluate the same saved plan that apply will consume, and bypass must be strongly controlled and visible.
Drift and out-of-band changes
IaC does not prove that every change goes through code. An administrator may edit a resource, the service may supply defaults, and an operator or autoscaler may create dynamic objects. Detect drift through scheduled read-only plans, provider-native configuration services, cloud audit logs and posture-management rules.
Do not automatically apply every difference. A plan may erase an emergency change or remove a resource transferred to another management system. Classify the drift, identify its owner, decide whether to import, revert or document it, and then test the selected action.
Safe Terraform assessment methodology
1. Map the pipeline
Diagram commit → review → init → plan → policy → approval → apply → state → monitoring. For every edge, record identity, artefact, storage, network access and logging.
2. Perform static analysis
Review providers, modules, sources, versions, backend, variables, outputs, dynamic blocks and high-impact resources. Combine scanners with manual module analysis because a syntax rule cannot understand every business boundary.
3. Generate a plan in isolation
Use a dedicated account or project and harmless data. Test policy enforcement, approval and runner identity. Never give production secrets to code from an untrusted branch.
4. Apply only a minimal canary
If dynamic proof is needed, create a small tagged resource under a cost limit and automatic cleanup. Confirm cloud audit events, state handling, drift detection and destroy. Do not touch existing business data.
5. Exercise failure controls
Test state locking, job interruption, provider unavailability, token expiry and the recovery workflow. Do not corrupt production state. Use a copy and isolated backend.
Detection and incident response
Correlation should connect commit, author, review, plan hash, job, runner, cloud session and resulting resource changes. Alert on:
- workflow, backend, module-source or provider changes;
- unusual
.terraform.lock.hclupdates; - production apply outside the approved pipeline;
- state access or modification by a new identity;
- policy-check disablement or an ownerless exception;
- cloud changes without a corresponding commit and run ID;
- unexpected download of a saved plan;
- static credentials or an unusually long session.
During an incident, stop applies, preserve the plan and logs, revoke sessions, block the malicious commit, inspect state and cloud audit events, and rotate reachable credentials. Rebuild the runner from a trusted image before resuming deployments.
Terraform and IaC hardening
- Protect branches, CODEOWNERS, review rules and build provenance.
- Pin providers, commit the lock file and upgrade through dedicated pull requests.
- Pin modules to governed versions; remember that provider lock data does not cover them.
- Use ephemeral runners and short-lived OIDC federation.
- Separate plan and apply roles and isolate production from test environments.
- Encrypt, version, lock and audit remote state, including historical versions.
- Treat plans, logs and caches as potentially sensitive artefacts.
- Enforce policy against the saved plan and govern every exception.
- Detect drift but require a decision before automated correction.
- Build safe defaults using CISA Secure by Design principles and continuously review common cloud misconfigurations.
Terraform security checklist
- The pipeline binds commit, lock file, plan, approval and apply.
- Provider and module sources are version-constrained and governed.
- The lock file is committed and reviewed when changed.
- State is remote, encrypted, versioned, locked and audited.
- Sensitive values do not enter the repository, logs or persistent artefacts.
- The runner is ephemeral and uses a short-lived identity.
- Untrusted pull-request code receives neither secrets nor the apply role.
- Policy as code evaluates exactly the plan selected for deployment.
- Drift is detected and routed through a formal decision process.
- Cloud audit logs connect each change to a run ID and commit.
- State failure and recovery tests use an isolated copy.