Policy Bots in PRs
Summary
- A policy bot is a required PR check that runs your admission policies against changed manifests before they can merge.
- Insight: running the same rules in the PR that the cluster enforces at admission moves failures left, from a rejected rollout to a red check.
- Key Concepts: conftest/Rego, Kyverno CLI dry-run, changed-file scoping, required status checks, shift-left policy.
- When to Use: any manifest repo where you enforce security or convention rules and want fast, per-PR feedback.
- Limitations: PR-time checks validate manifests, not live cluster state, so keep the in-cluster admission controller as the real enforcement point.
Recipe
Run policy on only the YAML a PR changed, and make the job a required check.
- Detect changed manifest files in the PR.
- Run
conftest test(Rego) orkyverno apply(dry-run) against them. - Fail the job on any violation.
- Mark the job as a required status check in branch protection.
Working Example
A GitHub Actions workflow that scopes to changed YAML and runs both engines.
# .github/workflows/policy.yaml
name: policy
on:
pull_request:
paths: ["overlays/**", "base/**", "platform/**"]
jobs:
policy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: List changed manifests
id: changed
run: |
FILES=$(git diff --name-only origin/${{ github.base_ref }}... \
-- '*.yaml' '*.yml' | tr '\n' ' ')
echo "files=$FILES" >> "$GITHUB_OUTPUT"
- name: conftest
if: steps.changed.outputs.files != ''
run: |
curl -sL -o conftest.tar.gz \
https://github.com/open-policy-agent/conftest/releases/latest/download/conftest_Linux_x86_64.tar.gz
tar xf conftest.tar.gz && sudo mv conftest /usr/local/bin/
conftest test --policy policy/ ${{ steps.changed.outputs.files }}
- name: kyverno dry-run
if: steps.changed.outputs.files != ''
run: |
curl -sL -o kyverno.tar.gz \
https://github.com/kyverno/kyverno/releases/latest/download/kyverno-cli_linux_x86_64.tar.gz
tar xf kyverno.tar.gz && sudo mv kyverno /usr/local/bin/
kyverno apply policy/kyverno/ \
--resource ${{ steps.changed.outputs.files }} \
--warn-exit-code 0 --detailed-resultsA minimal Rego policy that rejects containers running as root.
# policy/security.rego
package main
deny[msg] {
input.kind == "Deployment"
c := input.spec.template.spec.containers[_]
not c.securityContext.runAsNonRoot
msg := sprintf("container %q must set runAsNonRoot: true", [c.name])
}The equivalent Kyverno policy, reused from what the cluster admits.
# policy/kyverno/require-non-root.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-non-root
spec:
validationFailureAction: Enforce
rules:
- name: run-as-non-root
match:
any:
- resources:
kinds: [Deployment]
validate:
message: "containers must set runAsNonRoot: true"
pattern:
spec:
template:
spec:
containers:
- securityContext:
runAsNonRoot: trueDeep Dive
Why scope to changed files
Running policy against the whole repo on every PR is slow and noisy.
It also surfaces pre-existing violations the author did not touch, which stalls unrelated work.
Diffing against the base ref limits the check to what the PR actually changes.
fetch-depth: 0 is required so the runner has the base history to diff against.
conftest versus Kyverno at PR time
Conftest runs Rego and is engine-agnostic; it happily tests any YAML or JSON.
It is a good fit when your policy library is already Rego, or when you gate non-Kubernetes config too.
Kyverno's CLI runs the exact ClusterPolicy objects your admission controller enforces.
Running kyverno apply in the PR is a true dry-run of production admission, so there is no policy drift between CI and cluster.
Many platforms run both: Kyverno for parity with the cluster, conftest for broader repo hygiene.
Making the check enforce
A green or red check does nothing unless branch protection requires it.
Add the job's context name to required_status_checks.contexts on the protected branch.
required_status_checks:
strict: true
contexts: [policy, kubeconform]Now a policy violation blocks merge, and the reconciler never sees the bad manifest.
Rendering before testing
If you use Kustomize or Helm, test the rendered output, not the templates.
Policies match on final Kubernetes objects, so render first, then pipe into the engine.
kubectl kustomize overlays/prod | conftest test --policy policy/ -Gotchas
Testing raw Helm or Kustomize sources gives false results.
The policy sees templates, not the objects the cluster gets; always render first.
git diff with the wrong base misses files or flags everything.
Use origin/${{ github.base_ref }}... (three dots) and fetch-depth: 0 so the merge base is correct.
Warnings that exit non-zero break PRs for advisory rules.
Use --warn-exit-code 0 in Kyverno so Audit-level rules inform without blocking.
Policy in the PR is not a substitute for admission control.
A PR check only sees the repo; enforce the same policies in-cluster so out-of-band kubectl apply is still caught.
Deleted files can crash the run.
Filter the diff to existing paths, or the engine errors on a path that no longer exists.
Alternatives
Use the in-cluster Kyverno admission webhook only.
Simplest to run, but feedback comes at deploy time, not in the PR, which is slower for authors.
Use OPA Gatekeeper with gator test in CI.
Strong if you have standardized on Gatekeeper constraints; gator gives the same PR-time dry-run as Kyverno CLI.
Use a hosted policy platform's PR app.
Less to maintain and nice dashboards, but it adds a vendor and a token with repo access.
Use a pre-commit hook for conftest.
Fastest local feedback, but easily bypassed, so keep the required CI check as the real gate.
FAQs
Does the policy bot need cluster credentials? No. Conftest and kyverno apply run fully offline against files, so no kubeconfig or cluster access is required.
How do I keep CI and cluster policy in sync? Point the Kyverno CLI at the same ClusterPolicy files you deploy to the cluster, ideally from one directory in the repo.
Can I block only some rules and warn on others? Yes. Use validationFailureAction: Enforce for hard rules and Audit for advisory ones, and let warnings exit zero.
What about Docker image policy, like requiring digests? Write a rule that rejects floating tags in image: fields; both Rego and Kyverno pattern matching can enforce digest pinning.
Will this slow down PRs much? Scoped to changed files, the check usually runs in well under a minute, dominated by downloading the CLI binaries.
Related
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).