DevOps in Depth · Compliance as Code & Policy Enforcement

Compliance as Code & Policy Enforcement

Configuration Management & IaC named a split between preventive and detective guardrails and pointed here for the vendor-neutral version of it. Shift-Left Security for DevOps pointed here for "the policies behind" its IaC scanning gates "at real depth." Release Trains & Change Management called this "policy-as-code as its own discipline" and left the deeper treatment for this page. Security & Compliance covers AWS Config, Conformance packs, and Service Control Policies at exam depth — real products, wired into one vendor's console. This page covers what those products are actually instances of: a general-purpose policy engine that takes a structured description of a change or a resource, checks it against a rule written as code, and returns allow or deny — wherever you choose to plug that decision in. Learn the engine once — Open Policy Agent and its Rego language, or HashiCorp Sentinel — and AWS Config rules, Azure Policy, GCP Organization Policy, and a Kubernetes admission webhook all read as the same machine wearing a different vendor's badge.

☺ Explain it like I'm 10

Imagine one referee who has memorized exactly one rulebook — and that same referee can walk onto a soccer field, a basketball court, or a chess tournament and apply it, because the rulebook never changes, only the game around it does. A policy engine is that referee. Write the rule once — "no bucket may be public," "every deploy needs a cost-center tag" — and the same rulebook can referee a pull request, a Terraform plan, a Kubernetes cluster, or a cloud account, blowing the same whistle in all four places. The tools you'll meet elsewhere on this platform — AWS Config, Azure Policy, GCP Organization Policy — are each just one stadium that hired this referee and built its own scoreboard around them.

🐢🤖Your hosts for this topic: Timmy the Turtle & Recon the Robot — Timmy is the guardrail who blocks what shouldn't ship in the first place; Recon is the loop that keeps checking everything that already did, forever. Together they're the two moments one policy engine gets invoked at.

One engine, many rulebooks: policy as code, generally

☺ Like you're 10: A rule written once — "no public buckets" — turns into a program a computer can check, and that same program can referee a pull request, a running cluster, or an entire cloud account without being rewritten for each one.

Policy as code is the practice of writing a governance rule — a security control, a tagging standard, a compliance requirement — as versioned, machine-checkable code instead of a paragraph in a wiki page, and having a dedicated engine evaluate that code against a structured description of whatever's under review, rather than hand-coding the check into every tool that might need it. The cleanest way to describe every tool in this space, borrowed from decades-old access-control architecture, is a split into two roles. A Policy Decision Point (PDP) is the engine — OPA, Sentinel, AWS CloudFormation Guard — that takes a structured input plus a policy and returns a decision: allow or deny, plus the reason why. A Policy Enforcement Point (PEP) is wherever that decision actually does something — a CI job exiting non-zero, a Terraform run halting before apply, a Kubernetes admission webhook rejecting an object outright, a cloud API call failing before it ever takes effect. The decision-making logic and the place it's enforced are two separate, swappable pieces — the same PDP, the same Rego file or the same Sentinel policy, can be pointed at as many PEPs as your estate actually has.

AWS Config rules, Service Control Policies, and CloudFormation Guard are AWS's answer to exactly this split — a purpose-built PDP wired permanently into one specific PEP: the AWS control plane. That's a perfectly good design if every resource you'll ever govern lives inside one AWS account, and Security & Compliance covers that implementation at the depth the DOP-C02 exam expects. It stops being sufficient the moment your estate includes a Kubernetes cluster, a GitHub Actions pipeline, or a second cloud — none of which speak AWS Config's rule format or answer to an SCP. A general-purpose PDP exists to solve exactly that: write the rule once, and plug the same decision-making engine into as many enforcement points as you actually have.

◆ Key idea

Every policy-as-code tool on the market is a PDP (decides) paired with one or more PEPs (enforces). AWS Config + Config rules is a PDP soldered to one PEP. OPA and Sentinel are PDPs sold separately from any PEP at all — which is exactly why the same policy file can gate a pull request, a Terraform run, and a Kubernetes cluster without three separate rule libraries.

