Policy as code is the right idea. A guardrail that depends on a person remembering is a suggestion, and suggestions do not survive a deadline.
The difficulty is entirely in the rollout. Most failed attempts we have seen were not technically wrong; they were introduced in a way that made the policy engine an adversary.
Run in audit mode for longer than feels necessary
Every policy should spend time reporting what it would have blocked before it blocks anything. This is not caution for its own sake. It tells you the actual violation rate, which is almost always higher than anyone expects.
Turning enforcement on with a violation rate you have not measured means blocking a large share of deployments on the first day. You will be asked to turn it off, and you will not get a second attempt for a long time.
The rejection message is the whole interface
For most engineers, the only contact they ever have with your policy system is the moment it refuses something. That message is the product.
A rejection that names a constraint but not a fix is an obstacle. One that names the fix is a linter.
A message saying a resource violates a security policy is useless. A message saying the container must set runAsNonRoot, here is the field, here is why, and here is who to ask if that is impossible, gets fixed in two minutes and nobody complains.
Fail earlier than admission
Admission control is the enforcement point, but it should rarely be where a developer first learns about a problem. By then they are deploying, which is late.
Running the same policies in the pull request means violations surface while the change is being written. Admission control then becomes a backstop that catches the cases that bypassed the pipeline, rather than the primary feedback mechanism. Same rules, evaluated twice, in very different emotional contexts.
Exceptions must exist and must expire
There will be legitimate cases a policy cannot accommodate. If there is no exception path, teams will find a technical route around the policy instead, and that route will be invisible to you.
What worked: exceptions requested in code, reviewed like any change, granted with an expiry, and reported on. An exception that never expires becomes permanent by neglect, and after two years the policy describes an intention rather than a state.
Keep the policy set small enough to reason about
Every policy is a rule someone must understand to debug a failure. A large set becomes a system with emergent behaviour, where two policies interact to reject something neither intended, and diagnosing that costs hours.
We were better served by a small number of policies that were universally enforced and universally understood than by comprehensive coverage that people navigated by trial and error.
The measure that matters
Not how many policies you enforce, and not how many violations you block. It is whether the violation rate is falling, which tells you people are building compliant things by default rather than being stopped from building non-compliant ones.
A steady block rate over months is not enforcement working. It is a queue of frustrated engineers who have not changed what they build, and some of them are already looking for a way around.



