Policy-as-code isn’t new. OPA has been around since 2016. Kubernetes admission controllers have existed forever in cloud-years. And yet, most teams only start caring now. Policy-as-code With Opa: A Practical Starter Kit
In my experience, that’s because the pain finally outweighs the friction.
Modern platforms have crossed a threshold:
-
Hundreds of repos
-
Dozens of teams
-
Multiple clouds
-
CI pipelines nobody fully understands
-
Security requirements that can’t be “just guidelines” anymore
At that scale, human-enforced rules stop working. You can write docs. You can run brown-bags. You can beg people to follow standards. It doesn’t stick.
So teams swing the pendulum the other way: hard enforcement. Admission controllers everywhere. Pipelines that fail for reasons nobody understands. Suddenly developers hate “platform” and start finding ways around it.
This is where policy-as-code should help but often doesn’t, because it’s applied badly.
The real value of OPA is not “security” or “compliance”. It’s creating a shared, testable contract between platform teams and developers about what’s allowed, what’s required, and what’s intentionally flexible.
But OPA is sharp. Rego is weird. And it’s easy to build something technically impressive that is socially unshippable.
This guide exists to help you avoid that.
Not theory. Not marketing. Just the stuff that actually works.
What is OPA
OPA is a policy engine, not a product and not a framework.
At its core, OPA does three things:
-
You give it input data (JSON)
-
You give it policy (written in Rego)
-
It evaluates the policy and returns a decision
That’s it.
OPA does not:
-
Deploy itself
-
Integrate automatically with Kubernetes
-
Know what “secure” means
-
Decide when to block or warn
Those responsibilities are on you.
Think of OPA like a very fast, embeddable rules engine that speaks JSON and returns structured answers.
Why Rego exists
Rego is a declarative query language. You don’t write “if/else” logic the way you would in Go or Python. Instead, you define conditions under which something is true.
This feels alien at first. Most people fight it. The trick is to stop thinking in control flow and start thinking in sets of violations.
In practice, the best mental model is:
“Given this input, what are all the reasons this should fail?”
OPA works best when policies are pure functions:
-
No side effects
-
No hidden state
-
Same input → same output
That’s what makes them testable and portable.
Where OPA runs
OPA itself is just a binary or library. It can run:
-
As a sidecar
-
As a Kubernetes admission webhook
-
Inside CI
-
Embedded in Go services
-
As a standalone API
Gatekeeper, Conftest, Styra DAS, and others are wrappers around OPA.
This distinction matters later.
What problems does OPA actually solve?
OPA is powerful, but it’s not magic. Let’s be brutally clear about what it’s good at and what it isn’t.
Centralizing rules without centralizing execution
Before OPA, policy lived in weird places:
-
Bash scripts in CI
-
Hardcoded checks in controllers
-
Wiki pages nobody reads
-
Tribal knowledge
OPA lets you define rules once and evaluate them anywhere.
That’s huge.
It means:
-
The same rule can run in CI and admission
-
You don’t re-implement logic in five languages
-
You can review policy like code
But note the subtlety: policy is centralized, enforcement is not.
This is where many teams go wrong. They try to “centralize enforcement” and end up with a bottleneck. OPA is at its best when teams own execution, but share rules.
Making implicit standards explicit
Most platforms already have rules:
-
“Don’t run privileged containers”
-
“Every service needs an owner”
-
“Prod resources must have limits”
OPA forces you to answer uncomfortable questions:
-
Is this a hard rule or a guideline?
-
Are there exceptions?
-
What does “prod” actually mean?
Writing policy exposes ambiguity. That’s painful but valuable.
In practice, OPA becomes a mirror. If your standards are vague, your policies will be worse.
Shifting left without lying to yourself
OPA in CI is great if you’re honest about what CI can do.
CI policies are:
-
Fast
-
Developer-visible
-
Easy to iterate
But CI doesn’t know:
-
Runtime state
-
Admission-time mutations
-
What actually gets deployed
OPA doesn’t fix that. It just makes the tradeoff explicit.
The win is early feedback, not perfect enforcement.
What OPA does not solve
Let’s kill some myths.
-
It won’t fix bad org communication
-
It won’t make bad rules acceptable
-
It won’t magically align security and developers
-
It won’t replace human judgment
If your policy blocks legitimate work with no escape hatch, people will route around it. Every time.
OPA gives you a tool. Not legitimacy.
Where should policy run: CI vs admission
- This is the most misunderstood part of OPA adoption.
- Most blog posts frame this as a choice. It’s not.
CI: the empathy layer
CI is where policy earns trust.
In CI, policy should:
-
Explain why something is a problem
-
Fail fast
-
Be easy to reproduce locally
-
Prefer warnings early on
Examples that belong in CI:
-
Missing labels
-
Deprecated APIs
-
Insecure defaults
-
Terraform drift risks
CI policies should assume good intent.
If a developer is surprised by a failure, you’ve already lost.
Admission: the seatbelt
Admission control is for invariants. Things that must never happen.
Examples:
-
Privileged pods in prod
-
HostPath mounts
-
Public S3 buckets
-
Cluster-admin bindings
Admission policies should be:
-
Few
-
Boring
-
Extremely well-understood
If a policy ever triggers in prod and people say “wait, what?”, that policy doesn’t belong there.
Why running both actually works
Here’s the pattern I’ve seen succeed:
-
Introduce policies in CI first
-
Let teams trip over them
-
Improve messages, exceptions, ergonomics
-
Collect data: how often do they fail?
-
Only then promote some rules to admission
CI is where you debug policy logic.
Admission is where you lock doors.
Trying to do both at once is how trust dies.
The starter kit architecture
A good OPA setup is boring.
If your policy repo feels “clever”, that’s a smell.
Repo layout that scales
Here’s a layout that has survived real growth:
Key ideas:
-
lib
= reusable helpers
-
policies
actual rules
-
data
configuration, not logic
-
tests
mirrors policies
If helpers grow faster than policies, you’re doing it right.
Workflow that doesn’t fight humans
-
Developers run locally
-
CI runs the same thing
-
Admission runs the same ruleset (subset)
No forks. No “CI version” vs “prod version”.
Policy versioning should be boring Git tags.
How to write testable, maintainable Rego
This is where most OPA efforts fail.
write violations, not allows
Bad pattern:
Why?
-
You can have multiple violations
-
Tests become simpler
-
Messages are explicit
push complexity into helpers
Helpers should:
-
Normalize input
-
Abstract API weirdness
-
Be heavily tested
Policies should read like English.
If your policy logic is hard to explain verbally, rewrite it.
data, not conditionals
- Don’t hardcode environments.
- This lets you change behavior without changing logic a lifesaver during incidents.
Testing Rego without hating life
Tests should answer one question:
“If someone breaks this, will we know why?”
Good tests:
-
Small inputs
-
One violation at a time
-
Explicit messages
Avoid snapshot-style testing. It rots.
Example policies to ship first
If you start with the wrong policies, OPA dies politically.
Start with policies that:
-
Catch real bugs
-
Rarely need exceptions
-
Feel obviously reasonable
Great first policies:
-
Require resource limits
-
Block privileged containers
-
Enforce ownership labels
-
Prevent deprecated APIs
Avoid:
-
Naming conventions
-
Opinionated formatting
-
“Best practices” with no risk
Every policy is a social contract.
Common pitfalls
Over-enforcement too early
Fix
Start in CI. Always.
Rego cleverness
Fix
Optimize for readability, not elegance.
No exception story
Fix
Design exceptions on day one.
Platform-only ownership
Fix
- Let teams contribute policy.
- OPA fails when it becomes “platform says no”.
Gatekeeper vs OPA everywhere: choosing a path
Gatekeeper is OPA + Kubernetes opinionated glue.
Use Gatekeeper if:
-
You only care about Kubernetes
-
You want fast admission integration
-
You accept its CRD model
Use raw OPA / Conftest if:
-
You want CI, Terraform, APIs
-
You want full control
-
You hate YAML CRDs
In practice? Many teams use both.
- Gatekeeper for admission.
- OPA everywhere else.
You Might Be Interested In
- Cloud Egress Control: How To Stop Data Exfiltration Via Outbound Traffic
- Terraform Security Checks: Catching Risky Infrastructure Before Apply
- Secrets Management Comparison: Env Vars Vs Kms Vs Vault When To Use What?
Conclusion
OPA is not hard because it’s complex. It’s hard because it forces decisions teams have been avoiding for years. What is a rule versus a guideline? Who gets to define exceptions? When is it acceptable to block work? Policy-as-code turns these fuzzy questions into executable answers, and that can be uncomfortable.
The teams I’ve seen succeed with OPA don’t start by enforcing everything. They start by listening. They run policies in CI first, they let developers trip over them, and they treat every failure as feedback about the policy itself, not just the workload being evaluated. Over time, the rules that matter prove themselves through repetition and consistency. Only then do they earn the right to block deployments.
Technically, the winning pattern is boring by design: small rules, violations-first logic, helpers instead of clever conditionals, and tests that explain intent. Socially, the winning pattern is transparency. Developers should know what the rules are, why they exist, and how to get unblocked when reality doesn’t fit the model.
If you take one thing away from this guide, let it be this: OPA is not a security tool you “roll out.” It’s a contract you negotiate, encode, test, and continuously refine. When you treat it that way, it becomes one of the most effective pieces of platform infrastructure you can own.
FAQs about Policy-as-code With Opa: A Practical Starter Kit
What’s the difference between OPA and Gatekeeper?
OPA is the core policy engine. It’s just a binary and a language (Rego) that evaluates input against rules and returns decisions. It has no built-in opinion about Kubernetes, CI, Terraform, or anything else. That flexibility is both its strength and its sharp edge you are responsible for wiring it into systems, handling enforcement, surfacing errors, and managing rollout.
Gatekeeper is a Kubernetes-specific implementation built on top of OPA. It gives you admission control, CRDs, constraint templates, and a standard deployment model out of the box. The tradeoff is loss of flexibility. Gatekeeper is great when your policy scope is “Kubernetes admission, full stop.” The moment you want the same policy logic to run in CI, Terraform, or custom tooling, you’ll feel the seams. In practice, many mature setups use Gatekeeper for admission and raw OPA (via Conftest or custom integrations) everywhere else.
Should we run policy only in admission control?
Running policy only in admission control is one of the fastest ways to burn developer trust. Admission is the worst possible place for first contact with a policy: it’s late, it blocks work, and it often happens under time pressure. When developers discover rules only after fails, they experience policy as an obstacle, not a safeguard.
Admission control should be the final enforcement layer, not the teaching layer. Policies should almost always appear in CI first, where developers get fast feedback, clearer context, and the ability to fix issues before deployment. Admission should be reserved for rules that truly must never be violated. If a rule isn’t important enough to justify blocking prod at the worst possible moment, it probably doesn’t belong in admission.
What policies belong in CI vs admission?
CI is where policy can afford to be nuanced. It’s the right place for rules that guide behavior, catch common mistakes, and surface risk early. These are policies where the value comes from feedback, not enforcement. Things like missing labels, unsafe defaults, deprecated APIs, or configuration drift all work well in CI because developers can fix them cheaply and immediately.
Admission is for invariants. These are rules that protect the platform itself, not developer ergonomics. If breaking the rule would put you in incident response mode, it belongs in admission. If breaking the rule mostly results in “someone should clean this up later,” it belongs in CI. Mixing these categories is how teams end up with noisy, brittle admission controllers that everyone hates but nobody dares to remove.
How do we keep policies maintainable long-term?
Long-term maintainability comes down to resisting cleverness. Policies should be small, boring, and explicit. Most complexity should live in shared helper libraries that normalize inputs and hide API quirks. The policy files themselves should read like a checklist of expectations, not like a programming puzzle.
Tests are non-negotiable. If policies don’t have good tests, they will rot quietly until they break something important. Good tests make refactoring safe and give teams confidence to evolve rules as the platform changes. Just as important is social ownership: if policies are written only by a central team and never touched by others, they will drift away from reality. Healthy policy repos accept contributions and feedback from the teams they govern.
How do you actually make Rego testable?
Rego becomes testable when you stop thinking in terms of “allow” and start thinking in terms of “violations.” Policies that produce explicit violation messages are easier to reason about, easier to assert against in tests, and easier to debug when something goes wrong. Instead of testing whether something is allowed, you test that the right violations appear for the right reasons.
Testability also depends on purity. Good Rego policies behave like pure functions: given a specific input and data context, they always produce the same output. That means no hidden assumptions, no reliance on external state, and minimal input shapes in tests. When tests fail, you should be able to understand why by reading the test and the policy side by side, without mentally simulating complex logic. If that’s not true, the policy needs to be simplified.

