Policy as Code: OPA Guardrails With Fast Feedback
A developer opens a pull request for a Terraform change. Sixteen hours later, a security review rejects it: the S3 bucket lacks encryption. The developer fixes it, waits another day for re-review. This cycle—write, wait, reject, fix, wait—drains velocity and breeds resentment toward security processes.
Shift-left advocates say to check policies earlier. But “earlier” often means CI, which still means waiting for pipelines after pushing code. Real shift-left means before the commit—policy checks that run in seconds during git commit, catching violations while the context is fresh and the fix is trivial.
OPA (Open Policy Agent) and Conftest make this possible. OPA is a general-purpose policy engine that evaluates structured data against policies written in Rego. Conftest wraps OPA with ergonomic defaults for infrastructure files—it parses Terraform, Kubernetes YAML, and Dockerfiles into JSON that OPA can evaluate. Together, they provide fast, local policy enforcement that doesn’t require cloud credentials or pipeline execution.
Policy adoption paradox: comprehensive policies with slow feedback get disabled. Minimal policies with fast feedback get expanded. Start with five critical policies—encryption enabled, no public access, resource limits, no privileged containers, no :latest tags—that run in under two seconds. Get adoption first, then add coverage.
Pre-commit Hooks for Instant Feedback
Pre-commit is where shift-left becomes real. Conftest integrates with the pre-commit framework to run policies against staged files before they’re committed. The key is speed—pre-commit hooks that take more than a few seconds get disabled.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/open-policy-agent/conftest
rev: v0.45.0
hooks:
- id: conftest
name: Conftest Kubernetes
entry: conftest test
args:
- --policy=policies/kubernetes
- --all-namespaces
files: '\.ya?ml$'
exclude: 'policies/.*'
- id: conftest
name: Conftest Terraform
entry: conftest test
args:
- --policy=policies/terraform
- --parser=hcl2
files: '\.tf$'Pre-commit only runs against staged files by default, which keeps evaluation fast. Separate hooks for different file types ensure Kubernetes policies don’t run on Terraform files and vice versa. The same policies run at pre-commit, CI, and deployment—just with different optimization strategies at each stage.
The pre-commit framework shown above is language-agnostic, but each ecosystem has its canonical approach:
| Ecosystem | Pre-commit Tool | Installation |
|---|---|---|
| Python | pre-commit | pip install pre-commit && pre-commit install |
| Node.js | Husky + lint-staged | npx husky init |
| Ruby/Rails | Overcommit | gem install overcommit && overcommit --install |
| Go | Lefthook | go install github.com/evilmartians/lefthook |
Husky is Node.js-based but runs any shell command, making it viable for polyglot repositories with a package.json. For pure infrastructure repos without Node, the Python pre-commit framework or Lefthook (Go-based, no runtime dependencies) are better choices.
Speed is non-negotiable: if policy checks take more than a few seconds, developers will find ways to skip them. Pre-commit hooks should complete in under two seconds for typical changesets. If yours don’t, namespace filtering and policy bundling (covered in the comprehensive guide) can dramatically improve performance.
Writing Policies That Get Adopted
Four principles separate policies that get adopted from policies that get bypassed:
Single responsibility. Each rule should check one thing. A 50-line rule that validates images, resources, and labels produces confusing violations and resists modification. Split concerns into separate rules: deny_unauthorized_image, deny_missing_resources, deny_missing_labels. When a violation fires, developers know exactly what to fix.
Actionable messages. “Policy violation” tells the developer nothing. “Container missing resource limits” is better. “Container ‘nginx’ missing resource limits. Add spec.containers[].resources.limits.cpu and memory” is what they actually need. Include the resource name, the violation, and the fix path.
Minimal false positives. Precision matters more than recall. A policy that blocks valid configurations trains developers to request exceptions—or bypass the system entirely. Start permissive and tighten over time. Test against real configurations before enforcing. Provide escape hatches for legitimate edge cases.
Fast evaluation. Policies should evaluate in milliseconds, not seconds. Avoid external HTTP calls within policies—pre-load data into bundles. Avoid complex regex on large inputs. Avoid deep recursion.
Structure violation messages consistently:
[Resource Type] [Resource Name]: [Violation].
Fix: [Specific remediation]Examples: “Deployment ‘api-server’: Missing required label ‘team’. Fix: Add metadata.labels.team with your team name.” Or: “Pod ‘worker’: Container ‘app’ uses image from unauthorized registry ‘docker.io’. Fix: Use images from ‘gcr.io/company-project’ or ‘artifactory.company.com’.”
$ Stay Updated
> One deep dive per month on infrastructure topics, plus quick wins you can ship the same day.
Organize policies by technology, then resource type, then concern. This structure makes policies discoverable and enables selective evaluation:
policies/
├── kubernetes/
│ ├── pods/
│ │ ├── privileged.rego
│ │ ├── resources.rego
│ │ └── images.rego
│ └── common/
│ └── helpers.rego
├── terraform/
│ └── aws/
│ ├── s3.rego
│ ├── iam.rego
│ └── security_groups.rego
└── data/
└── allowed_registries.jsonConftest uses the --namespace flag to selectively evaluate policies. Run only Kubernetes policies on YAML files with conftest test deployment.yaml --namespace kubernetes. Run only Terraform AWS policies with conftest test tfplan.json --namespace terraform.aws. This is how namespace organization pays off—you can run subsets of policies based on context.
Terraform Plan Evaluation in Practice
Two approaches exist for evaluating Terraform configurations: HCL parsing and plan JSON. HCL parsing (conftest test *.tf --parser hcl2) catches static violations without cloud access—fast, no credentials required, but no variable resolution. Plan JSON evaluation sees the complete resolved configuration but requires running terraform plan first. Use HCL parsing for fast feedback at pre-commit; use plan JSON for security policies that need the full picture.
Plan JSON contains the full resolved state of what Terraform will create—variables interpolated, modules expanded, data sources resolved. This is where security policies have complete visibility.
The plan JSON structure nests resource changes under input.resource_changes. Each change includes before (current state), after (planned state), and actions (create, update, delete, no-op). Helper functions make policies more readable:
package terraform.aws
import future.keywords.if
import future.keywords.in
import future.keywords.contains
# Helper: get all resources being created or modified
resources[resource] {
resource := input.resource_changes[_]
resource.change.actions[_] != "no-op"
}
# Helper: filter resources by type
resources_by_type(type) := [r | r := resources[_]; r.type == type]
These helpers eliminate boilerplate from every policy. Instead of repeating the resource iteration logic, each policy focuses on what it’s actually checking. Here’s S3 bucket security that blocks public access and requires encryption:
deny contains msg if {
bucket := resources_by_type("aws_s3_bucket")[_]
bucket.change.after.acl == "public-read"
msg := sprintf("S3 bucket '%s' cannot be public-read", [bucket.name])
}
deny contains msg if {
bucket := resources_by_type("aws_s3_bucket")[_]
not bucket.change.after.server_side_encryption_configuration
msg := sprintf("S3 bucket '%s' must have encryption enabled", [bucket.name])
}
Security group rules that block open SSH follow the same pattern:
deny contains msg if {
sg := resources_by_type("aws_security_group")[_]
rule := sg.change.after.ingress[_]
rule.cidr_blocks[_] == "0.0.0.0/0"
rule.from_port <= 22
rule.to_port >= 22
msg := sprintf("Security group '%s' allows SSH from 0.0.0.0/0", [sg.name])
}
Going Further
This article focused on getting started fast: pre-commit hooks, design principles, and Terraform policies. Production deployments require more—Kubernetes-specific policies, exception handling for legitimate edge cases, CI/CD integration patterns, and performance optimization techniques. The comprehensive guide covers all of these topics with detailed examples and tested patterns.
Policy as Code: OPA Guardrails With Fast Feedback
Implementing infrastructure policies with OPA and Conftest that catch violations before they reach production.
What you'll get:
- Rego policy starter patterns
- Pre-commit hook setup guide
- Terraform guardrail rule pack
- Policy rollout governance checklist
The measure of success isn’t how many violations you block—it’s how few violations reach production combined with how little friction developers experience. Policies that developers trust are fast enough to not slow them down, accurate enough to not cry wolf, and flexible enough to handle real-world complexity.
Table of Contents
Share this article
Found this helpful? Share it with others who might benefit.
Share this article
Enjoyed the read? Share it with your network.