ResourcesInfrastructure

Policy as Code: What It Is, Tools, and Examples

Policy as code writes security, compliance, and operational rules as versioned code that a policy engine evaluates automatically. This guide covers how it works, examples, where to enforce policies, a comparison of policy as code tools, and how to implement it.

Policy as code is the practice of writing security, compliance, and operational rules as code, storing them in Git, and letting a policy engine evaluate them automatically every time something changes. Instead of a reviewer remembering that production databases must be encrypted, a rule checks every change and blocks the ones that break it, with the reason attached.

TL;DR — Policy as code has three parts: rules written in a policy language, an engine that evaluates them against structured input, and an enforcement point that acts on the decision. Rules are versioned, reviewed, and tested like application code. Enforce them where changes happen: in pull requests, at infrastructure plan time, at Kubernetes admission, when someone requests a resource, and continuously against what is already running. Open Policy Agent is the most common general-purpose engine; Kyverno, Sentinel, Checkov, and the cloud-native policy services cover narrower scopes.

What is policy as code?

Policy as code (PaC) turns organizational rules into machine-readable definitions that are evaluated automatically. Every implementation has three parts:

  1. The policy. A rule written in a policy language, such as Rego for Open Policy Agent, YAML for Kyverno, or Sentinel’s own language. For example: “every resource needs owner and cost_center tags”.
  2. The engine. The component that evaluates the policy against structured input, usually JSON: a Terraform plan, a Kubernetes manifest, a request payload, or an inventory of running resources. The engine returns a decision, such as allow, deny, or a list of violations.
  3. The enforcement point. The place that asks for the decision and acts on it: a CI job that fails, an admission controller that rejects a pod, a workflow that stops before provisioning, or a scheduled audit that opens a ticket.

Because policies are code, they live in Git, go through pull requests, have tests, and keep a history. The same rule is applied identically in every environment, and every decision can be logged as audit evidence.

How policy as code works

A mature policy as code practice follows the same lifecycle as application code:

  1. Write the rule. Translate a requirement, such as “no public buckets”, into a policy file. Keep each rule small, named, and owned by a team.
  2. Test it. Write unit tests with sample inputs that should pass and fail. opa test and conftest verify run these tests in CI.
  3. Distribute it. Publish the policies to every enforcement point from one source of truth, as a bundle, a container image, or a Git reference, so CI, Kubernetes, and workflows all use the same version.
  4. Evaluate it. The enforcement point sends the input to the engine and gets a decision with the reasons.
  5. Enforce and record. Block, warn, or let the change through depending on the rule’s severity, and keep the decision, the input, and the policy version as evidence.

Start new rules in an audit-only mode that reports violations without blocking, measure how many existing resources would fail, fix or grant exceptions, then switch to blocking. Rules that block on day one tend to be disabled on day two.

Policy as code examples

The rules teams write first are usually the same:

RuleInput evaluatedTypical enforcement point
Every resource has owner and cost_center tagsTerraform or OpenTofu planCI or the provisioning workflow
Storage buckets are never publicTerraform plan, cloud inventoryPlan time, then continuous audit
Databases are encrypted at restTerraform plan, cloud inventoryPlan time, then continuous audit
Instance sizes are limited per environmentRequest or planRequest form, plan time
Container images come from approved registriesKubernetes manifestAdmission controller
Workloads declare CPU and memory limitsKubernetes manifestAdmission controller
Production changes happen inside maintenance windowsChange requestWorkflow before execution
Only approved regions are usedPlan, requestPlan time, request form

Here is the first rule written in Rego for Conftest. It reads the JSON output of terraform show -json and reports every created or updated resource that supports tags but is missing a required one:

package main
import rego.v1
required_tags := {"owner", "cost_center"}
deny contains msg if {
some rc in input.resource_changes
some action in rc.change.actions
action in {"create", "update"}
"tags" in object.keys(rc.change.after)
tags := object.get(rc.change.after, "tags", {})
missing := required_tags - {key | some key, _ in tags}
count(missing) > 0
msg := sprintf("%s is missing required tags: %v", [rc.address, missing])
}

Running conftest test plan.json returns a non-zero exit code and one message per violation, which is all an enforcement point needs to block the change and explain why.

Where to enforce policies

The same rule is worth checking at several points, because each catches a different failure:

