enforce compliance rules on every Kubernetes API request

Policy as Code in Kubernetes: enforcing compliance and security with OPA Gatekeeper and Kyverno

14 min read

Wiki pages do not block a deployment. Policy as Code turns security and compliance rules into version-controlled admission checks that run on every create and update—before workloads reach production.

Why documented rules fail without admission enforcement

Production clusters accumulate requirements that are easy to state and hard to apply consistently: no root containers, approved image registries only, resource requests on every pod, mandatory labels for cost allocation, default-deny networking, and no secrets in ConfigMaps. Teams capture these in wikis and Slack threads, but documentation does not reject a bad manifest. A Helm chart that runs as root, pulls from an untrusted registry, and omits limits still deploys until someone notices during an incident review. Manual review of every pull request does not scale. Custom admission webhooks rot without tests and owners. Policy as Code stores rules in Git, evaluates them on every API server CREATE and UPDATE through validating and mutating admission webhooks, and gives platform teams preventive enforcement instead of post-mortem compliance.

OPA Gatekeeper versus Kyverno: two engines, one admission path

Both tools sit in the admission chain, evaluate incoming resources, then accept, deny, or mutate before persistence, and can audit existing objects in report-only mode. OPA Gatekeeper wraps Open Policy Agent and Rego—powerful for cross-cutting logic, external data, and organizations that already standardize on OPA outside Kubernetes. Policies ship as ConstraintTemplate CRDs plus Constraint instances with parameters. Kyverno writes policies in YAML with Kubernetes-native match, validate, mutate, generate, and verifyImages rules—lower learning curve and first-class mutation without Rego. Gatekeeper mutation is narrower—Assign and ModifySet mutators cover many defaults but Kyverno mutation is richer for pod defaults. Gatekeeper is CNCF graduated with a large policy library; Kyverno is CNCF incubating and growing fast. Start with Kyverno when teams want YAML-only policies and built-in mutation. Choose Gatekeeper when compliance logic spans multiple systems or needs Rego expressiveness. Many estates run Kyverno for workload guardrails and Gatekeeper for specialized Rego constraints—pick one primary engine per concern to avoid conflicting webhooks.

Bash · install Kyverno and Gatekeeper with Helm
helm repo add kyverno https://kyverno.github.io/kyverno/
helm upgrade --install kyverno kyverno/kyverno \
  --namespace kyverno --create-namespace

helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm upgrade --install gatekeeper gatekeeper/gatekeeper \
  --namespace gatekeeper-system --create-namespace \
  --set enableExternalData=false

Kyverno: block root containers and auto-inject security defaults

Use a deny rule that catches any container or initContainer with runAsUser zero instead of a brittle pattern that misses partial securityContext blocks. Test rejected and accepted pods with kubectl before enabling Enforce cluster-wide. Mutation policies can add runAsNonRoot, drop capabilities, and readOnlyRootFilesystem when developers omit securityContext—pair mutation with validation so teams cannot override back to privileged settings. Exclude kube-system and the policy engine namespace from enforcement.

YAML · Kyverno ClusterPolicy disallow root UID
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-root-containers
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: disallow-root-uid
      match:
        any:
          - resources:
              kinds: [Pod]
      exclude:
        any:
          - resources:
              namespaces:
                - kube-system
                - kyverno
      validate:
        message: "Containers must not run as root (UID 0)."
        deny:
          conditions:
            any:
              - key: "{{ request.object.spec.containers[?securityContext.runAsUser == `0`] | length(@) }}"
                operator: GreaterThan
                value: 0
              - key: "{{ request.object.spec.initContainers[?securityContext.runAsUser == `0`] | length(@) }}"
                operator: GreaterThan
                value: 0
Bash · verify rejection and acceptance
# Rejected — runs as root
kubectl run nginx-root --image=nginx:1.27 --dry-run=server -o yaml \
  --overrides='{"spec":{"containers":[{"name":"nginx-root","image":"nginx:1.27","securityContext":{"runAsUser":0}}]}}' \
  | kubectl apply -f -

# Accepted — non-root UID
kubectl run nginx-safe --image=nginx:1.27 --dry-run=server -o yaml \
  --overrides='{"spec":{"containers":[{"name":"nginx-safe","image":"nginx:1.27","securityContext":{"runAsNonRoot":true,"runAsUser":1000}}]}}' \
  | kubectl apply -f -
