Kubernetes Policy Enforcement: Shift Left Before Admission
The Problem With Catching Violations at the Gate
Imagine shipping code and only finding out it doesn’t compile after it’s already been deployed to staging. That’s essentially what happens when Kubernetes policy enforcement lives exclusively at the admission-webhook layer.
Admission controllers—whether powered by OPA Gatekeeper or Kyverno—are powerful. But by the time a manifest reaches the API server, a developer has already written it, a PR has been opened, a pipeline has run, and someone has typed kubectl apply. A rejection at that point is expensive: it breaks deployments, frustrates engineers, and erodes trust in the platform team.
The fix isn’t better admission webhooks. It’s moving policy enforcement left—into the developer’s workflow, the CI pipeline, and the PR gate—so violations surface in seconds, not after a deploy.
What “Shift Left” Actually Means for Policy
Shift left isn’t a tool; it’s a layered strategy. Think of it as a series of fences, each catching different classes of violations:
- IDE / local lint — catch obvious mistakes before a file is even committed
- Pre-commit hooks — block bad manifests from entering the repo
- CI pipeline — validate every manifest on every PR, with structured output
- PR gate — fail the merge check if policy tests don’t pass
- Admission webhook — last line of defense; rarely fires if the above are working
Each fence has a different cost-of-failure curve. The further left the catch, the cheaper the fix.
Tools for the Left Side of the Pipeline
Conftest (OPA / Rego)
Conftest lets you write policy-as-code in Rego and evaluate it against any structured config file—Kubernetes YAML, Helm output, Terraform plans, Dockerfile.
# Install
brew install conftest
# Evaluate a manifest against local policies
conftest test deployment.yaml --policy ./policies/
A simple policy that forbids containers running as root:
package main
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
not container.securityContext.runAsNonRoot
msg := sprintf("container '%v' must set runAsNonRoot: true", [container.name])
}
This is the same Rego you’d use in OPA Gatekeeper—meaning your CI policies and your admission policies can share a single source of truth.
Kyverno CLI
If your cluster runs Kyverno, its CLI lets you evaluate the exact same ClusterPolicy resources locally:
# Install
brew install kyverno
# Apply a policy file against a manifest
kyverno apply ./policies/disallow-root.yaml --resource deployment.yaml
No need to maintain separate policy formats for CI vs. the cluster. Write the policy once, run it everywhere.
kubeconform for Schema Validation
Before policy checks, validate that manifests are structurally sound. kubeconform is a fast schema validator that supports CRDs:
kubeconform -strict -kubernetes-version 1.29.0 ./manifests/
This catches typos, missing required fields, and version drift before any policy check runs.
Wiring It Into CI
Here’s a minimal GitHub Actions job that runs schema validation and policy checks on every PR:
name: Validate Manifests
on: [pull_request]
jobs:
policy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Schema validation
uses: docker://ghcr.io/yannh/kubeconform:latest
with:
args: "-strict -kubernetes-version 1.29.0 ./manifests/"
- name: Install conftest
run: |
wget https://github.com/open-policy-agent/conftest/releases/download/v0.51.0/conftest_0.51.0_Linux_x86_64.tar.gz
tar xzf conftest_*.tar.gz && mv conftest /usr/local/bin/
- name: Policy checks
run: conftest test ./manifests/ --policy ./policies/ --output github
The --output github flag formats violations as inline PR annotations—reviewers see exactly which line in which file triggered which policy, without leaving the PR view.
The Admission Webhook Is Still Worth Having
Shifting left doesn’t mean removing admission controllers. It changes their role: from the only enforcement point to the last enforcement point.
That distinction matters operationally. A webhook that fires frequently is a source of incidents—it blocks legitimate deployments, creates alert fatigue, and gets disabled by frustrated teams. A webhook that fires rarely means your upstream gates are working. It becomes a safety net for the cases that genuinely slip through: manual kubectl apply commands, automated scripts that bypass the pipeline, or third-party operators deploying their own resources.
In practice, this also makes webhook policy simpler to write. You can keep the admission layer focused on hard-blocking rules (no privileged: true, no hostNetwork, mandatory label schemas) and handle advisory or informational checks earlier in the pipeline where failures are cheap.
A Practical Rollout Strategy
Rolling this out on an existing cluster without disruption:
- Audit first. Run
conftestorkyverno applyagainst your existing manifests in dry-run mode. See what would fail before you enforce anything. - Start with
warnmode. Both Kyverno and Gatekeeper supportwarnseverity—violations are surfaced but not blocking. Use this to build awareness before enforcement. - Add CI checks in report-only mode. Set the CI step to always pass but emit structured output for two or three sprints. Let developers get used to seeing violations before you break builds.
- Flip to hard-fail gradually. Start with the highest-severity policies (root containers, privileged pods) and work down.
- Share policies between CI and the cluster. If you’re using Kyverno, keep your
ClusterPolicyYAMLs in the same repo as your other config. CI useskyverno applyagainst them; the cluster reconciles them via GitOps.
The Broader Lesson
Admission webhooks solved a real problem when they were introduced—giving cluster operators a programmable gate at the Kubernetes API boundary. But a gate that only catches violations after a developer has written, committed, reviewed, and attempted to deploy a manifest is a very expensive place to learn.
The tools to move that feedback left—Conftest, the Kyverno CLI, kubeconform—are mature, free, and easy to add to any CI pipeline. The admission controller doesn’t go away; it just stops being your first line of defense and becomes your last.
That’s a much better place for it to be.