Preventive vs. detective: the same rule, two different moments

☺ Like you're 10: Some checks stop a bad thing from ever happening; other checks notice a bad thing that already happened and say so, days later. A setup that only does one of these has a hole in it.

Configuration Management & IaC already drew this split for one vendor's tools: a preventive guardrail — implemented on AWS as an SCP — blocks a disallowed action outright, before it happens. A detective guardrail — an AWS Config rule — flags a non-compliant resource after the fact, once it already exists. That split is not an AWS idea; it's the general shape every policy-as-code program eventually needs, because a preventive gate only sees what actually goes through the pipeline it's wired into. A hard gate in CI stops a bad Terraform plan from merging; it does nothing about a security group widened by hand from the cloud console during a 2 a.m. incident, or a resource that fell out of Terraform's management entirely. That gap is exactly what a detective check exists to close: the same policy, re-evaluated on a recurring schedule against a fresh snapshot of live state rather than a pending change, catching what bypassed the gate instead of what tried to pass through it.

One policy bundle, two invocation points Preventive — before it exists blocks the change Commit / PR terraform plan JSON Policy gate opa eval / conftest test BLOCKS MERGE Merge allowed Rejected Detective — after it exists flags the drift Deployed resource already live Policy re-evaluates on a recurring schedule FLAGS, DOESN'T BLOCK forever Compliant Non-compliant → remediation same rule, evaluated at two different times — one blocks a change, one catches what already changed

The payoff of writing the rule as a general-purpose policy rather than an AWS-specific Config rule is that the exact same Rego package can drive both halves of that diagram. The preventive half feeds it a Terraform plan JSON as input and runs inside CI. The detective half feeds the identical policy a resource inventory pulled from a cloud SDK or a CSPM tool's snapshot as input, on a cron schedule, and runs outside CI entirely. One policy, two input documents, two PEPs — not two rule libraries maintained in parallel and inevitably drifting apart from each other.

The reference engine: OPA and Rego, at the level you actually need

☺ Like you're 10: Rego reads less like a programming language and more like a spec sheet — "this is denied if all of these things are true at once" — written down once instead of remembered by whoever's on call.

The Open Policy Agent (OPA) is the reference-implementation PDP most other tools converge on or interoperate with. It runs as a standalone binary, a sidecar process, or an embedded library, and it separates every evaluation into two documents: input is the thing being evaluated right now — a Terraform plan, a Kubernetes admission request, an HTTP API call — and data is the policy's own reference material, loaded once and reused across evaluations: an allow-list of regions, a mapping of team to cost center, anything a rule needs to check against rather than check itself. Policies are written in Rego and organized into namespaced packages, queried by path the same way you'd address a file — data.terraform.s3.deny asks OPA for the deny set inside the terraform.s3 package.

OPA has no built-in concept of "allow" or "deny" — by convention, a policy defines a set named deny or violation, and the calling tool (Conftest, Gatekeeper, a hand-rolled CI script) simply checks whether that set came back empty. Inside a single rule, every line in the body is implicitly ANDed — all of them have to hold for that rule to fire — and multiple rules sharing the same name are implicitly ORed, so a policy file typically grows into a stack of small, independent deny[msg] { ... } blocks, each one a separate reason a change might get rejected, rather than one large branching function.

package terraform.s3

import future.keywords.in

# Every S3 bucket in the plan must carry an approved cost_center tag,
# and default encryption may never be removed from an existing bucket.

allowed_cost_centers := {"eng-platform", "eng-payments", "eng-data"}

deny[msg] {
	some addr
	rc := input.resource_changes[addr]
	rc.type == "aws_s3_bucket"
	rc.change.actions[_] in {"create", "update"}

	tag := object.get(rc.change.after.tags, "cost_center", "")
	not tag in allowed_cost_centers

	msg := sprintf(
		"%s: cost_center tag %q is missing or not one of %v",
		[rc.address, tag, allowed_cost_centers],
	)
}

