Terraform security is the discipline of making sure the infrastructure you define as code is safe by the time it reaches a cloud account, not just convenient to write. Terraform is a superb way to describe infrastructure declaratively and reproducibly, but it will build exactly what you tell it to, including a wide-open security group or a public database if that is what the configuration says. The good news is that because everything is code, every risk is reviewable, testable, and catchable before anything is provisioned. This deep dive walks through the Terraform-specific practices that matter most, from the state file to CI, and fits inside our broader infrastructure as code security guide.
It helps to keep one idea in mind throughout: with Terraform, a security problem and a code change are the same thing. A misconfiguration is a line in a pull request, an over-privileged role is a resource block, a leaked secret is a committed file. That is a gift, because it means you can apply the same tools you already trust for application code, version control, review, automated testing, and CI gates, to the infrastructure itself. The practices below are simply the highest-leverage places to point those tools. None of them require slowing your team down; most of them are one-time setup that then protects every future change automatically.
The state file is your biggest risk
Every conversation about Terraform security should start with state. Terraform records the real-world resources it manages in a state file, and that file is not just metadata: it can contain secrets in plaintext. Database passwords, generated access keys, private keys, and any sensitive attribute Terraform reads back from a provider can all land in state exactly as they are. Anyone who can read the state can read those secrets.
The first rule follows directly: never commit state to version control. A terraform.tfstate file in Git is a credential leak waiting to be cloned, and it stays in history even after you delete it. Add it to .gitignore on day one.
Instead, use a remote backend with encryption at rest, state locking, and tight access control. Locking prevents two runs from corrupting state simultaneously, encryption protects the secrets inside it, and access control keeps state readable only by the pipelines and people who genuinely need it.
terraform {
backend "s3" {
bucket = "acme-tfstate-prod"
key = "network/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "tf-locks"
}
}
Lock down the backend itself as well: block public access on the storage bucket, restrict who can read it with a scoped policy, and turn on versioning so you can recover from a bad apply. Treat the state store like the crown-jewels datastore it is.
Two related habits go a long way. First, audit who and what can read state, and revisit that list periodically; access tends to accumulate as teams grow, and a broad read grant on a state bucket quietly becomes a broad read grant on every secret inside it. Second, if you genuinely do not want a value in state at all, avoid having Terraform manage the resource that produces it, or generate the secret outside Terraform and reference it, so the sensitive material never flows through the state file in the first place. State encryption protects the file at rest, but reducing what lands in state at all is the stronger control.
Keep secrets out of code and variables
The second recurring Terraform security problem is hardcoded secrets. It is tempting to drop an API token straight into a .tf file or a terraform.tfvars, but those files are usually committed, and anything committed is effectively public within your organization.
Do not hardcode secrets, and do not commit .tfvars files that contain them. Instead, pull secrets at runtime from a dedicated secrets manager or a vault provider so the value lives in a purpose-built system rather than in your repository.
data "vault_kv_secret_v2" "db" {
mount = "secret"
name = "prod/database"
}
resource "aws_db_instance" "main" {
username = "app"
password = data.vault_kv_secret_v2.db.data["password"]
}
When you do accept sensitive input, mark it as such. Setting sensitive = true on a variable or output keeps its value from being printed in plan and apply output, which is a common way secrets end up pasted into CI logs and chat channels.
variable "db_password" {
type = string
sensitive = true
}
Marking values sensitive reduces exposure but does not encrypt state, so this practice works alongside a secured backend rather than replacing it.
Give providers least privilege
Terraform acts on your cloud through provider credentials, and those credentials are often far more powerful than any single configuration needs. A pipeline that only manages networking does not need permission to delete databases or rotate IAM users.
Scope each provider's identity to the resources it actually manages. Prefer short-lived, federated credentials such as OpenID Connect from your CI system over long-lived static keys, and separate roles per environment so a compromised development pipeline cannot touch production. Least privilege here limits the blast radius if a run, a runner, or a set of credentials is ever compromised.
Getting the scope exactly right on the first try is hard, so treat it as iterative. Start deliberately narrow, run a plan, and widen the permissions only for the specific actions Terraform actually needs, rather than granting a broad administrative role and promising to tighten it later, a promise that is rarely kept. It is also worth being explicit about where the credentials live: a long-lived key baked into a developer's laptop or a CI variable is itself a secret to protect, which is another reason federated, expiring credentials are the safer default.
Mind the module supply chain
Modules are one of Terraform's best features and one of its quieter risks. When you pull a module from a public registry, you are running someone else's code with your credentials, and an unpinned reference means the code can change under you without warning.
Pin both module and provider versions to specific, known-good releases rather than floating ranges, so an upstream change never lands in your plan unreviewed. Vet registry sources before you adopt them, and for anything sensitive or widely reused, host vetted modules in a private registry you control.
module "vpc" {
source = "app.private-registry.acme.com/networking/vpc/aws"
version = "3.4.1"
}
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.40"
}
}
}
The dependency lock file, .terraform.lock.hcl, complements this by recording exact provider versions and checksums. Commit it so every run and every teammate resolves the same, verified provider binaries.
Catch insecure resource defaults with static analysis
Most real-world Terraform security incidents are not exotic. They are ordinary misconfigurations: a storage bucket left public, a security group opened to 0.0.0.0/0, a volume or database created without encryption, a database exposed to the internet. Cloud providers often make the insecure option easy, and a small omission in HCL becomes a real exposure.
# Risky: open to the entire internet
resource "aws_security_group_rule" "ssh" {
type = "ingress"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
This is exactly what static analysis, sometimes called IaC scanning, is built to find. A scanner parses your Terraform before it is applied and flags the public bucket, the world-open port, the unencrypted disk, and the missing logging setting, mapping each to the resource and line that caused it. Because it reads the plan rather than the running cloud, it catches problems while they are still a one-line fix in a pull request rather than an incident in production. Many of these misconfigurations map to the access-control and misconfiguration risks highlighted in the OWASP Top 10 (2025), which makes them an easy shared language between security and platform teams.
Enforce policy as code
Static scanning tells you what is misconfigured against a general baseline. Policy as code lets you encode your organization's own rules and enforce them automatically. Using an OPA or Sentinel-style engine, you write guardrails as testable code: every bucket must be encrypted, no security group may allow unrestricted inbound access, production resources must carry a cost-center tag, only approved regions are permitted.
The key is to evaluate these policies at plan time, before apply, so a violation blocks the change rather than documenting it after the fact. Policy as code turns tribal knowledge and wiki pages into automated gates that apply consistently to every change, from every engineer, on every run.
Detect drift
Even perfectly secured Terraform can be undermined after the fact when someone makes a change directly in the cloud console. That divergence between your code and reality is called drift, and it is a security concern as much as an operational one: a manually opened port or a disabled encryption setting will not show up in your reviewed configuration.
Run terraform plan regularly against production, ideally on a schedule, and treat unexpected diffs as signals to investigate. Detecting drift early keeps your code the true source of record and stops out-of-band changes from quietly weakening your posture. Pair drift detection with a cultural rule that the console is for reading, not changing: when every production change flows through a reviewed pull request, drift becomes the exception that stands out rather than the norm you have learned to ignore.
Scan Terraform in CI
All of these practices come together in your pipeline. A solid Terraform CI stage runs terraform fmt -check and terraform validate to catch formatting and syntax issues, generates a terraform plan, and then runs an IaC security scan and your policy checks against that plan.
The crucial detail is failure behavior: configure the pipeline to fail on critical findings so insecure infrastructure cannot merge. Warnings can inform, but criticals should block. Running the scan on every pull request also gives reviewers security feedback inline, next to the change, which is where it is cheapest to act on.
Order matters here too. Run the fast, cheap checks first so a formatting or validation error fails in seconds, and save the security scan and policy evaluation for the plan, which is the most faithful representation of what will actually change. Scanning the plan rather than only the raw configuration catches issues that arise from how modules and variables resolve together, not just what a single file says in isolation. Finally, keep the ruleset in version control alongside the infrastructure so that tightening a policy is itself a reviewed change with a clear history, and so every branch is measured against the same bar.
# Illustrative CI stage
steps:
- run: terraform fmt -check
- run: terraform validate
- run: terraform plan -out=plan.tfplan
- run: iac-scan plan.tfplan --fail-on=critical
How Rainforest helps
Rainforest provides generic IaC security scanning that fits naturally into this workflow. It analyzes your Terraform for insecure defaults and misconfigurations, connects findings to the exact resource and line, and runs inside CI so issues are caught on pull requests before anything is provisioned. Because it treats infrastructure as code as part of the same application security picture, you get one consistent view across your code and your cloud definitions rather than a separate silo. You can read more about our approach to IaC security testing and how it fits into the wider application security testing platform.
Terraform gives you an enormous amount of leverage over your infrastructure, and security is about making sure that leverage points in the right direction. Secure your state, keep secrets in a vault, scope your providers, pin your modules, and let automated scanning and policy checks catch the rest in CI. If you want to see how automated Terraform scanning looks against your own configurations, book a demo. For neighboring platforms, our guides on CloudFormation security and Kubernetes security apply the same principles to their respective ecosystems.
Frequently asked questions
Is Terraform secure by default?
No. Terraform faithfully provisions whatever your configuration describes, and cloud providers often default to the more permissive option. If your code specifies a public bucket, an unencrypted volume, or a security group open to the internet, Terraform will build exactly that. Security comes from how you write and review the configuration, protect state, and scan changes, not from Terraform itself.
How do I secure Terraform state?
Use a remote backend with encryption at rest, state locking, and strict access control, and never commit state to version control. State can hold secrets in plaintext, so restrict who and what can read it, enable versioning on the backing store, and block public access to the bucket or container that holds it. Treat the state store like a sensitive datastore.
How do I keep secrets out of Terraform?
Do not hardcode secrets in .tf or .tfvars files, and do not commit files that contain them. Pull secrets at runtime from a secrets manager or vault provider, and mark sensitive variables and outputs with sensitive = true so they are not printed in plan or apply logs. Remember that marking values sensitive does not encrypt state, so pair it with a secured backend.
How do I scan Terraform for misconfigurations?
Run static analysis, also called IaC scanning, against your Terraform before you apply it. A scanner reads your configuration or plan and flags insecure defaults such as public storage, open ports, and unencrypted resources, tying each finding to the resource and line. Wire the scan into CI so it runs on every pull request and fails the build on critical findings.
What is policy as code for Terraform?
Policy as code means writing your organization's security and compliance rules as executable checks, using an OPA or Sentinel-style engine, and enforcing them automatically. Evaluated at plan time, these guardrails block changes that violate rules such as requiring encryption, forbidding unrestricted inbound access, or limiting allowed regions, turning written standards into consistent automated gates.

Written by
Bruno Baldo
CMO
Um pouco de marketing e um pouco de curiosidade e temos a receita pra criar um apaixonado por cyber!

Infrastructure as Code (IaC) Security: The Complete Guide
IaC security explained: the threat model, IaC scanning across the SDLC, policy as code, secrets and state, and a practical remediation workflow.

AWS CloudFormation Security: Best Practices
A practical guide to CloudFormation security: secrets handling, IAM least privilege, insecure defaults, drift, stack policies, and scanning in CI.

Kubernetes Security: The Complete Guide
A complete guide to Kubernetes security: the 4C's model, attack surface, RBAC, pods, network policies, secrets, supply chain, and shift-left practices.
