Safeguard
Cloud Security

Policy as Code: Enforcing Cloud Security Guardrails in CI/CD Instead of Manual Review

OPA reached CNCF Graduated status in January 2021 — yet most teams still catch misconfigured IAM roles by eyeballing a pull request.

Safeguard Research Team
Research
Updated 7 min read

Open Policy Agent entered the Cloud Native Computing Foundation as a sandbox project on March 29, 2018, and by January 29, 2021 it had climbed to Graduated status — the same tier as Kubernetes and Prometheus, and CNCF's highest bar for production maturity. That trajectory matters because OPA's policy language, Rego, solved a problem most cloud teams were still solving with a human: deciding, at the moment a change is proposed, whether an IAM role, a security group, or a Terraform plan violates security policy. This is the foundation of cloud security automation — the broader discipline of cloud security and DevOps automation that treats guardrails as pipeline code rather than a manual step: encoding those judgments so a machine applies them to every change instead of relying on a reviewer to notice. The 2019 Capital One breach is the case study everyone in this space cites, and for good reason — a misconfigured web application firewall with an over-permissioned IAM role was exploited via server-side request forgery to reach the EC2 instance metadata service, harvest temporary credentials, and exfiltrate roughly 106 million customer records from S3. Nobody wrote that IAM policy maliciously; it passed whatever review existed at the time. Done well, this is what cloud security and DevOps automation looks like in practice — the guardrail becomes part of the pipeline instead of a person's job. This post walks through how OPA/Rego and HashiCorp Sentinel let teams turn "someone should catch this in review" into "the pipeline cannot proceed until this is fixed," where in the CI/CD lifecycle to place those checks, and what changes when guardrails move from advisory documentation to enforced code.

What problem does policy as code actually solve?

Policy as code solves the gap between a written security standard and what actually gets deployed. A wiki page that says "S3 buckets must not be publicly readable" only works if every engineer reads it, remembers it, and applies it correctly on every pull request, forever. Policy as code instead expresses that rule as executable logic — a Rego rule, a Sentinel policy, or a YAML guardrail — that runs automatically against every proposed change and produces a deterministic allow/deny decision. This shifts enforcement from a point-in-time human judgment call to a repeatable, versioned, testable artifact that lives in source control alongside the infrastructure it governs. Because the policy is code, it gets code review, unit tests, and a change history, so a security team can prove exactly what rule was in force when a given resource was approved — a materially different audit posture than "we told people not to do that in a Confluence doc." In that sense, policy as code is the enforcement layer of cloud security automation: the piece that turns detection into prevention.

How does Rego evaluate a real policy decision?

Rego is a declarative language, loosely modeled on Datalog, that takes structured JSON input and returns a structured decision rather than executing imperative steps. OPA is typically embedded at the decision point itself — as a sidecar, a library, or invoked directly by a tool like Conftest against a terraform plan JSON export — so the policy engine never needs network access to the resource it's judging. A minimal rule blocking public S3 buckets looks like this:

package terraform.s3

deny[msg] {
  resource := input.resource.aws_s3_bucket[name]
  resource.acl == "public-read"
  msg := sprintf("S3 bucket %v must not use public-read ACL", [name])
}

Run against a terraform plan, this either returns an empty deny set (pass) or a list of human-readable violation messages (fail), and CI treats a non-empty result as a build failure. Because Rego rules are ordinary code, they can be unit tested with opa test before they ever gate a real deployment, which is what separates policy as code from a brittle shell script grepping Terraform output.

How is HashiCorp Sentinel different from OPA and Rego?

Sentinel is a separate, proprietary policy-as-code framework built and maintained by HashiCorp — it does not use Rego, and it is not an OPA-compatible engine. Sentinel ships embedded inside Terraform Cloud and Terraform Enterprise, and inside the Enterprise editions of Vault, Consul, and Nomad, and it evaluates policies against a "sentinel document," a structured representation HashiCorp generates from the Terraform plan (or the equivalent Vault/Consul/Nomad request) before an apply is allowed to proceed. The practical difference for a platform team is deployment model rather than expressive power: OPA is a general-purpose engine you wire into whatever enforcement point you choose — Kubernetes admission via Gatekeeper, an API gateway, a CI step — while Sentinel is scoped to the HashiCorp product surface and ships with built-in policy sets tuned to Terraform plans and Vault/Nomad/Consul requests. Teams standardized on Terraform Cloud/Enterprise often adopt Sentinel because it requires no separate policy engine to operate; teams with a mixed stack spanning Kubernetes, service meshes, and multiple IaC tools more often standardize on OPA specifically because it is not tied to one vendor's control plane.

What would policy as code have caught in the Capital One breach?

The Capital One breach is instructive precisely because every individual failure in the chain — an overly permissive IAM role attached to the WAF, an EC2 instance metadata service left reachable in a way that allowed credential theft via SSRF, and broad S3 read permissions on the compromised role — is the kind of condition a pre-deploy policy check is designed to catch mechanically. A Rego or Sentinel rule enforcing least-privilege IAM (denying wildcard s3:* or * resource scopes on roles attached to internet-facing services) or requiring IMDSv2 on EC2 instances would have evaluated the same infrastructure-as-code change that a human reviewer approved, and it would have applied the same scrutiny on the thousandth deploy as the first. That is the core argument for automated enforcement over manual review: reviewers get faster, busier, and less consistent over time; a policy engine evaluates every change with the same rule set, every time, without fatigue.

Where in the CI/CD pipeline should these guardrails run?

Guardrails work best layered at multiple points rather than relying on a single gate. Pre-merge, a Conftest or Sentinel check against the Terraform plan catches misconfiguration before it ever reaches an apply step, which is the cheapest place to reject a change. At admission time, a Kubernetes validating webhook (the pattern OPA Gatekeeper and tools like Kyverno both implement) re-checks the same class of policy against the actual object being created, catching drift introduced outside the IaC pipeline — a kubectl apply run directly against a cluster, for instance. Runtime checks form a third layer, detecting when a live resource has drifted from its approved baseline after deployment. Running the same logical policy at plan-time and admission-time is deliberate redundancy: IaC review can be bypassed by anyone with cluster credentials, so admission enforcement is what actually guarantees the rule holds in production, not just in the repository.

How Safeguard Helps

Safeguard's guardrail system applies this same policy-as-code model as a first-class product surface rather than something teams have to assemble from separate OPA and CI tooling. Guardrails are defined as YAML policies with a target, one or more rules, and an effect of BLOCK, WARN, or AUTO_FIX, and they're enforced consistently across six points in the lifecycle — IDE, commit, CI, registry, Kubernetes admission, and runtime — so the same rule that blocks a non-compliant image in CI also denies it at the cluster's admission controller if it somehow gets there another way. The safeguard gate CLI step plugs directly into a CI pipeline and exits non-zero on any blocking violation, producing a machine-readable report auditors can replay later, while the admission controller ships as a Helm chart supporting both enforce and audit (dry-run) modes for safe rollout. Built-in guardrails like blocking CISA KEV-listed CVEs in production, requiring a signed SBOM attestation, and blocking unsigned container images cover the same category of risk that IAM and S3 misconfiguration checks address in the IaC layer — and every guardrail decision, block or allow, is logged to a signed audit stream so the answer to "what policy was in force when this was approved" is a query, not an investigation. That's cloud security and DevOps automation working as intended: the same policy enforced the same way at every stage, with a record to prove it.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.