100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
DevSecOps & Security Automation
65 minadvanced

Practice — write OPA policies to block insecure Terraform

This exercise applies Module 3 end to end. You will write a small set of Rego policies that scan a Terraform plan and block three common, high-impact misconfigurations: a publicly accessible storage bucket, an unencrypted database, and a security group rule opening a sensitive port to the entire internet. You will then wire the policy bundle into a pipeline gate exactly as Lesson 14 described, and prove it blocks each planted flaw.

Analogy🏏Cricket
💪 Think of it like fitness: a proper strength test doesn't just ask whether you feel fit — it puts you through three distinct, measurable checks, say a squat, a press, and a sprint, and you only pass when each one independently clears its bar. Feeling strong proves nothing until every movement is verified on its own. This exercise builds that kind of test for infrastructure: three Rego rules that each block one concrete failure — a public bucket, an unencrypted database, and a port open to the internet — wired into a gate that proves it stops each planted flaw. This reveals why real assurance comes from independently verifying every specific weakness.

Step 1 — Produce a plan to test against. Export a Terraform plan as JSON so Rego can traverse its structure. Include, deliberately, one bucket resource without a private access control, one database resource without encryption enabled, and one security group rule with a 0.0.0.0/0 source on a sensitive port, so your policies have concrete violations to catch during testing.

Analogy🏏Cricket
♟️ Think of it like chess: before trusting a new tactic in a real game, you set up a training board with the exact threats you want to handle already in place — a pinned knight here, an exposed king there — so you can prove the tactic answers each one. A test position with no dangers proves nothing. Exporting a Terraform plan as JSON that deliberately contains a public bucket, an unencrypted database, and a wide-open security group rule builds precisely that training board: known, planted violations your policies must be shown to catch. This reveals why you construct the failing case on purpose before you rely on the rule that should stop it.
bash
# Export the plan as JSON for policy evaluation
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json

Step 2 — Write the storage and database rules. Create a package that inspects every planned storage resource for a public access setting and every planned database resource for a missing encryption flag, emitting a clear deny message naming the offending resource address for each violation, so a developer can locate and fix the exact block without needing to read the Rego source.

Analogy🏏Cricket
🍳 Think of it like cooking: a health inspector doesn't just declare a kitchen 'unsafe' — they check every fridge for a safe temperature and every surface for cleanliness, then leave a slip naming the exact appliance at fault so the chef can fix that one thing without decoding the regulations. A vague verdict helps no one. Rego rules that inspect every planned storage resource for public access and every database for missing encryption, each emitting a deny message that names the offending resource address, work the same way. This reveals why a precise, resource-naming failure message is what lets a developer fix the exact block without reading the policy source.
rego
package terraform.storage

deny[msg] {
  r := input.resource_changes[_]
  r.type == "aws_s3_bucket_public_access_block"
  r.change.after.block_public_acls == false
  msg := sprintf("bucket policy '%s' does not block public ACLs", [r.address])
}

package terraform.data_protection

deny[msg] {
  r := input.resource_changes[_]
  r.type == "aws_db_instance"
  r.change.after.storage_encrypted == false
  msg := sprintf("database '%s' is not encrypted at rest", [r.address])
}

Step 3 — Write the network rule and run the gate. Add the open-port rule from Lesson 14's network package, then run Conftest against your test plan and confirm all three deny messages appear, each naming the correct resource. Fix each planted flaw one at a time, re-running the gate after every fix, and confirm the corresponding message disappears while the others still fire, proving each rule is independent and accurate.

Analogy🏏Cricket
💼 Think of it like business: a thorough audit doesn't hand back one lump 'fail' — it lists each finding separately, and as management fixes them one at a time, the auditor re-runs the review and watches exactly that item clear while every other open finding still stands. That independence is how you prove each control genuinely works on its own. Adding the network rule, running Conftest, and confirming all three deny messages fire — then fixing each planted flaw and re-running to see only its message vanish — follows the same method. This reveals why resolving findings one at a time is how you verify every rule is accurate and independent.
rego
package terraform.network

deny[msg] {
  r := input.resource_changes[_]
  r.type == "aws_security_group_rule"
  r.change.after.cidr_blocks[_] == "0.0.0.0/0"
  r.change.after.from_port <= 22
  r.change.after.to_port >= 22
  msg := sprintf("SG rule '%s' opens SSH to the internet", [r.address])
}

# Run the full bundle:
# conftest test plan.json --policy ./policies
#   -> 3 failures expected on the deliberately insecure plan

Step 4 — Wire it into a pipeline and roll out safely. Add the Conftest command as a required step on any pull request that changes Terraform, following the warn-then-block rollout Lesson 14 recommended: run the gate in report-only mode first against real, existing infrastructure code to catch any unexpected false positives, then switch it to blocking once the team trusts the results and any legitimate exceptions have a reviewed, time-boxed suppression.

Analogy🏏Cricket
🏏 Think of it like cricket: a new bowling action isn't unleashed in a title decider the day it's invented — it's first tried in the nets, then in a low-stakes warm-up where a no-ball costs nothing, and only once it's proven clean and consistent does it come out in a match that actually counts. Rushing it straight into the final risks conceding the game to an untested flaw. Rolling a Conftest gate out in report-only mode against real infrastructure first, then switching it to blocking once the team trusts the results, follows exactly this graded progression. This reveals why you observe a new control in a safe setting before letting it decide the outcome.
  • Build a test plan with deliberate storage, database, and network violations.
  • Write focused Rego rules per domain, each with a clear, resource-naming deny message.
  • Run Conftest against the test plan and confirm each violation is caught independently.
  • Fix flaws one at a time to verify each rule fires and clears correctly on its own.
  • Roll the gate out in warn-only mode first, then switch to blocking once trusted.
Lesson 18 of 35
0% complete