Frontend Labs
⋯

Part 3 — Infrastructure as Code and the deployment pipeline

The pipeline that builds and ships your software has, by design, the credentials to change production. That makes it one of the highest-value targets you own. This Part secures the code that defines infrastructure, the CI/CD system that runs it, and the integrity of what comes out the other end.

Chapter 11 — IaC security

In one sentence

Infrastructure as Code makes every cloud misconfiguration a reviewable, testable, gate-able diff — but only if you scan the plan, protect the state, pin the modules, and detect drift.

Where we are

You know cloud misconfigurations (Part 2). IaC (Terraform, Pulumi, CloudFormation, CDK, Bicep) is how you prevent them at review time instead of finding them in production. It also introduces its own risks: the state file is a secrets store, modules are a supply chain, and reality drifts from code.

How it works

Scan the plan, not just the code.

   Static scan of .tf files          catches obvious issues but misses
                                     anything computed, from variables, or
                                     from modules.
   Scan the PLAN (terraform plan
     -out=tfplan; terraform show
     -json tfplan > plan.json)       sees the ACTUAL resource attributes
                                     that will be created -> far fewer
                                     false negatives. Do this in CI.

   Scanners:  Checkov, tfsec (now in Trivy), KICS, Terrascan, Snyk IaC,
              `trivy config`, cfn-nag / cfn-guard / cfn-lint (CFN),
              Regula. Run 2 (they have different rule coverage), fail the
              build on HIGH+ with a documented, time-boxed exception file.

Policy as code — encode YOUR rules, not just the scanner's defaults.

   OPA/Rego + conftest     policy tests against plan.json / k8s manifests.
   Sentinel                Terraform Cloud/Enterprise native policy.
   CloudFormation Guard    (cfn-guard) rules for CFN/CDK output.
   Checkov custom policies  Python/YAML.

   Rules worth writing:
     - no security group with 0.0.0.0/0 on 22/3389/5432/6379/9200/27017
     - every S3 bucket: BlockPublicAcls, encryption with a customer CMK,
       versioning
     - every RDS: storage_encrypted, no public access, deletion protection,
       backup retention >= N
     - IMDSv2 required on every launch template / instance
     - no IAM policy with Action "*" or "iam:*" on Resource "*"
     - no iam:PassRole without a resource scope + PassedToService condition
     - CloudTrail multi-region + log file validation on
     - all resources tagged owner/env/data-classification
     - only approved regions
   These become PR-blocking checks -> the misconfig never merges.

Protect the state.

   Terraform state (.tfstate) contains EVERY resource attribute IN
   PLAINTEXT -- including RDS master passwords, generated secrets, private
   keys, IAM policy documents. It is a credentials file.

   [ ] NEVER commit state to git. Add *.tfstate* to .gitignore; scan for
       it (gitleaks).
   [ ] Remote backend: S3 with SSE-KMS (customer CMK) + versioning +
       Block Public Access + a bucket policy restricting to the CI role
       and a break-glass role; DynamoDB table for state locking. Or
       Terraform Cloud/Enterprise / Spacelift / env0 with encryption and
       RBAC.
   [ ] Tight IAM on the state bucket -- reading it = reading every secret
       in the estate.
   [ ] `terraform output` marks sensitive values, but `terraform show` /
       `state pull` still expose them -- treat those commands as
       privileged.
   [ ] Prefer generating secrets OUTSIDE Terraform (in Vault / Secrets
       Manager with a rotation Lambda) so they never enter state at all.

Pin the supply chain.

   [ ] Provider lockfile (.terraform.lock.hcl) committed, with checksums
       for all platforms your CI + devs use (`terraform providers lock
       -platform=...`).
   [ ] Module sources pinned: a version constraint for registry modules;
       a commit SHA (not a branch) for `git::` sources. No `ref=main`.
   [ ] Prefer a PRIVATE module registry with reviewed, versioned modules
       (the "paved road" for infra -- Ch 13).
   [ ] Vet third-party modules like any dependency (G1 Ch 47): what
       providers do they configure, what data sources do they read, do
       they have `local-exec` provisioners running scripts?
   [ ] `local-exec` / `remote-exec` provisioners run arbitrary commands on
       the CI runner -- treat a module that uses them as code execution.