deny[msg] {
	some addr
	rc := input.resource_changes[addr]
	rc.type == "aws_s3_bucket_server_side_encryption_configuration"
	rc.change.actions == ["delete"]

	msg := sprintf(
		"%s: default encryption is being removed from an S3 bucket",
		[rc.address],
	)
}

That policy is genuinely just two straightforward rules, and that's deliberate — most of what a policy-as-code program actually enforces reads this plainly once you know the some/iteration pattern and the deny convention. Because a Rego file is a plain text file, it gets the same discipline as any other code, starting with tests that live in the same package: opa test discovers any file ending in _test.rego, runs every rule prefixed test_, and mocks input per test case with with input as { ... } — so a change that quietly stops a rule from catching what it used to catch fails a test the same day it's introduced, not the day an auditor asks why a public bucket shipped clean.

package terraform.s3

test_denies_missing_cost_center_tag {
	count(deny) == 1 with input as {
		"resource_changes": [{
			"address": "aws_s3_bucket.reports",
			"type": "aws_s3_bucket",
			"change": {"actions": ["create"], "after": {"tags": {}}},
		}],
	}
}

test_allows_approved_cost_center {
	count(deny) == 0 with input as {
		"resource_changes": [{
			"address": "aws_s3_bucket.reports",
			"type": "aws_s3_bucket",
			"change": {
				"actions": ["create"],
				"after": {"tags": {"cost_center": "eng-platform"}},
			},
		}],
	}
}
# Unit-test the policy itself, before it ever gates a real plan
opa test policy/ -v

# Evaluate a real Terraform plan against it — this is the PR-time PEP
terraform show -json plan.tfplan > plan.json
conftest test plan.json -p policy/

# Or ask OPA directly for the decision, the way a CI script would
opa eval -i plan.json -d policy/ "data.terraform.s3.deny"

Sentinel: HashiCorp's take on the same pattern

☺ Like you're 10: Sentinel is the same referee idea, except HashiCorp built it directly into Terraform's own run pipeline instead of selling it as a separate tool you have to wire in yourself.

HashiCorp Sentinel ships as part of Terraform Cloud and Terraform Enterprise and evaluates automatically as a stage in every plan/apply run — there's no separate CI step to build, because the PEP is already the platform running your Terraform. Its policy language isn't Rego; it's Sentinel's own HCL-adjacent syntax that imports typed views of the run — tfplan/v2 for the plan, tfstate/v2 for prior state, tfconfig/v2 for the configuration itself — and expresses a rule as a set of named boolean checks that main combines.

import "tfplan/v2" as tfplan

s3_buckets = filter tfplan.resource_changes as _, rc {
	rc.type is "aws_s3_bucket" and
	(rc.change.actions contains "create" or rc.change.actions contains "update")
}

has_cost_center = rule {
	all s3_buckets as _, bucket {
		"cost_center" in keys(bucket.change.after.tags else {})
	}
}

main = rule {
	has_cost_center
}

What Sentinel has that a raw OPA setup doesn't ship with out of the box is a first-class notion of how strictly a policy binds, set per policy in a sentinel.hcl configuration rather than buried in a wrapper script:

Enforcement levelBehavior
advisoryThe check runs and logs its result, but never blocks the run — visibility only, no gate.
soft-mandatoryBlocks the run by default, but a user holding an authorized Terraform Cloud role can override and proceed anyway — the override itself is logged.
hard-mandatoryBlocks the run unconditionally. No override exists at run time, regardless of role.
policy "mandatory-cost-center-tag" {
	source            = "./mandatory-cost-center-tag.sentinel"
	enforcement_level = "hard-mandatory"
}