YAML · Kyverno mutate policy for default securityContext
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-default-security-context
spec:
  rules:
    - name: add-run-as-non-root
      match:
        any:
          - resources:
              kinds: [Pod]
      mutate:
        patchStrategicMerge:
          spec:
            securityContext:
              +(runAsNonRoot): true
              +(runAsUser): 1000
            containers:
              - (name): "*"
                securityContext:
                  +(allowPrivilegeEscalation): false
                  +(readOnlyRootFilesystem): true
                  +(capabilities):
                    drop:
                      - ALL

OPA Gatekeeper: allow only trusted image registries

Define Rego once in a ConstraintTemplate, then bind parameters per environment through Constraint CRDs. Match production and staging namespaces first. Use startswith against registry prefixes and include initContainers in the template. Gatekeeper supports dryrun enforcement for audit-only rollout—fix violations in CI and manifests before switching enforcementAction to deny.

YAML · Gatekeeper ConstraintTemplate for allowed registries
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedregistries
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRegistries
      validation:
        openAPIV3Schema:
          type: object
          properties:
            registries:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedregistries

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not image_allowed(container.image)
          msg := sprintf("container '%v' image '%v' not in allowed registries", [container.name, container.image])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.initContainers[_]
          not image_allowed(container.image)
          msg := sprintf("initContainer '%v' image '%v' not in allowed registries", [container.name, container.image])
        }

        image_allowed(image) {
          prefix := input.parameters.registries[_]
          startswith(image, prefix)
        }
YAML · Gatekeeper Constraint instance
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRegistries
metadata:
  name: allow-only-trusted-registries
spec:
  enforcementAction: dryrun
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    namespaces:
      - production
      - staging
  parameters:
    registries:
      - "registry.company.com/"
      - "gcr.io/my-project/"
      - "docker.io/library/"

Audit existing workloads before enforce mode

Never flip production to deny on day one. Kyverno validationFailureAction Audit and Gatekeeper enforcementAction dryrun report violations without blocking creates. Kyverno writes PolicyReport and ClusterPolicyReport objects; Gatekeeper surfaces violations on Constraint status and through audit sync. Review reports for a full sprint, fix manifests and Helm charts in CI, then promote policies to Enforce. Namespace exclusions for kube-system, cert-manager, and the policy controllers themselves are mandatory.

YAML · audit-only settings
# Kyverno
spec:
  validationFailureAction: Audit

# Gatekeeper Constraint
spec:
  enforcementAction: dryrun
Bash · inspect policy reports
# Kyverno policy reports
kubectl get policyreport,clusterpolicyreport -A

# Gatekeeper constraint violations
kubectl get k8sallowedregistries.constraints.gatekeeper.sh \
  allow-only-trusted-registries \
  -o jsonpath='{range .status.violations[*]}{.namespace}/{.name}: {.message}{"\n"}{end}'

Operational practices: GitOps, CI tests, tiers, and monitoring

Store policies beside cluster config in Git and sync with Argo CD or Flux—the same review flow as application code. Test policies in CI with kyverno test fixtures and gator verify for Gatekeeper manifests before merge. Layer policies by criticality: enforce root, privileged, and registry rules first; add resource limits and label requirements after audit; keep naming conventions in report-only mode. Cap initial rollout at five to ten policies to avoid developer gridlock. Exclude DaemonSets and platform labels such as kube-dns from broad pod rules. Alert on rising Kyverno fail results or Gatekeeper audit violations the same way you alert on error rate spikes. Policy as Code closes the gap between written requirements and what the API server actually allows—start in audit, version everything, iterate weekly.

YAML · Argo CD Application for cluster policies
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cluster-policies
  namespace: argocd
spec:
  project: platform
  source:
    repoURL: https://github.com/company/k8s-policies.git
    targetRevision: main
    path: policies/
  destination:
    server: https://kubernetes.default.svc
    namespace: kyverno
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
Bash · test policies in CI
# Kyverno policy tests
kyverno test ./policy-tests/ --detailed-results

# Gatekeeper manifest verification
gator verify ./gatekeeper/policies/ --kube-version 1.29
YAML · Prometheus alert for Kyverno failures
groups:
  - name: policy-admission
    rules:
      - alert: KyvernoPolicyFailures
        expr: sum(rate(kyverno_policy_results_total{rule_result="fail"}[5m])) > 5
        for: 15m
        annotations:
          summary: "Sustained Kyverno policy failures in admission"

Admission policies extend the baseline from our Kubernetes security hardening guide for production clusters.

Image signature verification policies pair naturally with controls from our software supply chain security guide.