skip to content

IaC Policy Gates

You will learn to gate infrastructure changes before apply: evaluating a Terraform plan with Sentinel or conftest, writing custom Checkov checks, and deciding which severities block a PR versus warn. Interviewers ask how you would stop a public S3 bucket at review time, and this is the answer they expect.

on this pageshow

explore

questions

page 1 of 2

In a Terraform plan JSON, which section shows that one resource references another?

level: juniorimportance: must knowfreq 58%

answer

  1. values and links live apart
  2. expressions, not attributes
  3. look in the configuration section
  4. references carry addresses, ids do not
  5. then join by address

basics

~10 s

The configuration section. Each resource there carries an expressions object whose references list the addresses it points at. The planned_values section holds only each resource's resolved attribute values and records no links between resources.

solid answer

~40 s

Relationships live in `configuration`, not in `planned_values`. Under `configuration.root_module.resources`, every resource has an `expressions` object with one entry per argument, and an argument written as a reference appears as `{"references": ["aws_security_group.web.id", "aws_security_group.web"]}` — resource addresses, not values. `planned_values` gives you each resource's post-apply attributes keyed by address, and nothing else; the instance's `vpc_security_group_ids` there would be provider ids like `sg-0ab…`, which are opaque strings you cannot look up in the document, and for a group this run is creating there is no id printed at all. So any rule whose subject is "the security group this instance attaches to" is a two-step job: read the reference out of `configuration`, then join by address back into `planned_values` to read the target's attributes.

go deeper

for a junior

Be ready to name the two sections and say what each is for: configuration holds expressions and references, planned_values holds resolved attribute values per resource address. Knowing that links and values live apart is the whole point here.

for a middle

Explain why a provider id in planned_values is useless as a join key, and show the three-step shape of a cross-resource rule: find the subject, read the reference, join by address. Mention that references may point at variables or module outputs, not only resources.

for a senior

Demonstrate that you know what a green result means when the reference is a literal id or the target lives outside this configuration. An interviewer wants to hear you distinguish 'no violation found' from 'the rule could not evaluate its subject'.

for a principal

Own the consequence for a control catalogue: which controls are expressible against a single plan at all, and how you keep the gate's stated coverage honest when some of them are not. The framing to defend is that the artifact's shape, not your intent, sets the limit.