Enforcement pointWhat it catchesTypical tools
Pull request and CINon-compliant code before it is mergedConftest, Checkov, Trivy
Infrastructure planWhat will actually change, with computed valuesOPA or Sentinel on the plan JSON
Request timeInvalid self-service requests, before anything runsValidation in the request form or workflow
Kubernetes admissionManifests applied by any path, including kubectlOPA Gatekeeper, Kyverno
Runtime and continuous auditDrift, console changes, resources created outside IaCCloud Custodian, AWS Config, Azure Policy, scheduled scans
Workflow and automation levelHow automation itself runs: credentials, concurrency, ownershipOrchestrator governance, such as Kestra Policies

Checks in pull requests give the fastest feedback. Checks at plan time are the most accurate for infrastructure. Continuous audit is the only way to catch what was changed outside the pipeline, which is why it pairs naturally with drift detection. Request-time checks matter for self-service infrastructure, where a rejected request should fail before any resource exists.

Policy as code tools

ToolPolicy languageScopeOpen sourceBest for
Open Policy Agent (OPA)RegoAny JSON input: IaC, Kubernetes, APIs, CIYes, CNCF graduatedOne policy language across many enforcement points
ConftestRegoConfiguration files: Terraform plans, manifests, DockerfilesYesPutting OPA policies into CI and workflows
OPA GatekeeperRegoKubernetes admission and auditYesTeams already writing Rego elsewhere
KyvernoKubernetes YAMLKubernetes admission, mutation, generationYes, CNCFKubernetes-only governance without learning Rego
HashiCorp SentinelSentinelHCP Terraform, Terraform Enterprise, other HashiCorp productsNoOrganizations standardized on HashiCorp’s commercial stack
CheckovBuilt-in rules, custom in Python or YAMLStatic analysis of IaC and container filesYesBroad out-of-the-box security checks in CI
TrivyBuilt-in rules, custom in RegoIaC misconfiguration, images, dependenciesYesOne scanner for misconfigurations and vulnerabilities
Cloud CustodianYAMLRunning cloud resources, with remediation actionsYes, CNCFContinuous enforcement and clean-up in cloud accounts
AWS Config, Azure Policy, Google Cloud Organization PolicyProvider-specificResources in one cloudNoNative guardrails inside a single provider

IaC platforms such as HCP Terraform and Spacelift embed OPA or Sentinel to gate their own runs. That works well for changes that go through the platform, but the rules stop at its boundary: they do not see Kubernetes, scripts, or resources changed elsewhere. For a broader comparison of those platforms, see Spacelift alternatives.

OPA vs. Sentinel

OPA is open source, general-purpose, and evaluates any JSON, so the same Rego can govern Terraform plans, Kubernetes manifests, and API requests. Sentinel is proprietary and runs inside HashiCorp’s commercial products. HCP Terraform supports both. Teams that want a single policy language across tools usually pick OPA; teams fully standardized on Terraform Enterprise often keep Sentinel.

OPA Gatekeeper vs. Kyverno

Both enforce policies at Kubernetes admission and audit existing resources. Gatekeeper uses Rego through constraint templates, which is consistent if Rego is already used for infrastructure. Kyverno policies are plain Kubernetes YAML and can also mutate resources and generate defaults, which lowers the barrier for teams whose only target is Kubernetes.

Scanners vs. policy engines

Checkov and Trivy ship hundreds of ready-made checks and give quick coverage of common misconfigurations. OPA and Kyverno start empty and enforce exactly the rules you write. Most teams combine them: a scanner for baseline security in CI, and a policy engine for organization-specific rules such as tags, sizes, regions, and ownership.

Ready to orchestrate your infrastructure?

Policy as code vs. infrastructure as code vs. compliance as code

Infrastructure as codePolicy as codeCompliance as code
DescribesWhat to buildRules that what you build must respectRegulatory controls mapped to automated checks
ExampleA Terraform module for a database“Databases must be encrypted at rest”SOC 2 control mapped to the encryption rule, with evidence kept
OutputChanges to infrastructureAllow, deny, or violationsAudit evidence and control status
Typical toolsTerraform, OpenTofu, Pulumi, AnsibleOPA, Kyverno, SentinelPolicy engines plus evidence collection and reporting

IaC produces changes, policy as code decides whether they are allowed, and compliance as code proves to an auditor that the decisions were made. All three are part of everything as code. For framework-specific guidance, see SOC 2 compliance and PCI DSS compliance automation.