Architecturally the two tools make opposite trade-offs. Sentinel is a PDP permanently wired to one PEP by its vendor — Terraform runs — which is why it needs no separate integration work but also why it can't gate anything that isn't a Terraform run. OPA is a PDP with no default PEP at all; you supply the wiring, which is more setup but is exactly what lets the same Rego package also gate a Kubernetes cluster or a plain GitHub Actions pipeline that never touches Terraform Cloud. Reach for Sentinel when your estate already runs entirely through Terraform Cloud/Enterprise and you want its enforcement-level model for free; reach for OPA the moment you need one policy to referee more than one kind of pipeline.

Wiring the gate: three places a pipeline can enforce policy

☺ Like you're 10: The same rulebook can referee at three different moments — while you're still drafting the change, right before it takes effect, and every single time afterward that anyone tries to create something new.

draft-time pre-apply runtime, forever Commit / PR PR gate OPA via Conftest blocks merge Plan & apply CD gate Sentinel / custom hook blocks apply Running system Admission gate Gatekeeper / Kyverno blocks every create, forever — even by hand three enforcement points, one policy — only the last one sits in the system's own write path

The PR-time gate — Conftest or OPA run against terraform plan -json, alongside the Checkov/tfsec pattern Shift-Left Security for DevOps already covers for known-bad IaC patterns — is the cheapest place to run a check and catches the most, since fixing a policy violation as a one-line diff comment is about as cheap as feedback gets. Its blind spot is that it only sees the change in front of it, not the accumulated state of everything already deployed.

The plan/apply-time gate — Sentinel inside Terraform Cloud, or a custom OPA hook wired into whatever tool actually runs apply — sits one layer more authoritative: it evaluates the fully computed plan, including resource changes a provider default or a module update introduces that the PR diff alone never shows.

The admission-time gate is the only one of the three that also catches a change that skipped the pipeline entirely, because it sits in the write path of the live system itself, not the write path of the pipeline that's supposed to lead to it. On Kubernetes, this is OPA Gatekeeper or Kyverno intercepting a kubectl apply — including one run by hand, at 2 a.m., by someone with cluster credentials and no pull request in sight. On AWS, the equivalent is a Service Control Policy: it doesn't care whether an API call originated from a pipeline, the console, or a rogue script, because it's evaluated by IAM itself before the call is ever allowed to execute.

Treat policy like code: tests, versions, and the fail-open question

☺ Like you're 10: A policy file is a program, so treat it like one — write a test for it, review a change to it, and decide in advance what happens if the referee doesn't show up to the game.

A policy-as-code program earns its name only once the policies themselves get the same discipline as application code: version-controlled, code-reviewed — a change to a deny rule is at least as consequential as a change to the infrastructure it governs, sometimes more — unit tested the way the previous section's _test.rego file demonstrates, and released as a versioned bundle so a consuming pipeline pins to a known-good policy version instead of getting a rule change mid-run because someone pushed to main an hour ago. OPA's own bundle format (a signed .tar.gz served over HTTP or pulled from an OCI registry) exists precisely so a fleet of PEPs — every CI runner, every Gatekeeper install — can poll for and pin to the same versioned policy source of truth.

The question every hard gate eventually has to answer explicitly is what happens when the policy engine itself is unreachable at evaluation time. Fail-open lets the change through unevaluated — governance silently switches off at precisely the moment something's already gone wrong with the engine protecting it. Fail-closed blocks everything until the engine answers — safer, but it makes the policy engine's own uptime a hard dependency of every merge, every apply, every kubectl apply across the org it gates.

⚠ Watch out

There is no default here that's free of a real trade-off, and the choice should be made per policy, not once for the whole program. Fail-closed makes sense for a hard, preventive gate protecting something genuinely dangerous — a public bucket holding customer data, an IAM policy granting *:* — provided the policy engine itself is run with a reliability budget at least as strict as whatever it's gating; a fail-closed gate backed by a policy engine nobody's paged for is a self-inflicted outage waiting to happen. Fail-open is defensible only for genuinely advisory checks, where missing one evaluation costs a delayed warning, not a live exposure.

The continuous-compliance loop, and the audit trail it produces for free