## The plan is several views, not one picture A Terraform plan rendered as JSON is not a single model of your infrastructure. It is a set of parallel views of the same run, and each answers a different question. Two of them matter for cross-resource rules: - **`planned_values`** — what each resource will look like after apply. Under `planned_values.root_module.resources` you get one entry per resource instance, each with an `address` (`aws_instance.web[0]`), a `type`, a `name`, and a `values` object holding the resolved attributes. - **`configuration`** — your code, normalised into JSON. Under `configuration.root_module.resources` you get one entry per resource *block* (not per instance), each with an unindexed `address`, a `mode` (`managed` or `data`), and an `expressions` object with one entry per argument. The distinction that decides what rules you can write: **`planned_values` records values; `configuration` records links.** ## Why the values view cannot answer a relationship question Take a real control: *an instance tagged `tier = data` must not attach a security group that allows egress to `0.0.0.0/0`.* Two resources, and the violation exists only in the pair. From `planned_values` you can read the instance's `tags`, and you can separately read each security group's `egress` blocks. What you cannot read is **which group this instance attaches to**. The instance's `vpc_security_group_ids` attribute holds provider identifiers — opaque strings like `sg-0ab12…` — and an identifier is not an address. There is no index in the document that maps `sg-0ab12…` back to `aws_security_group.web`, so you have nothing to look up. And when the same run is creating that group, the identifier does not exist yet at all, so the attribute carries no usable value either. Written only against `planned_values`, the rule collapses into one of two useless shapes: flag *every* group with open egress (which fires on the NAT and load-balancer groups nobody objects to), or flag *every* data-tier instance (which tells you nothing about its network exposure). ## The link is an expression, and expressions are in `configuration` In the configuration view the same argument looks like this: ``` "expressions": { "vpc_security_group_ids": { "references": ["aws_security_group.web.id", "aws_security_group.web"] } } ``` An expression entry is one of two things. Either a **`constant_value`**, when the argument was written as a literal, or a **`references`** array, when it was written as a reference to something else. The array typically carries both the full traversal (`aws_security_group.web.id`) and the bare resource address (`aws_security_group.web`), because both are legitimate ways to name what was traversed. The bare address is the one you want: it is exactly the key you will match on in `planned_values`. References are not always to resources. `var.subnet_id`, `local.sg`, `module.network.security_group_id` and `data.aws_ami.base.id` all appear in the same array shape, so a rule that assumes every reference names a managed resource in this module will trip over the first indirection. ## The shape of every cross-resource rule Once you know where the two halves live, the pattern is always the same three steps: 1. Find the subject in `configuration` — the resource whose argument creates the relationship. 2. Pull the target's address out of that argument's `references`. 3. Join by address into `planned_values` and read the target's attributes. Then assert over the pair. The join key is a string address, and getting that string right — indices, module prefixes — is where the work actually is. ## The case with no reference at all If someone wrote `vpc_security_group_ids = ["sg-0ab12…"]`, the expression is a `constant_value` and there is no `references` entry to follow. The group is a real thing in the account, it is doing real damage, and it is simply not in this document — not as a managed resource, not as anything. A rule that starts by reading `references` finds nothing, produces no violation, and reports green. That is the more general limit of a plan gate: the document describes **one root configuration's run**, so a subject that lives in another configuration, or was created by hand, is not there to be judged.

  • What exactly does a references array contain for one argument?
    Usually both the full traversal that was written and the bare resource address it started from — for example `aws_security_group.web.id` alongside `aws_security_group.web`. Strip to the bare address before joining, since that is the form `planned_values` uses. References can also name non-resources: `var.sg_id`, `local.groups`, `module.network.sg_id`, or a `data.` address, so check what kind of thing you landed on before assuming you have a resource.
  • What if the attribute was written as a hard-coded id string instead of a reference?
    Then the configuration entry is a `constant_value` and there is no `references` array at all. The target resource is not declared in this configuration, so there is nothing in the document to join to and nothing to evaluate. A rule that only walks references will silently produce no violation — which looks identical to a pass. Detect that case explicitly and report it as unevaluable rather than letting it fall through.
  • Does the configuration section also tell you the attribute's final value?
    Only when it was a literal, as a `constant_value`. Anything computed, defaulted by the provider, or derived from a variable or another resource is resolved into `planned_values`, not into `configuration`. So the two sections are complementary rather than redundant: read links from one and values from the other.

planned_values is a phone book: every entry's details, no calls. configuration is the call log: who dialled whom, no details. A question about a conversation needs both, joined by name.

saying these in an interview costs you the question

  • Assumes planned_values records links between resources
  • Treats a provider id as a joinable resource address
  • Expects final attribute values in the configuration section
  • Believes the plan contains every resource in the account
  • Writes the rule against one resource and calls the pair covered

context

open as a page

In a Terraform plan JSON, where do resources declared inside a module appear to a policy rule?

level: juniorimportance: must knowfreq 66%

basics

~20 s

The plan is flattened. Every resource a module declares is expanded into the plan's resource lists under a module-qualified address such as module.network.aws_s3_bucket.flow_logs, so ordinary resource rules fire on module-created resources without knowing modules exist.

open as a page

Why must a Helm chart be rendered before conftest can evaluate its Kubernetes manifests?

level: juniorimportance: must knowfreq 64%

basics

~10 s

A chart's templates are Go template text, not valid YAML, and fields hidden behind conditionals only appear once values are applied. Render the chart first, then evaluate the manifests it produces.

open as a page

In a Terraform plan JSON, how do `planned_values` and `resource_changes` differ for a policy rule?

level: juniorimportance: must knowfreq 72%

basics

~20 s

planned_values is the whole post-apply state, every managed resource including untouched ones. resource_changes is the per-resource diff, carrying the action and the before and after values. A rule over planned_values judges the estate; a rule over resource_changes judges this run.

open as a page

In a Terraform plan, what does an attribute marked "known after apply" mean for a policy rule?

level: juniorimportance: must knowfreq 68%

basics

~20 s

It means the value does not exist yet: the provider or cloud only produces it when the resource is actually created, so the plan holds a placeholder. A policy rule evaluating that attribute has nothing real to test.

open as a page

In a Sentinel policy for HCP Terraform, what do the tfplan, tfstate and tfconfig imports each give you?

level: juniorimportance: must knowfreq 68%

basics

~20 s

tfplan gives the changes this run proposes, tfstate gives the resources the workspace already has, and tfconfig gives the configuration as written. They are three separate documents, so a rule reads whichever one answers its question.

open as a page

Why does a policy rule run over recorded infrastructure state catch violations a plan-time rule never will?

level: juniorimportance: must knowfreq 72%

basics

~20 s

A plan only contains the resources a change touches, so anything nobody is editing never appears in it. Running the same rule over recorded state or a cloud inventory judges every resource that exists, changed or not.

open as a page

A Terraform plan rule matching only the `create` action let a database retention downgrade ship. Why?

level: middleimportance: must knowfreq 62%

basics

~20 s

Lowering a backup window on an existing database is planned as an in-place edit, so change.actions is ["update"], not ["create"]. The filter never matched, so the rule never ran. Match when the actions array contains create or update.

open as a page

Why does a rule denying deprecated TLS policies quietly pass a listener whose value is unknown until apply?

level: middleimportance: must knowfreq 52%

basics

~20 s

A value that does not exist matches nothing. The rule hunts for a forbidden TLS policy, finds no value at all under that attribute, and reports no violation — a pass that looks identical in CI to a genuine one.

open as a page

Why does one conftest rule banning plaintext secret env values need per-format logic?

level: middleimportance: must knowfreq 50%

basics

~20 s

Because conftest policies read the parsed document, not the file text, and each parser produces a different shape. A Dockerfile becomes an array of instruction objects, a Kubernetes container env is a list of name/value objects, and a pipeline env is a plain map - three different paths for one logical field.

open as a page

A Helm chart passes conftest on your laptop but the CI policy gate blocks the same commit - how do you diagnose it?

level: seniorimportance: must knowfreq 57%

basics

~20 s

The two runs evaluated different documents. Compare the rendered output, not the chart: the values files, set flags, release name and subchart versions used by each render decide whether the fields the rule wants exist at all.

open as a page

A pull request adds an inline Checkov skip comment whose reason you cannot verify — how do you review it?

level: seniorimportance: must knowfreq 52%

basics

~20 s

Review the skip as the change, not the comment: decide whether the finding is real or a rule bug, insist on the narrowest form, require a reason checkable in the same diff, and remember an annotation has no expiry and no approver.

open as a page

What file formats can conftest test, and how does a CI job learn it failed?

level: juniorimportance: should knowfreq 58%

basics

~20 s

conftest parses structured configuration - YAML, JSON, HCL, Dockerfiles, TOML, INI and more - into one JSON-like document and evaluates policies against it. Failures print to the console and the process exits non-zero, which is the signal CI acts on.

open as a page

In Checkov, what are the two ways to write a custom check, and when do you need the Python form?

level: juniorimportance: should knowfreq 48%

basics

~20 s

Checkov custom checks come in two forms: a declarative YAML policy made of attribute or connection conditions, and a Python class implementing a scan method. YAML covers straightforward attribute comparisons; Python is for logic YAML operators cannot express.

open as a page

In Checkov, what does a #checkov:skip comment inside a Terraform resource block do?

level: juniorimportance: should knowfreq 58%

basics

~20 s

A checkov:skip comment suppresses one named check for the single block it sits inside. The form is checkov:skip=CHECK_ID:reason; the reason is free text nothing validates, and the finding is reported as skipped rather than failing the run.

open as a page

In Terraform plan JSON, how do you join a configuration reference back to its planned resource?

level: middleimportance: should knowfreq 50%

basics

~20 s

Strip the reference down to its bare resource address, then look that address up among the planned resources. The two sides do not match literally: configuration addresses carry no count or for_each index, and module resources are nested differently in each section.

open as a page

How would you gate a Terraform plan so every module call uses an approved source at a pinned version?

level: middleimportance: should knowfreq 55%

basics

~20 s

Read the plan's configuration section rather than its expanded resources: walk each entry under the root module's module_calls, match the source string against an allowlist the platform team owns, and require an exact version rather than a floating range or a mutable reference.

open as a page

In a CloudFormation template scanned by cfn-guard, what does a rule see when a property is set by !Ref or !If?

