Policy as Code - Admission Policies and CI Manifest Checks

Status: Active
Last Updated: 2026-08-26
Category: GitOps - Governance
Prerequisites: gitops, k0s-deployments
Time: 3 hours
Tags: policy-as-code, kyverno, gatekeeper, opa, conftest, admission, compliance

Summary

Enforce guardrails as machine-checked rules instead of tribal knowledge: in-cluster admission policies with Kyverno or Gatekeeper, and pre-merge manifest checks with Conftest/OPA in the Forgejo pipeline. Includes a policy repo layout that keeps policies versioned, tested, and reviewed like everything else under gitops.

๐ŸŽฏ What You'll Learn

By the end of this article, you'll be able to:


Table of Contents

  1. Context / Why This Matters
  2. Two Enforcement Layers
  3. Kyverno / Gatekeeper Overview
  4. Conftest and OPA in CI
  5. Policy Repo Layout

Context / Why This Matters

gitops already ensures only merged commits reach the cluster โ€” but nothing yet says what those commits are allowed to contain. A squash-merged MR can still ship a container running as root, a plaintext secret, or an ingress exposing an internal service. Policy as code closes that gap: every guardrail becomes a versioned rule with a test, a review trail, and an owner.

This builds on RBAC concepts from rbac-basics (who may act) by adding constraints (what any actor may deploy), and complements secrets handling in secrets.


Two Enforcement Layers

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Layer 1: PRE-MERGE (CI)          fast, cheap, friendly โ”‚
โ”‚   conftest verify + conftest test on rendered output  โ”‚
โ”‚   Blocks the MR; developer fixes before review.       โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Layer 2: ADMISSION (cluster)     authoritative, final  โ”‚
โ”‚   Kyverno/Gatekeeper validates every request live     โ”‚
โ”‚   Catches bypasses: kubectl apply, API access, bugs.  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Rule of thumb: CI is for developer experience, admission is for guarantees. The same rules should exist in both layers so feedback is consistent โ€” write once, run twice.


Kyverno / Gatekeeper Overview

Kyverno Gatekeeper (OPA)
Language YAML policies Rego
Learning curve Low Moderate-high
Mutating rules Built-in (mutate) Via modifying admission policies
Best fit Teams wanting quick wins Orgs standardizing on OPA/Rego elsewhere

For fogserv.cloud's scale, Kyverno's YAML-native policies are the pragmatic default; keep Gatekeeper in mind if OPA adoption spreads to other layers (e.g., conftest already uses Rego).

Example Kyverno policies

# require-signed-images: only pull from our Harbor registry
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-registries
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-registry
      match:
        any:
          - resources:
              kinds: [Pod]
      validate:
        message: "Images must come from harbor.fogserv.cloud"
        pattern:
          spec:
            containers:
              - image: "harbor.fogserv.cloud/*"
---
# disallow-latest-tag
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-latest-tag
spec:
  validationFailureAction: Enforce
  rules:
    - name: require-image-tag
      match:
        any:
          - resources:
              kinds: [Pod]
      validate:
        message: "Pin an explicit tag; :latest is not allowed."
        pattern:
          spec:
            containers:
              - image: "*:*"

Apply and audit:

kubectl apply -f policies/
kyverno apply policies/restrict-registries.yaml --resource test-pod.yaml  # offline test
kubectl polkit describe clusterpolicy disallow-latest-tag                  # status

Start in Audit mode (validationFailureAction: Audit), read violation reports for a week, then flip to Enforce. Enforcing on day one produces outages, not security.


Conftest and OPA in CI

Conftest runs Rego policies against structured files โ€” perfect for rendered manifests inside a Forgejo Action, per the pipeline model in cicd-concepts.

Policy example (Rego)

package main.deny_plaintext_secrets

deny[msg] {
  input.kind == "Secret"
  msg := sprintf("%s/%s: raw Secret manifest committed to git", [input.metadata.namespace, input.metadata.name])
}

deny[msg] {
  input.kind == "Deployment"
  c := input.spec.template.spec.containers[_]
  c.securityContext.runAsRoot == true
  msg := sprintf("%s: container %s must set runAsRoot=false", [input.metadata.name, c.name])
}

Pipeline step

# .forgejo/workflows/lint-manifests.yml (excerpt)
jobs:
  policy:
    steps:
      - uses: actions/checkout@v4
      - name: Render manifests
        run: kustomize build clusters/prod > rendered.yaml
      - name: Unit-test the policies themselves
        run: conftest verify --policy policy/
      - name: Check rendered output
        run: conftest test rendered.yaml --policy policy/ --fail-on-warn

Note the second check: policies are code too โ€” they get tests (infrastructure-testing) and reviews like everything else.


Policy Repo Layout

Keep policies in their own directory of the GitOps repo (or a dedicated repo consumed as a Flux source):

policy/
โ”œโ”€โ”€ README.md                 # each policy: intent, owner, enforcement mode
โ”œโ”€โ”€ conftest/                 # CI-layer rules (Rego)
โ”‚   โ”œโ”€โ”€ deny_plaintext_secrets.rego
โ”‚   โ”œโ”€โ”€ deny_latest_tag.rego
โ”‚   โ””โ”€โ”€ *_test.rego           # conftest verify unit tests
โ”œโ”€โ”€ kyverno/                  # admission layer (ClusterPolicies)
โ”‚   โ”œโ”€โ”€ restrict-registries.yaml
โ”‚   โ”œโ”€โ”€ disallow-latest-tag.yaml
โ”‚   โ””โ”€โ”€ require-resource-limits.yaml
โ””โ”€โ”€ test-fixtures/
    โ”œโ”€โ”€ good/deployment.yaml  # must pass all policies
    โ””โ”€โ”€ bad/root-pod.yaml     # must fail specific policies

Conventions:


Practical Examples

Example 1: Blocking a plaintext Secret pre-merge

Developer commits secret.yaml containing stringData: PASSWORD=.... CI runs conftest, which denies with "raw Secret manifest committed to git". Developer switches to a Dotenvx/vault-injected secret per secrets; the violation never reaches a reviewer, let alone the cluster.

Example 2: Catching a CI/admission divergence

Someone applies via a stale local kubeconfig, bypassing CI. Kyverno admission rejects the Pod (restrict-registries). Because both layers share the same rule intent, behavior is identical either way โ€” the bypass attempt appears in Kyverno's audit log and gets triaged per drift-detection-runbook.


Troubleshooting & Common Pitfalls

Problem Cause Fix
Legitimate deploys blocked after enabling Enforce Policy written against idealized manifests, never audited Always start Audit mode; export violations, fix owners, then enforce
CI green but admission rejects Rules drifted between conftest and Kyverno versions Generate one from the other or add a parity test to CI
Everything passes because render step failed silently Empty rendered.yaml โ†’ zero documents to test Fail the job if render output is empty: [ -s rendered.yaml ]
Policy exceptions sprawl Ad-hoc namespace exclusions added under pressure Time-box exclusions with expiry comments; review quarterly
Rego policy silently matches nothing Typo in kind/path; no test coverage Require conftest verify fixtures per policy

Next Steps / Ops Actions

Sources & Related

External references consulted:

Related knowledge-base articles:

Change Log

2026-08-26

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse