Mission 0.4 · Stage 0 — Platform Foundations: The Paved Road on Kubernetes
Guardrails as Code: Kyverno Policies
Goal: The cluster rejects unsafe workloads, fills in sensible defaults, and reports violations, so standards are enforced automatically instead of in review meetings.
By the end of this mission
- Compare Kyverno with OPA Gatekeeper and Pod Security Admission
- Write validate, mutate, and generate policies
- Roll out policies in Audit mode first, then Enforce
- Test policies in CI with the Kyverno CLI
Part 1
Understand it first
Why policy engines
A platform can't review every manifest by hand. Admission controllers (Kubernetes course, admission controllers) intercept every create/update in the API server. A POLICY ENGINE plugs in there and evaluates rules: block privileged containers, require resource requests, require an owner label, allow images only from our registry, and require signed images (DevSecOps course, image signing).
KYVERNO writes policies as Kubernetes YAML: easy for platform teams, with no new language. OPA GATEKEEPER uses Rego, a powerful general-purpose policy language also used outside Kubernetes (Terraform plans, APIs). POD SECURITY ADMISSION (built in) covers the basics of pod security per namespace (restricted, baseline) with no extra install. Many platforms use PSA for the baseline plus Kyverno or Gatekeeper for organisation-specific rules.
Three kinds of rule
VALIDATE: allow or deny (or just report) a resource. MUTATE: change it on the way in (add default labels, set imagePullPolicy, inject a securityContext). GENERATE: create other resources when something happens (every new team namespace gets a default NetworkPolicy, ResourceQuota, and LimitRange). Kyverno also has VERIFYIMAGES rules for Cosign/Notary signatures, and background scans that report violations in existing resources as PolicyReports.
Roll out safely
A new enforce-mode policy can instantly block every deploy of a team that doesn't comply yet. Start with validationFailureAction: Audit (violations are reported, not blocked), publish the report, give teams time (and a golden path that already complies), then switch to Enforce. Exclude system namespaces explicitly. Test policies in CI against sample good and bad manifests.
Part 2
Your project after this mission · 6 files change
- shoplite-platform/
- addons/
- kyverno/
- policies/
- add-default-labels.yamlnew
- namespace-defaults.yamlnew
- require-requests-limits.yamlnew
- restrict-registries.yamlnew
- tests/
- kyverno-test.yamlnew
- kyverno-app.yamlnew
Part 3
Build it, step by step
- 1
Install Kyverno
Chart
kyvernofromhttps://kyverno.github.io/kyverno/, 3 admission controller replicas for HA (if Kyverno is down and its webhook fails closed, nothing can be created). Policies go in a separate Application synced after Kyverno is healthy (sync waves).shoplite-platform/addons/kyverno/kyverno-app.yamlwhole fileyaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: kyverno namespace: argocd annotations: { argocd.argoproj.io/sync-wave: "-5" } spec: project: default source: repoURL: https://kyverno.github.io/kyverno/ chart: kyverno targetRevision: 3.5.1 helm: valuesObject: admissionController: { replicas: 3 } destination: server: https://kubernetes.default.svc namespace: kyverno syncPolicy: automated: { prune: true, selfHeal: true } syncOptions: [CreateNamespace=true, ServerSideApply=true] - 2
Validate: require requests and limits
Every container needs CPU/memory requests and a memory limit, the input Karpenter and the scheduler rely on. Audit mode first.
shoplite-platform/addons/kyverno/policies/require-requests-limits.yamlwhole fileyaml apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-requests-limits annotations: policies.kyverno.io/description: Containers must set CPU/memory requests and a memory limit. spec: validationFailureAction: Audit # → Enforce after teams comply background: true rules: - name: check-resources match: any: [{ resources: { kinds: [Pod] } }] exclude: any: [{ resources: { namespaces: [kube-system, kyverno, argocd] } }] validate: message: "Set resources.requests.cpu, requests.memory and limits.memory on every container." pattern: spec: containers: - resources: requests: { cpu: "?*", memory: "?*" } limits: { memory: "?*" } - 3
Validate: only our registries
Images must come from the company registry (ECR) or a small allow-list. This blocks typo-squatted and unreviewed public images.
shoplite-platform/addons/kyverno/policies/restrict-registries.yamlwhole fileyaml apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: { name: restrict-image-registries } spec: validationFailureAction: Enforce background: true rules: - name: allowed-registries match: any: [{ resources: { kinds: [Pod], namespaceSelector: { matchLabels: { shoplite.dev/team: "?*" } } } }] validate: message: "Images must come from 123456789012.dkr.ecr.ap-south-1.amazonaws.com or public.ecr.aws/shoplite." pattern: spec: containers: - image: "123456789012.dkr.ecr.ap-south-1.amazonaws.com/* | public.ecr.aws/shoplite/*" - 4
Mutate: add defaults
Add a restrictive pod securityContext (
runAsNonRoot, the default seccomp profile) when a team hasn't set one. The+( )anchors only add a field if it's missing, so teams can still override. Mutations make compliance free for teams.shoplite-platform/addons/kyverno/policies/add-default-labels.yamlwhole fileyaml apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: { name: add-defaults } spec: rules: - name: default-security-context match: any: [{ resources: { kinds: [Pod] } }] exclude: any: [{ resources: { namespaces: [kube-system] } }] mutate: patchStrategicMerge: spec: securityContext: +(runAsNonRoot): true +(seccompProfile): { type: RuntimeDefault } - 5
Generate: every team namespace gets defaults
When a namespace labelled
shoplite.dev/teamis created, Kyverno generates a default-deny NetworkPolicy (Kubernetes course, NetworkPolicy) and a ResourceQuota, and keeps them in sync.shoplite-platform/addons/kyverno/policies/namespace-defaults.yamlwhole fileyaml apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: { name: namespace-defaults } spec: rules: - name: default-deny-ingress match: any: [{ resources: { kinds: [Namespace], selector: { matchLabels: { shoplite.dev/team: "?*" } } } }] generate: apiVersion: networking.k8s.io/v1 kind: NetworkPolicy name: default-deny-ingress namespace: "{{request.object.metadata.name}}" synchronize: true data: spec: podSelector: {} policyTypes: [Ingress] - name: quota match: any: [{ resources: { kinds: [Namespace], selector: { matchLabels: { shoplite.dev/team: "?*" } } } }] generate: apiVersion: v1 kind: ResourceQuota name: team-quota namespace: "{{request.object.metadata.name}}" synchronize: true data: spec: hard: { requests.cpu: "20", requests.memory: 40Gi, pods: "100" } - 6
Test policies in CI
The Kyverno CLI applies policies to sample manifests offline. Put this in the platform repo's pipeline so a policy change can't silently block everything.
shoplite-platform/addons/kyverno/tests/kyverno-test.yamlwhole fileyaml apiVersion: cli.kyverno.io/v1alpha1 kind: Test metadata: { name: platform-policies } policies: [../policies/require-requests-limits.yaml, ../policies/restrict-registries.yaml] resources: [good-pod.yaml, bad-pod.yaml] results: - { policy: require-requests-limits, rule: check-resources, resources: [good-pod], result: pass } - { policy: require-requests-limits, rule: check-resources, resources: [bad-pod], result: fail }terminal$ kyverno test shoplite-platform/addons/kyverno/tests/kubectl get policyreport -A -o wide | head -4── expected output ──Test Summary: 2 tests passed and 0 tests failedNAMESPACE NAME KIND NAME PASS FAIL WARN ERROR SKIPcatalog 3f1c... Deployment catalog 4 1 0 0 0checkout 9b2e... Deployment checkout 5 0 0 0 0
Checkpoint — you should now have
- ✓Kyverno runs with 3 replicas and all policies are synced from Git.
- ✓Pods from unapproved registries are rejected with a clear message.
- ✓New team namespaces automatically receive a default-deny NetworkPolicy and a quota.
- ✓Policy tests run in CI.
Part 4
Break it on purpose
Make each change, run the command, and read the error before revealing the diagnosis. Recognising these messages on sight is what makes you fast on a real team. Undo the change afterwards.
Break #1
Enforce on day one
Switch require-requests-limits to Enforce without looking at the audit report first, then deploy.
Break #2
Kyverno down, cluster frozen
Kyverno is running a single replica, and its node is drained.
Part 5
Interview questions from this mission
Kyverno vs OPA Gatekeeper?
How do you roll out a new enforcement policy without breaking teams?