The scariest Terraform mistake I’ve watched someone make wasn’t in the code itself — it was in the state file. A public S3 bucket, used as a Terraform backend, with no encryption and no access restrictions, sitting there with every database password and API key the team had ever provisioned in plaintext. Terraform makes infrastructure reproducible and version-controlled, which is fantastic for velocity, but that same convenience means a single mistake can expose or misconfigure your entire infrastructure at once.
Here’s how I think about Terraform security across the full lifecycle — from state management to the actual resources it provisions.
Where Risk Lives in a Terraform Workflow
flowchart TD
A[Terraform Code] --> B[Plan]
B --> C[State File]
C --> D[Apply]
D --> E[Provisioned Infrastructure]
C -.Exposed Secrets Risk.-> F[Attacker Access]
A -.Hardcoded Credentials.-> F
style C fill:#f96,stroke:#333
style F fill:#f66,stroke:#333
1. Secure Your Terraform State
Terraform state files often contain sensitive data in plaintext — database passwords, private keys, and other resource attributes captured at apply time. Treat your state backend with the same seriousness as a secrets manager.
- Use a remote backend (S3, Azure Storage, GCS, or Terraform Cloud) rather than local state files.
- Enable encryption at rest on the backend storage.
- Restrict access with strict IAM policies — state should not be broadly readable.
- Enable state locking (via DynamoDB for S3 backends, or natively in Terraform Cloud) to prevent concurrent modification and corruption.
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
2. Never Hardcode Secrets in Terraform Code
Credentials committed to .tf files end up in version control history permanently, even if you delete them in a later commit.
- Use environment variables or a secrets manager (Vault, AWS Secrets Manager) to inject sensitive values.
- Mark sensitive variables explicitly so Terraform redacts them from plan/apply output.
variable "db_password" {
type = string
sensitive = true
}
- Add
*.tfvarsfiles containing real values to.gitignore, and use a scanning tool likegitleaksortrufflehogin CI to catch accidental commits.
3. Apply Least Privilege to the Terraform Execution Role
The IAM role or service principal Terraform runs as should have exactly the permissions needed to manage the resources in its scope — not broad administrator access “to avoid permission errors.”
- Scope IAM policies to specific resource types and, where possible, specific resource ARNs.
- Use separate execution roles per environment (dev, staging, production) so a mistake in one doesn’t cascade into another.
- Review and prune permissions periodically as your infrastructure evolves.
4. Scan Terraform Code for Misconfigurations
Static analysis tools can catch dangerous patterns before they’re ever applied — a security group open to 0.0.0.0/0, an S3 bucket without encryption, or a database with public accessibility enabled.
tfsec .
checkov -d .
Both tfsec and checkov are widely used, open-source options that plug directly into CI pipelines and fail builds on high-severity findings, the same way image scanners do for container builds — see my related post on Container Image Scanning Explained for a parallel approach in a different part of the stack.
5. Use Modules from Trusted Sources
Public Terraform Registry modules are convenient, but pulling infrastructure code from an unverified or unmaintained source is a supply chain risk, just like an unpinned container base image.
- Pin module versions explicitly rather than tracking
latestor a branch reference. - Review the source of third-party modules before adopting them widely.
- Prefer modules maintained by verified publishers or your own organization for anything security-sensitive.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.8.1"
}
6. Integrate Terraform Securely into CI/CD
If you’re running Terraform through Jenkins or another CI/CD system, the same daemon and credential-handling principles that apply to Docker apply here too — protect the execution environment, restrict who can trigger applies against production, and require plan review before apply. I’ve covered practical setup patterns in Using Jenkins with Terraform for Infrastructure as Code and the broader deployment context in Setting Up Jenkins for Continuous Deployment on AWS.
- Require a manual approval step before
terraform applyruns against production. - Store CI credentials for cloud providers as encrypted secrets, never as plaintext environment variables in job configuration.
- Log every plan and apply for audit purposes.
7. Review Plans Before Applying
terraform plan exists precisely so you can review exactly what’s about to change before it happens. Never skip this step in production workflows, even when it feels like a formality.
terraform plan -out=tfplan
terraform apply tfplan
Applying a saved plan (rather than re-running apply independently) also guarantees the plan reviewed is exactly the plan executed, with no drift in between.
Common Terraform Security Mistakes
- Storing state locally or in an unencrypted, publicly accessible bucket.
- Hardcoding cloud credentials or database passwords directly in
.tffiles. - Granting the Terraform execution role broad administrator permissions.
- Using unpinned module versions that can change unexpectedly between runs.
- Skipping
terraform planreview and applying directly in production. - Never running static analysis, so misconfigurations like open security groups slip straight into production.
Best Practices Checklist
- Use a remote, encrypted, access-restricted backend for state.
- Mark sensitive variables and never hardcode secrets in code.
- Scope the Terraform execution role to least privilege, per environment.
- Run
tfsecorcheckovin CI on every pull request. - Pin module versions and vet third-party sources.
- Require plan review and manual approval before production applies.
- Enable state locking to prevent concurrent modification corruption.
FAQs
Is Terraform state always sensitive? Often, yes. Many resource types store attributes in state that include sensitive values, even if you didn’t explicitly define them as secrets in your code — so treat all state as sensitive by default.
Can I use Terraform Cloud instead of managing my own backend? Yes, and it handles state encryption, locking, and access control for you, which removes a significant amount of the manual setup described above.
How do static analysis tools like tfsec differ from cloud provider security tools? Static analysis tools check your Terraform code before anything is deployed, catching misconfigurations early, while cloud-native tools (like AWS Security Hub) typically assess resources after they already exist.
Should every Terraform apply require manual approval? For production environments, yes — a manual review of the plan output is a cheap, high-value control that catches unintended changes before they affect live infrastructure.
Conclusion
Terraform’s biggest security risks aren’t usually in some exotic edge case — they’re in how state is stored, how credentials are handled, and how much privilege the execution role is granted. Lock down your backend, keep secrets out of code, scope permissions tightly, and add static analysis to your pipeline. Combined with careful CI/CD practices, this turns Terraform from a potential single point of failure into one of the most auditable, secure parts of how you manage infrastructure.