☺ Like you're 10: A rule that only runs once, when something's created, tells you what was true on day one. A rule that keeps running forever is what lets you say "we're compliant" as a fact about right now, not a memory of an audit that happened once.

Security & Compliance already named the shape of this loop for AWS Config specifically: "desired state (the rule), actual state (the config item), a diff, a correction — the same shape you already know from Terraform state and GitOps drift." Stated without the AWS nouns, the loop is: a policy bundle is deployed once; a scheduler — a cron job, an EventBridge-equivalent trigger, a controller loop — re-runs the exact same deny/violation set against a freshly pulled snapshot of live state; and every single evaluation writes a timestamped, rule-ID-tagged, pass-or-fail record, whether or not it found anything wrong. A clean pass is itself evidence, not a non-event.

When an evaluation comes back non-compliant, the loop closes without a human in it by attaching a remediation action to that specific finding — on AWS, an SSM Automation document; in general, any webhook or function triggered off the failing evaluation — carrying the same guardrail Security & Compliance flags for it: a retry count and a rate limit, so a remediation action that itself keeps failing doesn't retrigger into a storm instead of quietly paging someone.

The part worth naming explicitly, because it's what Release Trains & Change Management promised a deeper treatment of: map each policy to a named control ID from whatever framework actually applies — SOC 2 CC6.1, PCI-DSS requirement 3.4, a specific CIS Benchmark line item — and a passing evaluation record becomes simultaneously an engineering safeguard and citable audit evidence, queryable by control ID on demand instead of gathered by a human under deadline the week before an auditor arrives. Turning that evidence trail into an actual audit deliverable — who signs off on a control's design, how often it's re-tested, which framework an organization is actually scoped against — is a governance discipline in its own right, and DevSecOps' Compliance & Governance covers it properly; what this page gives you is the evidence-producing machinery that discipline gets built on top of.

Same pattern, different badge: mapping the vendor tools

☺ Like you're 10: Every cloud, every CI tool, and Kubernetes itself reinvented this same referee-and-rulebook idea under a different brand name — once you've seen the pattern once, you can find it under any vendor's logo.

With the general pattern in hand, the AWS-specific services Security & Compliance covers at exam depth — and the near-equivalents on other clouds — read as instances of exactly four moving pieces, not four unrelated products to memorize separately:

General conceptVendor-neutral toolAWS implementationAlso seen as
Policy engine — the PDPOPA/Rego, HashiCorp SentinelAWS CloudFormation GuardAzure Policy's rule language; GCP's Config Validator, itself built on OPA
Preventive gateCI hard gate, Sentinel in a Terraform run, an admission webhookService Control Policies, IAM permission boundariesAzure Policy deny effect; GCP Organization Policy constraints
Detective / continuous checkA scheduled opa eval against live state, a CSPM scanAWS Config managed & custom rulesAzure Policy audit effect; GCP Security Command Center findings
Bundled rule set at scaleAn OPA/Sentinel policy bundle, versioned and distributedConformance packs, org conformance packsAzure Policy initiatives; GCP organization policy bundles
Closing the loop, no humanA webhook/function triggered on a failing evaluationSSM Automation remediation actionsAzure Policy remediation tasks; a function triggered by an SCC finding
Kubernetes-native instanceOPA Gatekeeper, Kyverno — admission webhooks running the same PDP/PEP splitnot tied to any one cloud; runs identically wherever the cluster does

The point isn't that OPA and AWS Config compete — Config's rule engine is already doing the OPA/Rego job, just permanently soldered to one PEP, and that fusion is a genuine advantage the moment your entire estate is a single AWS account. A general-purpose PDP is what you reach for the exact moment that stops being true — when the same tagging rule, the same encryption requirement, the same "no public anything" policy also has to referee a Kubernetes cluster, a second cloud, or a CI pipeline that's never heard of AWS Config in the first place.

