I didn’t learn Terraform security from a whitepaper. I learned it from broken prod, panicked rollbacks, and staring at a at 2am wondering who approved that.
This post is what I wish someone had explained early on: how Terraform security checks actually work in real pipelines, what’s worth enforcing, and how to do it without turning security into a bureaucratic joke.
I’m not going to sell you tools. I’m not going to repeat generic “best practices.”
This is about where real incidents come from and how to stop them before apply.
Why “before apply” is where most Terraform security wins happen
Almost every serious infra incident I’ve seen had a moment before apply where it could’ve been caught cheaply.
Some real examples:
-
A security group widened to “temporarily” and forgotten.
-
An S3 bucket created without encryption because the module default was old.
-
An IAM policy copy-pasted with because “we’ll tighten it later.”
-
A production database recreated because
All of these showed up clearly in a Terraform plan.
None of them were fixed “after deploy” without pain.
Why “we’ll fix it later” fails in practice
In theory, you can remediate after apply.
In reality:
-
Ownership is unclear once the resource exists
-
Nobody wants to touch prod unless something is on fire
-
The original context (“this is temporary”) is gone
-
Fixing it later risks downtime, so it gets postponed
By the time security notices, the infrastructure is “working,” which makes it politically harder to change.
What “shift left” actually means
“Shift left” doesn’t mean:
-
Run 15 scanners on every commit
-
Block engineers on theoretical edge cases
-
Enforce perfect compliance in dev
It means:
Put the strongest, highest-signal checks at the point where change is cheapest and most visible.
That point is before apply, when Terraform is still just a plan.
What you should actually scan in Terraform (and why)
Terraform security isn’t one scan. It’s layers. And not all layers are equally valuable.
Here’s how I think about it in practice.
Static IaC scanning
This is scanning
What it’s good at
-
Obvious misconfigurations
-
Known bad patterns (public buckets, open security groups)
-
Missing encryption flags
-
Deprecated resource usage
What it misses
-
Values coming from variables
-
Conditional logic
-
Module internals you don’t control
-
What actually gets created after interpolation
Static scans are cheap and fast, but noisy.
They’re best as early feedback, not hard gates.
If your pipeline fails on static scans alone, you’ll get false positives and engineer fatigue fast.
Module and provider supply-chain risks
This one gets ignored until it bites you.
Problems I’ve seen:
-
Pulling unpinned modules from GitHub
-
Providers auto-upgrading and changing behavior
-
Forked modules drifting from upstream security fixes
What actually matters:
-
Pin module versions
-
Pin provider versions
-
Know which modules are allowed in prod
- You don’t need SBOM-level paranoia here.
- You do need to avoid “latest” in production infrastructure.
Secrets in Terraform and repo history
Terraform will happily:
-
Store secrets in state
-
Echo them in plans
-
Commit them to Git if you’re careless
Scanning for secrets matters, but be realistic:
-
Most leaks come from copy-paste, not Terraform magic
-
State files are a bigger risk than
.tffiles -
Git history lasts forever
Focus on:
-
Preventing secrets in
.tfand.tfvars -
Using references (KMS, Vault, SSM, etc.)
-
Locking down state storage and access
Don’t try to “scan your way” out of bad secret hygiene.
Plan-time scanning
If you only do one thing well, do this.
A Terraform plan shows:
-
Exact resources
-
Exact permissions
-
Exact network exposure
-
Exact destructive actions
This is where you catch:
-
instead of on critical resources
-
New public endpoints
-
IAM expansion
-
Accidental deletions
-
Cross-region surprises
Plan-based checks have far fewer false positives because they see resolved configuration.
In every mature setup I’ve seen, plan scanning is the real gate.
Drift and posture checks after apply
Post-apply checks won’t stop incidents, but they:
-
Catch manual changes
-
Detect config drift
-
Reveal resources created outside Terraform
-
Highlight long-lived exceptions
They’re about visibility, not prevention.
Useful, but not a replacement for pre-apply controls.
Policies that prevent real incidents
Most teams block the wrong things.
- If you try to block everything, engineers will route around you.
- If you block nothing important, security becomes theater.
Here’s what actually prevents incidents.
Network exposure
Why it exists
Public exposure is still the fastest way to get owned.
Real mistakes it prevents
-
admin ports
-
Load balancers created without auth
-
Databases placed in public subnets
What to block
-
World-open access to sensitive ports
-
Public databases
-
Public access without explicit allowlist
What to allow
-
Intentional public services with documented exposure
-
Edge cases with explicit review
If you only block one thing: block accidental public exposure.
IAM mistakes
Why it exists
IAM mistakes don’t break immediately they rot quietly until exploited.
Real mistakes it prevents
-
permissions
-
Wildcard resource access
-
Privilege escalation paths
What to block
-
Admin-level policies without justification
-
IAM changes that expand scope unexpectedly
What to allow
-
Gradual privilege expansion with visibility
-
Temporary elevation with expiry
Block expansion without intent, not every wildcard blindly.
Encryption defaults
Why it exists
Unencrypted resources are usually accidents, not decisions.
Real mistakes it prevents
-
Unencrypted disks
-
Unencrypted buckets
-
Forgotten KMS settings
What to block
-
New storage without encryption enabled
What to allow
-
Legacy resources under migration
-
Explicit opt-outs with approval
This one has high signal and low drama. Block it.
Logging and audit gaps
Why it exists
If something goes wrong, logs are the difference between a bad day and a nightmare.
Real mistakes it prevents
-
No access logs
-
No audit trails
-
“We don’t know who changed that”
What to block
-
Critical services without logging enabled
What to allow
-
Low-risk dev environments with warnings
Warn first, then enforce once teams adjust.
Backups and data durability
Why it exists
Data loss is still more common than people admit.
Real mistakes it prevents
-
No backups
-
Short retention defaults
What to block
-
Destructive changes without safeguards
-
No-backup production databases
If you want engineers to trust Terraform, protect them from footguns.
Baseline hygiene
These don’t stop hackers they stop chaos.
Tags and regions matter because:
-
Ownership becomes visible
-
Costs are traceable
-
Alerts reach the right people
Don’t block deploys on missing tags at first.
Warn, then enforce once the org adapts.
How to handle exceptions without destroying your security model
Exceptions are inevitable.
Pretending otherwise is how security programs die.
Why “just this once” always becomes forever
Because:
-
Nobody tracks it
-
Nobody owns it
-
Nobody revisits it
Six months later, it’s “legacy.”
What a real exception contract looks like
A real exception has:
-
Scope
(which resource, which policy)
-
Reason
(documented, not “blocked my deploy”)
-
Owner
(a human, not a team name)
-
Expiry
(non-negotiable)
If any of those are missing, it’s not an exception it’s a bypass.
Time-bound, scoped waivers in code
The best exceptions:
-
Live next to the Terraform code
-
Are machine-readable
-
Expire automatically
When the waiver expires, the pipeline fails again. No reminders needed.
Break-glass vs normal exceptions
Break-glass:
-
Short-lived
-
Logged
-
Reviewed after the fact
Normal exceptions:
-
Planned
-
Reviewed
-
Revisited
Mixing these up is how “temporary” becomes permanent.
How mature teams close the loop
Mature teams:
-
Review expired waivers
-
Remove obsolete ones
-
Turn common exceptions into better defaults
Exceptions are feedback if you listen.
A Terraform security pipeline that teams don’t hate
You don’t need a monster pipeline.
Minimal pipeline
-
Static scan
-
Terraform plan
This already catches more than nothing.
Plan-based policy gating
-
Terraform plan
-
Policy checks on the plan
-
Block high-risk issues only
This is the sweet spot for most teams.
Drift + posture + ownership alerts
-
Pre-apply checks
-
Post-apply drift detection
-
Ownership-based alerts
This is where platform teams earn trust.
Where most teams over-engineer too early
-
Blocking dev on everything
-
Too many scanners
-
Zero tolerance from day one
Security that stops delivery gets bypassed. Always.
Common mistakes I’ve seen teams make
Too many scanners
More tools ≠ more security.
It usually means duplicate findings and confusion.
Too many blocking rules
If everything is critical, nothing is.
No ownership or tagging
Findings without owners never get fixed.
No plan scanning
This one still amazes me.
You’re ignoring the most accurate data you have.
No expiry on waivers
Permanent exceptions are policy failures.
What not to attempt on day one
-
Perfect coverage
-
Zero false positives
-
Full compliance frameworks
Final opinionated takeaway
Terraform security isn’t about locking engineers down.
It’s about catching expensive mistakes while they’re still cheap.
If your checks feel painful, they’re probably in the wrong place.
You Might Be Interested In
- Cloud Egress Control: How To Stop Data Exfiltration Via Outbound Traffic
- Policy-as-code With Opa: A Practical Starter Kit
- Secrets Management Comparison: Env Vars Vs Kms Vs Vault When To Use What?
Conclusion
Terraform security isn’t about catching every possible misconfiguration or building a perfect policy set on day one. It’s about stopping the mistakes that actually hurt you, at the moment when they’re still easy to fix. In practice, that means focusing on plan-time visibility, blocking only high-impact risks, and accepting that some exceptions will always exist.
The teams that get this right don’t have the most scanners or the strictest rules. They have clear ownership, policies tied to real incidents, and guardrails that feel fair to the people using them. When security checks align with how engineers actually work, they stop being obstacles and start being quiet protection.
If your Terraform security feels painful or ineffective, it’s usually not because you need more tools. It’s because the checks are in the wrong place, enforcing the wrong things, or trying to solve problems that don’t happen in the real world. Get those right, and most of the hard problems disappear before ever runs.
FAQs about Terraform Security Checks: Catching Risky Infrastructure Before Apply
Static scan vs plan scan?
Static scanning looks at Terraform code before variables are resolved, modules are expanded, or conditionals are evaluated. It’s useful for fast feedback and catching obvious mistakes early, but it has a hard ceiling on accuracy. In real codebases with shared modules and heavy variable use, static scans often complain about things that will never exist at runtime, which is why teams stop trusting them.
Plan scanning works on the fully resolved infrastructure graph. It sees what Terraform is actually going to create, change, or destroy. That’s why it catches real risk: widened IAM policies, new public endpoints, destructive changes. In practice, static scans are for guidance and education, while plan scans are where enforcement belongs.
When should a policy block vs warn?
A policy should block when the outcome is likely to cause a real incident, not just violate a standard. Public network exposure, destructive operations on stateful resources, or massive IAM privilege expansion are good examples because they regularly lead to outages or security events. Blocking here saves time and stress later.
Warnings are better for hygiene and gradual improvement: tagging, logging coverage, legacy defaults, or best-effort controls. If you block everything from day one, engineers stop engaging with the signal and start working around the system. Warnings let teams adapt without turning security into friction.
How strict is too strict?
You’re too strict the moment engineers feel forced to bypass controls to get work done. That doesn’t mean they’re irresponsible it usually means the policy doesn’t match reality. Overly strict rules often assume perfect information, perfect modules, and perfect timelines, none of which exist in real teams.
Strictness should increase over time, based on observed behavior and actual incidents. Start with the highest-impact risks, then tighten as defaults improve and trust builds. Security that teams respect is more effective than security that technically blocks everything but gets ignored in practice.
How long should exceptions last?
Exceptions should be as short as realistically possible, not as long as convenient. In most cases that means days or weeks, not quarters. The goal isn’t to punish teams, it’s to force a revisit while the context is still fresh and the fix is still feasible.
If an exception needs to be renewed repeatedly, that’s a signal the policy or the platform default is wrong. Mature teams treat recurring exceptions as feedback and either improve the baseline or formalize a safer pattern. Long-lived exceptions without review are how risk quietly accumulates.
Is drift detection worth it?
Drift detection won’t stop incidents on its own, but it’s absolutely worth it for visibility and accountability. It surfaces manual changes, emergency fixes, and “temporary” tweaks that never made it back into Terraform. Without drift detection, your Terraform state slowly stops representing reality.
The key is not to treat drift as a failure by default. Drift is information. Sometimes it’s an incident response, sometimes it’s a workaround, sometimes it’s bad behavior. The value comes from knowing it happened, who owns it, and whether it needs to be reconciled back into code.

