Stage 7 · Master
Policy as Code & Security Guardrails
Kyverno: Native Kubernetes Policies
Validate, mutate, generate. ClusterPolicy vs Policy. Verify images with cosign. Auto-generate NetworkPolicies, PodDisruptionBudgets.
Why Kyverno?
Kyverno is a Kubernetes-native policy engine. Unlike OPA (which uses Rego), Kyverno policies are written as Kubernetes resources (YAML) using familiar K8s patterns. No new language to learn. Runs as admission controller + background controller.
| Aspect | OPA/Gatekeeper | Kyverno |
|---|---|---|
| Language | Rego (custom) | YAML (Kubernetes native) |
| Learning Curve | Steeper (new language) | Lower (K8s patterns) |
| Policy Types | Validate only | Validate, Mutate, Generate |
| Background Scans | Audit only | Audit + auto-remediate |
| Image Verification | External (cosign, notary) | Built-in (cosign, sigstore) |
| Generators | No | Yes (generate resources) |
| CLI Testing | opa test | kyverno test |
| Ecosystem | Mature, CNCF Graduated | Growing, CNCF Incubating |
OPA for complex logic (loops, recursion, external data). Kyverno for K8s-native patterns (validate, mutate, generate, image verification). Many orgs use both.
Policy Types: Validate, Mutate, Generate
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels
spec:
validationFailureAction: Enforce
background: true
rules:
- name: check-labels
match:
any:
- resources:
kinds: ["Deployment", "StatefulSet", "DaemonSet", "Service", "ConfigMap", "Secret"]
validate:
message: "Missing required labels: team, environment, platform.example.com/managed-by"
pattern:
metadata:
labels:
team: "?*"
environment: "?*"
platform.example.com/managed-by: "?*"
Validate policy: blocks resource creation if labels missing. pattern uses ?* for non-empty string.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-default-labels
spec:
rules:
- name: add-labels
match:
any:
- resources:
kinds: ["Deployment", "StatefulSet", "Pod"]
mutate:
targets:
- apiVersion: apps/v1
kind: Deployment
patchStrategicMerge:
metadata:
labels:
platform.example.com/created-by: "kyverno"
platform.example.com/created-at: "{{ time_now }}"
spec:
template:
metadata:
labels:
platform.example.com/created-by: "kyverno"
# Also works for annotations
# mutate:
# targets:
# - apiVersion: v1
# kind: Pod
# patchStrategicMerge:
# metadata:
# annotations:
# platform.example.com/injected: "true"
Mutate policy: adds labels/annotations at admission time. Uses patchStrategicMerge for strategic merge patch.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: generate-networkpolicy
spec:
rules:
- name: generate-networkpolicy
match:
any:
- resources:
kinds: ["Namespace"]
selector:
matchLabels:
platform.example.com/generate-networkpolicy: "true"
generate:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
name: "default-deny-{{request.object.metadata.name}}"
namespace: "{{request.object.metadata.name}}"
synchronize: true
data:
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: "{{request.object.metadata.name}}"
ports:
- protocol: TCP
port: 443
- protocol: TCP
port: 8080
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Generate policy: creates NetworkPolicy in each namespace labeled for generation. synchronize: true keeps it in sync.
ClusterPolicy vs Policy
| Aspect | ClusterPolicy | Policy (Namespaced) |
|---|---|---|
| Scope | Cluster-wide | Single namespace |
| CRD | ClusterPolicy | Policy |
| Match | Any resource in any namespace | Resources in same namespace only |
| Generate | Can generate in any namespace | Can only generate in same namespace |
| Permissions | Requires cluster-admin to create | Requires namespace admin |
| Use Case | Platform-wide standards (security, labeling) | Team-specific overrides, exceptions |
Platform team owns ClusterPolicies. Teams can create namespaced Policies for exceptions (with approval). ClusterPolicy wins on conflicts unless namespaced Policy has higher priority.
Image Verification with Cosign
Kyverno has built-in image verification using cosign/sigstore. Verifies signatures, attestations (SBOM, SLSA), and certificate transparency logs. No external admission controller needed.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
background: true
rules:
- name: verify-cosign-signature
match:
any:
- resources:
kinds: ["Pod", "Deployment", "StatefulSet", "DaemonSet", "Job", "CronJob"]
verifyImages:
- image: "*"
key: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
# Or use keyless (OIDC identity)
# attestations:
# - predicateType: https://slsa.dev/provenance/v1
# - predicateType: https://spdx.dev/Document
# Skip verification for specific namespaces/images
# exclude:
# namespaces: ["kube-system", "monitoring"]
# images: ["ghcr.io/myorg/*:dev-*"]
Verify images signed with cosign. Supports keyless (OIDC identity) and key-based. Can require SLSA provenance attestations.
Auto-Generate NetworkPolicies, PDBs
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: generate-pdb
spec:
rules:
- name: generate-pdb
match:
any:
- resources:
kinds: ["Deployment", "StatefulSet"]
selector:
matchLabels:
platform.example.com/ha: "true"
generate:
apiVersion: policy/v1
kind: PodDisruptionBudget
name: "{{request.object.metadata.name}}-pdb"
namespace: "{{request.object.metadata.namespace}}"
synchronize: true
data:
spec:
minAvailable: 50%
selector:
matchLabels:
{{request.object.spec.selector.matchLabels}}
Auto-generates PDB for workloads labeled for HA. minAvailable: 50% ensures quorum during voluntary disruptions.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: generate-servicemonitor
spec:
rules:
- name: generate-servicemonitor
match:
any:
- resources:
kinds: ["Service"]
selector:
matchLabels:
platform.example.com/monitor: "true"
generate:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
name: "{{request.object.metadata.name}}-monitor"
namespace: "{{request.object.metadata.namespace}}"
synchronize: true
data:
spec:
selector:
matchLabels:
{{request.object.spec.selector}}
endpoints:
- port: http
path: /metrics
interval: 30s
namespaceSelector:
matchNames:
- "{{request.object.metadata.namespace}}"
Auto-generates ServiceMonitor for Services labeled for monitoring. Eliminates manual ServiceMonitor creation.
Testing Kyverno Policies
# Install kyverno CLI
brew install kyverno
# Test policy against resource
kyverno apply policy.yaml --resource=deployment.yaml
# Test with multiple resources
kyverno apply policy.yaml --resources-dir=test-resources/
# Dry run (no changes)
kyverno apply policy.yaml --resource=deployment.yaml --dry-run
# Test generate policies
kyverno apply policy.yaml --resource=namespace.yaml --generate
# Validate policy syntax
kyverno validate policy.yaml
# Run unit tests (kyverno test)
kyverno test policy.yaml --test=test.yaml
Kyverno CLI for local testing. Supports validate, mutate, generate. Dry-run mode for CI.
# test/require-labels-test.yaml
apiVersion: kyverno.io/v1alpha1
kind: Test
metadata:
name: require-labels-test
policies:
- require-labels.yaml
tests:
- name: "deployment with required labels passes"
resources:
- deployment-with-labels.yaml
results:
- kind: Deployment
name: test-deployment
namespace: default
policy: require-labels
rule: check-labels
result: pass
- name: "deployment without labels fails"
resources:
- deployment-without-labels.yaml
results:
- kind: Deployment
name: test-deployment
namespace: default
policy: require-labels
rule: check-labels
result: fail
message: "Missing required labels: team, environment, platform.example.com/managed-by"
Kyverno test format: defines test cases with expected pass/fail results. Run with kyverno test.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.