How to implement policy as code

  1. Start with five rules that matter. Pick the rules behind your last incidents and audit findings: public exposure, encryption, tags, regions, and sizes. A short list that blocks is worth more than a long list nobody reads.
  2. Choose one engine and one language. If you have more than one enforcement point, OPA and Rego avoid writing the same rule three times. Add Kyverno or a scanner only where they clearly fit better.
  3. Store policies in their own repository. Give them owners, pull request reviews, tests, and versioned releases, the same way you treat pipelines as code.
  4. Run in audit mode first. Report violations without blocking, measure how many existing resources fail, then fix or document exceptions before switching to enforcement.
  5. Make exceptions explicit. A rule without an exception process gets bypassed. Record exceptions in code, with an owner, a reason, and an expiry date.
  6. Enforce at more than one point. Pull request for fast feedback, plan or request time for accuracy, and a scheduled audit for what changed outside the pipeline.
  7. Measure. Track violations per week, time to fix, the number of active exceptions, and the share of changes evaluated. A rule that never fires may be useless; a rule that always fires may be wrong.

Common pitfalls: rules that are too broad and block legitimate changes, policies copied into several places that drift apart, no tests on the policies themselves, and enforcement only at deploy time, which misses everything changed by hand afterwards.

Policy as code with Kestra

Kestra is an open-source orchestration platform with declarative YAML workflows. It applies policy as code at two levels: it runs your policy engines as steps of infrastructure workflows, and it enforces policies on the workflows themselves.

  • Policy checks as workflow steps. Run Conftest, Checkov, or Trivy in a container, or orchestrate OPA directly, at any point of a workflow: after a Terraform plan, before a Kubernetes deployment, or on a self-service request. A failed check fails the run, and the result stays in the execution history.
  • Continuous audit on a schedule. A scheduled workflow can evaluate policies against what is running and open a ticket or start a remediation workflow when something drifts, one of the recurring day 2 operations.
  • A lifecycle for the policies. Workflows can test policies and promote them between environments, so policy changes go through the same review and release path as code.
  • Policies for automation itself. In Kestra Enterprise Edition, Policies are written in YAML and inject, restrict, require, or deny configuration on flows and plugins across namespaces. They support an EVALUATE mode that reports violations before an ACTIVE mode blocks them, and child namespaces inherit their parents’ rules and can only tighten them.

Example: a policy gate on a Terraform plan

The workflow below clones the repository that holds the Terraform code (in a terraform/ folder) and the Rego policies (in policy/), runs terraform plan, exports it as JSON, and tests it with Conftest. Any deny rule that matches, such as the tagging rule above, makes Conftest exit with an error, which fails the execution and notifies Slack.

id: terraform_policy_gate
namespace: platform.guardrails
inputs:
- id: repo_url
type: STRING
defaults: https://github.com/your-org/infrastructure.git
tasks:
- id: policy_workspace
type: io.kestra.plugin.core.flow.WorkingDirectory
tasks:
- id: clone
type: io.kestra.plugin.git.Clone
url: "{{ inputs.repo_url }}"
branch: main
- id: terraform_plan
type: io.kestra.plugin.terraform.cli.TerraformCLI
containerImage: hashicorp/terraform:1.9
env:
AWS_ACCESS_KEY_ID: "{{ secret('AWS_ACCESS_KEY_ID') }}"
AWS_SECRET_ACCESS_KEY: "{{ secret('AWS_SECRET_ACCESS_KEY') }}"
beforeCommands:
- terraform -chdir=terraform init -input=false
commands:
- terraform -chdir=terraform plan -input=false -out=tfplan
- terraform -chdir=terraform show -json tfplan > plan.json
- id: policy_check
type: io.kestra.plugin.scripts.shell.Commands
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
containerImage: openpolicyagent/conftest:v0.71.1
commands:
- conftest test plan.json --policy policy/
errors:
- id: alert_policy_violation
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{"text": "Policy check failed for {{ inputs.repo_url }} (execution {{ execution.id }}). See the policy_check logs for the violated rules."}

The same pattern works for Kubernetes manifests: replace the plan step with the manifests to test them with Conftest. If you run OPA as a server, the Enterprise io.kestra.plugin.ee.opa.policy.Evaluate task can query it instead of running Conftest. A simpler CI policy gate is available as a blueprint.

Example: a policy on the workflows themselves

In Kestra Enterprise Edition, this Policy reports every flow in its namespace tree that does not declare a team label. Switching enforcement to ACTIVE turns the report into a block on save and execution.

id: label-audit
enforcement: EVALUATE
rules:
- type: io.kestra.plugin.ee.rules.Require
on: FLOW
properties:
- labels.team
errorMessage: "Every flow must declare labels.team."

Terraform, Ansible and Kubernetes, orchestrated from one place.

Frequently asked questions

Find answers to your questions right here, and don't hesitate to Contact Us if you couldn't find what you're looking for.