{
 "slug": "infrastructure-as-code-iac-poisoning-gating-terraform-pulumi",
 "topic_id": "TOPIC-040",
 "cluster": "CI/CD & Software Supply Chain",
 "tier": "Tier B",
 "title": "The CI runner that can rewrite your entire cloud estate",
 "summary": "Infrastructure pipelines hold the broadest credentials in most organisations, apply changes automatically, and have no step where a human commits to the specific change being made.",
 "lede": "A pull request modifies a module. The runner produces a plan, applies it, and a storage bucket holding customer data becomes publicly readable. Every step executed exactly as configured.",
 "date": "2024-04-30",
 "category": "Developer",
 "author_id": "whit-calloway",
 "tags": [
  "Terraform",
  "infrastructure as code",
  "CI/CD",
  "cloud security",
  "plan approval",
  "Pulumi"
 ],
 "image_title": "Infrastructure Pipeline Broad Credentials",
 "schema": "Article",
 "key_takeaways": [
  "The infrastructure pipeline typically holds wider cloud privilege than any human, because it must create and destroy everything.",
  "A plan is reviewable and usually unreviewed — it is long, and nothing binds an approval to the specific plan that applies.",
  "Signing a rendered summary of the plan, then verifying the digest at apply time, closes both the review and the drift problem."
 ],
 "body": [
  {
   "type": "h2",
   "text": "The privilege concentration"
  },
  {
   "type": "diagram",
   "kind": "flow",
   "alt": "Binding the approval to the plan file closes the window between review and apply",
   "caption": "A re-plan between approval and apply produces a different hash, and the apply refuses.",
   "nodes": [
    {
     "label": "terraform plan -out=tfplan",
     "note": "plan file pinned"
    },
    {
     "label": "Render the security-relevant changes",
     "note": "not the whole diff",
     "good": true
    },
    {
     "label": "Sign the summary digest + plan hash",
     "note": "one gesture",
     "good": true
    },
    {
     "label": "Apply verifies both",
     "note": "mismatch → refuse",
     "good": true
    }
   ]
  },
  {
   "type": "p",
   "html": "An infrastructure pipeline must be able to create, modify and destroy every resource type it manages. In a mature estate that means broad administrative authority across networking, identity, storage and compute."
  },
  {
   "type": "p",
   "html": "Individual engineers usually hold narrower permissions than the pipeline does. The pipeline is the most privileged principal in the account and it is driven by repository events."
  },
  {
   "type": "table",
   "head": [
    "Trigger",
    "Who controls it",
    "Typical review"
   ],
   "rows": [
    [
     "Merge to main",
     "Repository write access",
     "Pull request review"
    ],
    [
     "Tag push",
     "Anyone who can push tags",
     "Often none"
    ],
    [
     "Scheduled apply",
     "The schedule",
     "None"
    ],
    [
     "Manual dispatch",
     "Anyone with workflow dispatch",
     "None"
    ],
    [
     "Upstream module update",
     "The module's publisher",
     "Depends on version pinning"
    ]
   ]
  },
  {
   "type": "p",
   "html": "The last row is the supply chain dimension. A module sourced from a registry and referenced loosely can change what your plan does without any change in your repository."
  },
  {
   "type": "h2",
   "text": "Why plan review does not happen"
  },
  {
   "type": "p",
   "html": "Plans are long. A non-trivial change produces hundreds of lines of diff across resources that mostly did not change in interesting ways."
  },
  {
   "type": "p",
   "html": "The security-relevant lines are a handful buried in that output: a policy document, an ingress rule, a public access block, an encryption setting. A reviewer scrolling a CI log reliably misses them."
  },
  {
   "type": "p",
   "html": "The second problem is that even a careful review is not binding. The plan reviewed and the plan applied are separate executions, and state can change between them."
  },
  {
   "type": "h2",
   "text": "Rendering the plan for a decision"
  },
  {
   "type": "p",
   "html": "The approval prompt should not be the plan. It should be the facts that change the answer."
  },
  {
   "type": "code",
   "text": "  Production apply — authorisation required\n\n  Account:  prod (4471-8823-9910)   Region: eu-west-1\n  Plan:     +4  ~11  -2\n\n  Security-relevant changes:\n    ~ aws_s3_bucket_public_access_block.customer_data\n        block_public_acls:  true → false\n        block_public_policy: true → false\n    ~ aws_security_group.api\n        + ingress 0.0.0.0/0 : 5432\n    - aws_cloudtrail.audit          (destroy)\n\n  Destroys:  2 resources, 1 stateful\n  Plan hash: 3f91c8e2b7d40568\n\n  Touch your security key to authorise this plan."
  },
  {
   "type": "p",
   "html": "The classification is mechanical — a fixed list of resource types and attributes that affect exposure, encryption, logging and identity. It is perhaps two hundred lines of matching rules and it turns an unread log into a readable decision."
  },
  {
   "type": "h2",
   "text": "Binding approval to the plan that applies"
  },
  {
   "type": "code",
   "text": "# Plan phase\nterraform plan -out=tfplan\nplan_hash=$(sha256sum tfplan | cut -d' ' -f1)\nrender_summary tfplan > summary.json\nrequest_signature --digest $(canonical_digest summary.json) \\\n                  --plan-hash \"$plan_hash\"\n\n# Apply phase — same plan file, verified\nverify_receipt receipt.json --issuer-jwks \"$JWKS\" || exit 1\n[ \"$(sha256sum tfplan | cut -d' ' -f1)\" = \"$(jq -r .plan_hash receipt.json)\" ] || {\n  echo \"plan changed since approval\"; exit 1; }\nterraform apply tfplan"
  },
  {
   "type": "p",
   "html": "A re-plan between approval and apply produces a different hash and the apply refuses. This closes the window where infrastructure state or a module version changed after someone said yes."
  },
  {
   "type": "h2",
   "text": "Scoping so this stays usable"
  },
  {
   "type": "p",
   "html": "Gating every apply would be unbearable; most applies are tag changes and instance resizes. Two dimensions narrow it sensibly."
  },
  {
   "type": "ul",
   "items": [
    "<strong style=\"font-weight:600\">Environment:</strong> production and anything holding customer data. Development and ephemeral environments apply freely.",
    "<strong style=\"font-weight:600\">Change class:</strong> only when the plan touches identity, network exposure, encryption, logging, or destroys stateful resources."
   ]
  },
  {
   "type": "p",
   "html": "In a typical estate that combination fires on a small fraction of applies — which is what makes the prompt something an engineer reads rather than dismisses."
  },
  {
   "type": "h2",
   "text": "What remains uncovered"
  },
  {
   "type": "p",
   "html": "Changes made outside the pipeline. Console access, direct API calls and emergency manual fixes bypass this entirely, and every organisation has them."
  },
  {
   "type": "p",
   "html": "Drift detection catches the resulting divergence after the fact. Closing the path properly means gating the destructive cloud operations directly, which is the same control applied one layer down."
  },
  {
   "type": "h2",
   "text": "Why plan review does not happen"
  },
  {
   "type": "p",
   "html": "Plans are long. A non-trivial change produces hundreds of lines across resources that mostly did not change in interesting ways, and the security-relevant lines are a handful buried in that output: a policy document, an ingress rule, a public access block, an encryption setting. A reviewer scrolling a CI log reliably misses them."
  },
  {
   "type": "table",
   "caption": "What triggers an apply, and what reviews it",
   "head": [
    "Trigger",
    "Typical review"
   ],
   "rows": [
    [
     "Merge to main",
     "Pull request review"
    ],
    [
     "Tag push",
     "Often none"
    ],
    [
     "Scheduled apply",
     "None"
    ],
    [
     "Manual dispatch",
     "None"
    ],
    [
     "Upstream module update",
     "Depends entirely on version pinning"
    ]
   ]
  },
  {
   "type": "p",
   "html": "The last row is the supply chain dimension: a module referenced loosely can change what your plan does without any change in your repository."
  },
  {
   "type": "h2",
   "text": "Objections and honest limits"
  },
  {
   "type": "p",
   "html": "<strong style=\"font-weight:600\">“We review the plan in the pull request.”</strong> Even a careful review is not binding, because the plan reviewed and the plan applied are separate executions unless the plan file is pinned and its hash verified."
  },
  {
   "type": "p",
   "html": "<strong style=\"font-weight:600\">“Every apply would need a signature.”</strong> It should not. Gate production and data-holding environments, and only when the plan touches identity, exposure, encryption, logging or destroys stateful resources. That fires on a small fraction of applies."
  }
 ],
 "faq": [
  {
   "q": "Why not just review the plan in the pull request?",
   "a": "Plans are long and the security-relevant lines are few. More importantly, the plan reviewed and the plan applied are separate executions unless the plan file is pinned and verified."
  },
  {
   "q": "Should every apply require a signature?",
   "a": "No. Gate production and data-holding environments, and only when the plan touches identity, exposure, encryption, logging or destroys stateful resources."
  },
  {
   "q": "What is the plan hash for?",
   "a": "Binding approval to the exact plan file. A re-plan between approval and apply produces a different hash, and the apply refuses."
  },
  {
   "q": "What about changes made in the console?",
   "a": "Uncovered. Drift detection finds them afterwards; closing the path means gating the destructive cloud operations directly."
  }
 ],
 "sources": [
  {
   "t": "Terraform — the plan command",
   "u": "https://developer.hashicorp.com/terraform/cli/commands/plan"
  },
  {
   "t": "Cloud provider guidance on privileged automation identities."
  },
  {
   "t": "CIS Benchmarks for cloud configuration — public access and logging controls."
  },
  {
   "t": "CISA — known exploited vulnerabilities and incident reporting",
   "u": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog"
  },
  {
   "t": "CIS Benchmarks — cloud configuration",
   "u": "https://www.cisecurity.org/cis-benchmarks"
  }
 ],
 "related": [
  {
   "slug": "kubernetes-production-cutover-gating-adding-cryptographic-human-verifi",
   "title": "GitOps cutover gating",
   "category": "Developer"
  },
  {
   "slug": "sigstore-vs-manav-signing-artifacts-prove-approved-deployment",
   "title": "Signing artifacts does not prove who approved",
   "category": "Comparison"
  },
  {
   "slug": "illusion-zero-trust-network-microsegmentation-fails-against",
   "title": "The illusion of Zero Trust",
   "category": "Developer"
  }
 ],
 "image": "https://cdn.twc.sh/images/igcache/Infrastructure%20Pipeline%20Broad%20Credentials/1500_900/blog.jpg",
 "wordcount": 890,
 "url": "/blog/infrastructure-as-code-iac-poisoning-gating-terraform-pulumi.html",
 "reading_time": "4 min read",
 "meta_description": "Infrastructure pipelines hold the broadest cloud credentials, apply changes automatically, and have no step where a human commits to the change.",
 "hub": {
  "slug": "topics/software-supply-chain",
  "title": "Software supply chain authorization"
 },
 "answer": "Because it must be able to create, modify and destroy every resource type it manages. In a mature estate that is broad administrative authority across networking, identity, storage and compute — wider than any individual engineer holds — and it is driven by repository events.",
 "answer_q": "Why is the Terraform runner the most privileged principal in your cloud account?",
 "entities": [
  {
   "name": "Terraform",
   "type": "SoftwareApplication",
   "url": "https://developer.hashicorp.com/terraform/cli/commands/plan",
   "primary": true
  },
  {
   "name": "Pulumi",
   "type": "SoftwareApplication",
   "url": "https://www.pulumi.com/"
  }
 ],
 "glossary": [
  {
   "term": "Plan file",
   "def": "The saved output of a plan, which an apply can consume directly. Pinning it is what makes an approval binding."
  },
  {
   "term": "Drift",
   "def": "Divergence between declared infrastructure and actual state, usually caused by changes made outside the pipeline."
  },
  {
   "term": "Module pinning",
   "def": "Referencing a module by an immutable version so its contents cannot change between plans."
  }
 ],
 "checklist": {
  "title": "Making plans reviewable",
  "id": "plans",
  "desc": "Five steps.",
  "steps": [
   {
    "name": "Classify the plan mechanically.",
    "text": "A fixed list of resource types and attributes affecting exposure, encryption, logging and identity. About two hundred lines of matching rules."
   },
   {
    "name": "Render the classified subset, not the diff.",
    "text": "Plus counts of creates, changes and destroys, and whether any destroyed resource is stateful."
   },
   {
    "name": "Pin the plan file and hash it.",
    "text": "Sign the summary digest together with the plan hash."
   },
   {
    "name": "Verify both at apply.",
    "text": "Refuse if either the receipt fails or the plan file changed."
   },
   {
    "name": "Gate destructive cloud operations directly too.",
    "text": "Console access and manual fixes bypass the pipeline entirely."
   }
  ]
 },
 "cta": {
  "title": "Where this fits in Manav",
  "html": "Manav binds the signature to both the rendered security summary and the plan file hash. A re-plan between approval and apply changes the hash, and the apply stops rather than proceeding under an approval that covered something else.",
  "href": "../docs.html",
  "label": "See plan gating"
 }
}