A policy that catches a misconfiguration before it merges is a deploy that doesn't become an incident — which is exactly why this page's subject matter feeds directly into change failure rate, one of the DORA metrics this course keeps circling back to. For the provenance and SBOM side of the same shift-left instinct — proving exactly what shipped, not just checking it before it did — see Supply-Chain Security & SBOM; for the IaC scanning tools that most commonly sit right in front of this page's PR-time gate, see Shift-Left Security for DevOps.

🎬 At the Ship-It Guild
🦫

Benny the Beaver: I wrote one Rego rule for "every bucket needs a cost-center tag." Do I have to write it again for the Kubernetes cluster, and again for the AWS account?

🐢

Timmy the Turtle: Same rule, Benny. Point Conftest at it for the PR, point Gatekeeper at it for the cluster — one rulebook, two referees.

👺

Gizmo: Or — hot take — just set every enforcement level to advisory. Nothing ever blocks, nobody ever complains. 🤑

🐢

Timmy the Turtle: Advisory forever just means the finding piles up until nobody reads it, Gizmo. Some of these need to be hard-mandatory — a public bucket holding customer data doesn't get a "proceed anyway" button.

🦊

Foxy: What if someone bypasses the pipeline entirely and hand-edits it from the console?

🤖

Recon: BEEP. Then my copy of the same rule catches it on the next scheduled evaluation. I can't block a console change — I'm not in that write path — but I flag it the moment it drifts, every cycle, forever.

🐢

Timmy the Turtle: That's the whole page in one scene. I stop what I can see coming. Recon catches what got past me.

🐢 Timmy's checkpoint

1. In one sentence, what's the difference between a Policy Decision Point and a Policy Enforcement Point — and give one example of each from this page. 2. What's the practical difference between a preventive control and a detective control, and why does a mature program need both instead of just the stronger-sounding one? 3. In Sentinel, what's the difference between soft-mandatory and hard-mandatory enforcement, and when would you choose each? 4. Why does a fail-closed policy gate turn the engine itself into a hard dependency of everything it protects, and what's the risk of choosing fail-open instead? 5. Name the AWS-specific counterpart to each of: a general-purpose policy engine, a preventive org-wide guardrail, and a detective rule re-evaluated on a schedule. 6. Why can the exact same Rego policy file serve as both a preventive CI gate and a detective drift check?

Check your answers
  1. A Policy Decision Point is the engine that evaluates a policy against structured input and returns allow/deny (OPA, Sentinel, CloudFormation Guard); a Policy Enforcement Point is wherever that decision actually takes effect (a CI job failing, a Terraform run halting, an admission webhook rejecting an object).
  2. Preventive controls block a disallowed action before it happens (an SCP, a CI hard gate); detective controls flag a resource that's already non-compliant, after the fact (an AWS Config rule, a scheduled scan). A preventive gate only sees changes that actually go through the pipeline it's wired into — a manual console change or break-glass edit bypasses it entirely, which only a detective check re-evaluating live state will ever catch.
  3. Soft-mandatory blocks by default but lets an authorized role override and proceed, with the override logged; hard-mandatory blocks unconditionally with no run-time override regardless of role. Choose soft-mandatory for rules with legitimate, occasional exceptions; hard-mandatory for violations with no acceptable exception in the organization.
  4. Fail-closed blocks every gated action whenever the policy engine itself is unreachable, so the engine's uptime becomes a prerequisite for merging, deploying, or applying anything — if it's down, nobody ships anywhere. Fail-open avoids that but silently disables governance at exactly the moment something's already wrong with the engine, which is close to the worst possible time for a check to go quiet.
  5. General-purpose policy engine → AWS CloudFormation Guard. Preventive org-wide guardrail → Service Control Policies (and IAM permission boundaries at the identity level). Detective rule re-evaluated on a schedule → AWS Config managed or custom rules.
  6. Because the policy logic only cares about the shape of its input, not where that input came from. The PR-time run feeds it a Terraform plan JSON as input; the scheduled run feeds the identical deny rules a resource inventory pulled from live state as input instead — same policy file, two different input documents, two different PEPs.