Kubernetes Policy Enforcement: Shift Left Before Admission

Kubernetes
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:

  1. IDE / local lint — catch obvious mistakes before a file is even committed
  2. Pre-commit hooks — block bad manifests from entering the repo
  3. CI pipeline — validate every manifest on every PR, with structured output
  4. PR gate — fail the merge check if policy tests don’t pass
  5. 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:

  1. Audit first. Run conftest or kyverno apply against your existing manifests in dry-run mode. See what would fail before you enforce anything.
  2. Start with warn mode. Both Kyverno and Gatekeeper support warn severity—violations are surfaced but not blocking. Use this to build awareness before enforcement.
  3. 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.
  4. Flip to hard-fail gradually. Start with the highest-severity policies (root containers, privileged pods) and work down.
  5. Share policies between CI and the cluster. If you’re using Kyverno, keep your ClusterPolicy YAMLs in the same repo as your other config. CI uses kyverno apply against 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.

Sources