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:
- โ Explain the shift-left vs admission-control split
- โ Write Kyverno ClusterPolicies for common guardrails
- โ
Run
conftestagainst manifests in CI before merge - โ Structure a policy repository with tests and docs
- โ Handle policy violations without blocking legitimate emergencies
Table of Contents
- Context / Why This Matters
- Two Enforcement Layers
- Kyverno / Gatekeeper Overview
- Conftest and OPA in CI
- 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:
- One policy = one file = one documented intent = one owner.
- Every policy has at least one
goodand onebadfixture, wired into CI so a broken policy fails the build. - Changes to
Enforce-mode policies follow the same MR discipline as prod manifests โ they are prod config (merge-strategies). - Exceptions live in code (excluded namespaces annotated in the policy), never as verbal agreements.
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
- Enable Kyverno in Audit mode on staging this week; review violations at next ops sync.
- Wire conftest into the manifest lint workflow alongside render checks (cicd-concepts).
- Track violations alongside drift metrics per drift-detection-runbook.
- Review who can change policies themselves via rbac-basics.
Sources & Related
External references consulted:
- https://kyverno.io/docs/
- https://www.openpolicyagent.org/docs/latest/
- https://www.conftest.dev/
- https://open-policy-agent.github.io/gatekeeper/website/docs/
Related knowledge-base articles:
- gitops โ pipeline these policies integrate into
- rbac-basics โ identity-side authorization
- secrets โ where secrets belong instead of manifests
- infrastructure-testing โ testing philosophy for policy fixtures
- k0s-deployments โ workloads the policies govern
Change Log
2026-08-26
- Initial creation: two-layer enforcement model, Kyverno/Gatekeeper comparison, conftest CI integration, policy repo layout.