Detect drift.

   Reality diverges from code: a click-ops hotfix, an auto-scaling change,
   an attacker's backdoor (a new IAM policy, an opened SG, a new admission
   webhook).
   [ ] Scheduled `terraform plan` (detects drift as a non-empty plan) with
       an alert on unexpected changes.
   [ ] AWS Config rules + conformance packs; CloudFormation drift
       detection; GCP Config Validator; Azure Policy compliance.
   [ ] Continuous cloud posture scan (Prowler/ScoutSuite/Steampipe,
       cloud-native Security Hub/SCC/Defender) -- the backstop for
       whatever IaC doesn't manage.
   [ ] Alert specifically on security-relevant drift: SG opened, policy
       broadened, logging disabled, encryption removed, public access
       enabled.

Worked example

A conftest policy that blocks a real mistake in review.

   # policy/no_public_ssh.rego
   package main
   deny[msg] {
     rc := input.resource_changes[_]
     rc.type == "aws_security_group_rule"
     v := rc.change.after
     v.type == "ingress"
     v.from_port <= 22
     v.to_port   >= 22
     cidr := v.cidr_blocks[_]
     cidr == "0.0.0.0/0"
     msg := sprintf("SG rule %s opens SSH to the world", [rc.address])
   }

   # in CI:
   terraform plan -out=tfplan
   terraform show -json tfplan > plan.json
   conftest test --policy policy/ plan.json     # exit 1 -> PR blocked

   A developer adds `cidr_blocks = ["0.0.0.0/0"]` to a bastion SG "just
   for testing". The PR check fails with the message above. They add their
   office CIDR instead, or route via SSM Session Manager (no SG needed).
   The misconfiguration never reaches an account.

Practice (60 min)

  1. Take a Terraform config (yours, or TerraGoat / CfnGoat). Run checkov and trivy config against the source; then run against plan.json. Compare findings — note what only the plan scan caught.
  2. Write three conftest/OPA policies encoding rules your org cares about (no public admin ports, mandatory encryption with a customer CMK, no iam:PassRole with Resource "*"). Make them fail on a bad plan, pass on a fixed one.
  3. Move a local state file to an S3 + DynamoDB backend with SSE-KMS and a restrictive bucket policy. Then terraform state pull | grep -i password — see for yourself what's in there. Rotate anything real.
  4. Introduce drift by hand (open an SG in the console). Run terraform plan and a Prowler scan — confirm both surface it.
  5. Add all of the above (plan scan + conftest + drift check) as CI jobs.

Common mistakes

  • Scanning .tf files only. Computed values, module outputs, and variable-driven config are invisible until you scan the plan.
  • State in git, or in an unencrypted/over-shared bucket. It's a plaintext dump of every secret in the estate.
  • Secrets generated inside Terraform. They land in state. Generate them in a secrets manager instead.
  • Modules pinned to a branch. ref=main is a mutable dependency — the same problem as @v3 for GitHub Actions (G1 Ch 47).
  • No drift detection. IaC that isn't reconciled is documentation, and an attacker's changes hide in the gap.
  • local-exec provisioners treated as config. They're arbitrary command execution on your CI runner.
  • Exceptions with no expiry. A # checkov:skip with no ticket and no date is permanent debt.

Check yourself

  1. Why scan the Terraform plan rather than the .tf source?
  2. What sensitive material is in a Terraform state file, and where should state live?
  3. How do you pin the IaC supply chain (providers and modules)?
  4. What is configuration drift and name two ways to detect it.
  5. Why is a module that uses local-exec a code-execution concern?
  6. Give three policy-as-code rules you'd make PR-blocking.

(Answers: Appendix G.)

Further reading

  • Tools: Checkov, tfsec/Trivy, KICS, Terrascan, conftest/OPA, CloudFormation Guard, Sentinel docs; terraform-compliance (BDD-style).
  • Guidance: "Terraform state best practices" (HashiCorp); the CIS benchmarks the scanners implement; OWASP IaC Security guidance.
  • Practice: Bridgecrew TerraGoat / CfnGoat / CDKGoat; cloud-custodian for policy-driven remediation.
  • Talk: "Policy as Code" talks from HashiConf / KubeCon; the OPA/Rego playground and "Styra Academy".

Generated from real-life-security-guide-v1.md · 8 Oct 2026 16:36