level: middleimportance: should knowfreq 46%

basics

~10 s

It sees the intrinsic itself, as data: a Ref or Fn::If node, not a resolved value. Nothing resolves parameters or conditions before deployment, so an equality test against a literal never matches.

open as a page

Your Sentinel rule blocks destroying resources tagged data-class=production — why does reading only the planned values miss them?

level: middleimportance: should knowfreq 54%

basics

~20 s

A resource being destroyed has no after values, and it is absent from the plan's planned end state entirely. Its tags live in the change's before values, taken from prior state — so the rule must read before, not after.

open as a page

In a Sentinel policy, which rule decides the verdict, and why might a rule you wrote never run?

level: middleimportance: should knowfreq 45%

basics

~20 s

The rule named main decides it: the policy passes only when main evaluates to true. Rules are evaluated lazily, so a rule that main never reaches is never evaluated at all and silently enforces nothing.

open as a page

Why does a custom Checkov check comparing conf["ami"] to a string fail every instance?

level: middleimportance: should knowfreq 38%

basics

~20 s

Checkov hands scan_resource_conf its own parsed representation of the block, where every attribute value is wrapped in a list. conf["ami"] is ["ami-0aaa1111"], never the bare string, so the comparison is always false and the check fails everything.

open as a page

What does a plan-time policy rule lose when you re-express it against recorded state or an inventory?

level: middleimportance: should knowfreq 58%

basics

~10 s

It loses the change action, the prior value, the surrounding request context that identified the author, and the implicit scope of only judging touched resources. A state record is attributes with no verb attached.

open as a page

When should a Checkov exemption be an inline skip comment rather than a config-file skip-check or skip-path?

level: middleimportance: should knowfreq 45%

basics

~20 s

An inline skip fits one resource that legitimately violates one check: narrow, and visible in the diff. Use skip-check or skip-path only when the rule or path is wrong for the whole scan, since both are invisible at the violating line.

open as a page

A policy gate denies a Terraform plan over a resource generated three modules deep — how do you resolve it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Translate the address back to the top-level module call the developer can actually edit, then fix it where it is fixable: a module input, a newer pinned version, or an upstream change. Attach any exception to the module and version, not to one plan.

open as a page

Your encryption-at-rest rule reads a key ARN unknown until apply: block, defer, or constrain the input?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Decide per control, not globally: blocking every unknown is safest but taxes teams with failures they cannot act on, deferring the assertion leaves a window where non-compliant infrastructure exists, and constraining the module input removes the unknown wherever you own the code.

open as a page

Your conftest gate has been green for months, but the rule never ran. How?

level: seniorimportance: should knowfreq 42%

basics

~20 s

A conftest run with nothing to evaluate exits zero. The usual causes are a namespace never selected on the command line, a renamed policy package, a glob that missed the files, or an extension with no parser attached - all of which look identical to a clean pass.

open as a page

A post-apply policy scan flags an end-of-support database engine in production — how do you choose what the finding triggers?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Nothing is deploying, so no gate can refuse it — the finding only starts a human workflow: an expiring exception, an owned ticket with a date, or a fix shipped through code. A clean re-run is the closure.

open as a page

A change-scoped Terraform plan gate never sees a legacy database. How do you close that gap?

level: principalimportance: should knowfreq 44%

basics

~20 s

Keep the blocking rule scoped to changed resources, and add a scheduled evaluation over the full recorded state that reports rather than blocks. Give each finding an owner and a date; widen the blocking rule once the backlog is drained.

open as a page

Your org already runs an IaC scanner — when do you extend it with custom checks rather than adopt a second policy engine?

level: principalimportance: should knowfreq 34%

basics

~20 s

Extend the scanner while your rules fit its model and its authors are the people already on the rota: you inherit its parsing, reporting, ids and pipeline placement for free. Adopt a separate engine when rules must span artifacts the scanner cannot parse, or when several gates should share one rule language.

open as a page

Your conftest rule requiring a PodDisruptionBudget for every Deployment never fires - why?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

conftest evaluates each YAML document in the stream separately, so a rule can never see a Deployment and a PodDisruptionBudget at the same time. Combine the inputs into one evaluation, or the rule stays permanently undefined.

open as a page

showing 1–30 of 37