Why Admission Control Matters
You've signed your images with Cosign, generated SBOMs, built from verified source, and deployed to a hardened cluster. But without admission control, anyone with kubectl access can deploy an unsigned, unverified image and bypass everything. Admission controllers are the enforcement point. They intercept every pod creation request and reject pods that don't meet your security policies. Without them, your entire supply chain security posture is advisory, not enforced.
Ready-to-use Kyverno policies specifically designed for CleanStart deployments are provided below.
The following diagram illustrates the Kubernetes admission webhook flow for policy enforcement:
graph TD A["Pod Creation<br/>Request<br/>kubectl apply"] -->|Send| B["Kyverno<br/>Admission<br/>Webhook"] B -->|Intercept| C["Validate<br/>Against<br/>Policies"] C -->|Check 1| D["Registry<br/>Policy"] D -->|Verify| D1["Image from<br/>registry.cleanstart.com?"] C -->|Check 2| E["Signature<br/>Policy"] E -->|Verify| E1["Image<br/>Cosign Signed?"] C -->|Check 3| F["SBOM<br/>Policy"] F -->|Verify| F1["SBOM<br/>Attestation<br/>Present?"] C -->|Check 4| G["CVE<br/>Policy"] G -->|Verify| G1["No Critical<br/>CVEs?"] D1 -->|Result| H{All<br/>Policies<br/>Pass?} E1 --> H F1 --> H G1 --> H H -->|Reject| I["Request<br/>Denied<br/>Error Message"] I -->|User| J["Fix Non-Compliant<br/>Image"] J -->|Retry| A H -->|Approve| K["Mutate<br/>Request"] K -->|Add Labels| L["Inject Security<br/>Labels"] K -->|Add Sidecar| M["Add Security<br/>Sidecar<br/>Optional"] L -->|Allow| N["Pod<br/>Created"] M -->|Allow| N N -->|Deploy| O["Secure Pod<br/>Running"] style B fill:#99ccff style C fill:#99ccff style H fill:#ffff99 style I fill:#ffcccc style K fill:#ccffcc style N fill:#99ff99Why Kyverno
Kyverno is a Kubernetes-native policy engine that operates fundamentally differently from OPA/Gatekeeper. Instead of using Rego, a specialized policy language that requires separate training and maintains a learning curve, Kyverno policies are written as Kubernetes resources in YAML. This means your team uses the exact same language already familiar from managing manifests, configuring Helm values, and setting up CI/CD pipelines. The cognitive load of adopting a new policy language is eliminated entirely, and policies can be managed with the same GitOps workflows and version control practices your infrastructure team already employs.
For teams that prefer OPA and already use it extensively elsewhere in their infrastructure, a detailed comparison is available at OPA Gatekeeper Policies. The choice between these two engines depends on your organizational context. Kyverno is the natural choice when your team prefers YAML and wants minimal friction in policy adoption. It also includes native support for image verification with Cosign built directly into the policy language, eliminating the need for custom webhook code. Kyverno excels at mutation policies, allowing you to automatically inject labels, security defaults, and sidecar containers without rejecting deployments. Finally, policy violations are reported as Kubernetes resources themselves, making them queryable and auditable just like any other cluster object.
OPA/Gatekeeper becomes preferable when you already maintain OPA policies elsewhere in your infrastructure—perhaps for Terraform validation, API gateway rules, or microservice policies. You might also choose OPA if your policy requirements involve complex conditional logic, external data lookups from policy-as-code repositories, or deeply interconnected rules that benefit from a full-featured policy language. A shared policy language across your entire stack, from infrastructure to application to API layers, creates consistency and reduces operational overhead when policies need updates.
In practice, both engines can coexist in a cluster. You might use Kyverno for straightforward image enforcement and OPA for complex cross-system policies. The decision often comes down to team preference and existing investments.
Installing Kyverno
# Add Kyverno Helm repohelm repo add kyverno https://kyverno.github.io/kyverno/helm repo update # Install Kyverno with high availability (production)helm install kyverno kyverno/kyverno \ --namespace kyverno \ --create-namespace \ --set replicaCount=3 \ --set resources.requests.cpu=100m \ --set resources.requests.memory=256Mi # Verify installationkubectl get pods -n kyverno# Expected: 3 kyverno pods running # Check webhook is registeredkubectl get validatingwebhookconfigurations | grep kyvernokubectl get mutatingwebhookconfigurations | grep kyvernoPolicy 1: Require CleanStart Images Only
This policy enforces that all container images must come from registry.cleanstart.com. No Docker Hub, no quay.io, no gcr.io — only verified CleanStart images.
# policy-require-cleanstart-registry.yamlapiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: require-cleanstart-registry annotations: policies.kyverno.io/title: Require CleanStart Registry policies.kyverno.io/category: Supply Chain Security policies.kyverno.io/severity: high policies.kyverno.io/description: >- All container images must be pulled from the CleanStart registry (registry.cleanstart.com). Images from other registries are rejected.spec: validationFailureAction: Enforce # Reject non-compliant pods background: true # Scan existing resources too rules: - name: validate-image-registry match: any: - resources: kinds: - Pod validate: message: >- Image '{{request.object.spec.containers[].image}}' is not from the CleanStart registry. All images must be pulled from registry.cleanstart.com. Contact your platform team if you need an image added to the approved registry. pattern: spec: containers: - image: "registry.cleanstart.com/*" # Also enforce init containers =(initContainers): - image: "registry.cleanstart.com/*" # And ephemeral containers =(ephemeralContainers): - image: "registry.cleanstart.com/*"Apply and test:
# Apply the policykubectl apply -f policy-require-cleanstart-registry.yaml # Test: this should SUCCEEDkubectl run test-pass --image=registry.cleanstart.com/python3:3.12-prod --dry-run=server# Output: pod/test-pass created (server dry run) # Test: this should FAILkubectl run test-fail --image=python:3.12-slim --dry-run=server# Output: Error: admission webhook denied the request:# Image 'python:3.12-slim' is not from the CleanStart registry. # Test: Docker Hub should also FAILkubectl run test-fail2 --image=nginx:latest --dry-run=server# Output: Error: admission webhook denied the requestAllowing additional registries (for sidecars like Istio, Datadog, etc.):
# If you need to allow specific additional registriesspec: rules: - name: validate-image-registry match: any: - resources: kinds: - Pod validate: message: "Image must be from an approved registry." deny: conditions: all: - key: "{{request.object.spec.containers[].image}}" operator: AnyNotIn value: - "registry.cleanstart.com/*" - "docker.io/istio/*" # Istio sidecars - "gcr.io/datadoghq/*" # Datadog agentPolicy 2: Verify Image Signatures (Cosign)
This policy requires that every image is signed with Cosign and the signature is verified before the pod is admitted. Instead of simply assuming images are safe because they come from a particular registry, Kyverno interacts with the Cosign tooling to cryptographically verify that a signature matching approved signing identities is present in the container image registry. This transforms image trust from a registry-level control into a cryptographic identity control, ensuring that only images explicitly signed by your build system can run.
# policy-verify-image-signatures.yamlapiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: verify-image-signatures annotations: policies.kyverno.io/title: Verify Cosign Image Signatures policies.kyverno.io/category: Supply Chain Security policies.kyverno.io/severity: high policies.kyverno.io/description: >- All container images must have a valid Cosign signature. Images without verified signatures are rejected.spec: validationFailureAction: Enforce webhookTimeoutSeconds: 30 rules: - name: verify-cosign-signature match: any: - resources: kinds: - Pod verifyImages: - imageReferences: - "registry.cleanstart.com/*" attestors: - entries: # Option A: Keyless verification (Sigstore/Fulcio) - keyless: subject: "build@clnstrt.dev" issuer: "https://accounts.google.com" rekor: url: https://rekor.sigstore.dev # Option B: Static public key verification # - keys: # publicKeys: |- # -----BEGIN PUBLIC KEY----- # MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE... # -----END PUBLIC KEY-----Apply and test:
# Apply the policykubectl apply -f policy-verify-image-signatures.yaml # Test with a signed CleanStart image (should SUCCEED)kubectl run signed-app \ --image=registry.cleanstart.com/python3:3.12.5-prod \ --dry-run=server# Output: pod/signed-app created (server dry run) # Manually verify an image signature (outside the cluster)cosign verify \ --certificate-identity build@clnstrt.dev \ --certificate-oidc-issuer https://accounts.google.com \ registry.cleanstart.com/python3:3.12.5-prodPolicy 3: Require SBOM Attestation
This policy ensures every image has an SBOM attestation attached (CycloneDX or SPDX). Images without SBOMs are rejected. By requiring attestations at admission time, you guarantee that every pod running in your cluster has a complete inventory of its software dependencies, enabling vulnerability scanning, license compliance audits, and incident response investigations.
# policy-require-sbom-attestation.yamlapiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: require-sbom-attestation annotations: policies.kyverno.io/title: Require SBOM Attestation policies.kyverno.io/category: Supply Chain Security policies.kyverno.io/severity: medium policies.kyverno.io/description: >- All container images must have a Software Bill of Materials (SBOM) attestation attached, giving every deployed image a complete inventory of its dependencies.spec: validationFailureAction: Enforce webhookTimeoutSeconds: 30 rules: - name: require-sbom match: any: - resources: kinds: - Pod verifyImages: - imageReferences: - "registry.cleanstart.com/*" attestations: - type: https://cyclonedx.org/bom attestors: - entries: - keyless: subject: "build@clnstrt.dev" issuer: "https://accounts.google.com" rekor: url: https://rekor.sigstore.dev conditions: - all: # Verify the SBOM has components (not empty) - key: "{{ components[].name | length(@) }}" operator: GreaterThan value: 0Verify SBOM attestation manually:
# Check SBOM attestation on a CleanStart imagecosign verify-attestation \ --type cyclonedx \ --certificate-identity build@clnstrt.dev \ --certificate-oidc-issuer https://accounts.google.com \ registry.cleanstart.com/python3:3.12.5-prod # Download the SBOM for inspectioncosign verify-attestation \ --type cyclonedx \ --certificate-identity build@clnstrt.dev \ --certificate-oidc-issuer https://accounts.google.com \ registry.cleanstart.com/python3:3.12.5-prod \ | jq -r '.payload' | base64 -d | jq '.predicate'Policy 4: Enforce Pod Security Standards
This policy enforces the security posture that CleanStart images are designed for: non-root, read-only filesystem, dropped capabilities, no privilege escalation. By validating these controls at admission time, you ensure that developers cannot accidentally create pods that circumvent the security boundaries the images themselves were designed to operate within.
# policy-enforce-pod-security.yamlapiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: enforce-cleanstart-pod-security annotations: policies.kyverno.io/title: Enforce CleanStart Pod Security Standards policies.kyverno.io/category: Pod Security policies.kyverno.io/severity: high policies.kyverno.io/description: >- Enforces security best practices for CleanStart containers: non-root execution, read-only root filesystem, dropped capabilities, no privilege escalation.spec: validationFailureAction: Enforce background: true rules: # Rule 1: Must run as non-root (UID 65532) - name: require-non-root match: any: - resources: kinds: - Pod validate: message: >- Containers must run as non-root. Set securityContext.runAsNonRoot: true and securityContext.runAsUser: 65532 (CleanStart standard UID). pattern: spec: securityContext: runAsNonRoot: true containers: - securityContext: runAsNonRoot: true # Rule 2: Read-only root filesystem - name: require-read-only-rootfs match: any: - resources: kinds: - Pod validate: message: >- Container root filesystem must be read-only. Set securityContext.readOnlyRootFilesystem: true. Use emptyDir or tmpfs volumes for writable paths. pattern: spec: containers: - securityContext: readOnlyRootFilesystem: true # Rule 3: No privilege escalation - name: deny-privilege-escalation match: any: - resources: kinds: - Pod validate: message: "Privilege escalation is not allowed." pattern: spec: containers: - securityContext: allowPrivilegeEscalation: false # Rule 4: Drop ALL capabilities - name: drop-all-capabilities match: any: - resources: kinds: - Pod validate: message: >- All capabilities must be dropped. Set securityContext.capabilities.drop: ["ALL"]. Add back only specific capabilities if absolutely required. pattern: spec: containers: - securityContext: capabilities: drop: - ALL # Rule 5: No privileged containers - name: deny-privileged-containers match: any: - resources: kinds: - Pod validate: message: "Privileged containers are not allowed." pattern: spec: containers: - securityContext: privileged: false # Rule 6: No hostPath volumes - name: deny-host-path match: any: - resources: kinds: - Pod validate: message: "hostPath volumes are not allowed." deny: conditions: any: - key: "{{ request.object.spec.volumes[?hostPath] | length(@) }}" operator: GreaterThan value: 0 # Rule 7: No host networking - name: deny-host-network match: any: - resources: kinds: - Pod validate: message: "Host networking is not allowed." pattern: spec: =(hostNetwork): false =(hostPID): false =(hostIPC): falseApply and test:
# Apply the policykubectl apply -f policy-enforce-pod-security.yaml # Test: compliant CleanStart pod (should SUCCEED)cat <<'EOF' | kubectl apply --dry-run=server -f -apiVersion: v1kind: Podmetadata: name: compliant-podspec: securityContext: runAsNonRoot: true runAsUser: 65532 fsGroup: 65532 containers: - name: app image: registry.cleanstart.com/python3:3.12.5-prod securityContext: runAsNonRoot: true readOnlyRootFilesystem: true allowPrivilegeEscalation: false privileged: false capabilities: drop: ["ALL"] volumeMounts: - name: tmp mountPath: /tmp volumes: - name: tmp emptyDir: sizeLimit: 100MiEOF# Output: pod/compliant-pod created (server dry run) # Test: non-compliant pod (should FAIL)cat <<'EOF' | kubectl apply --dry-run=server -f -apiVersion: v1kind: Podmetadata: name: non-compliant-podspec: containers: - name: app image: registry.cleanstart.com/python3:3.12.5-prod securityContext: runAsRoot: true privileged: trueEOF# Output: Error: admission webhook denied the requestPolicy 5: Enforce Resource Limits
CleanStart minimal images use significantly less resources than traditional images. This policy ensures every pod declares resource requests and limits, enabling the Kubernetes scheduler to make informed placement decisions and preventing resource starvation scenarios where a single badly-configured pod consumes all available CPU or memory.
# policy-require-resource-limits.yamlapiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: require-resource-limits annotations: policies.kyverno.io/title: Require Resource Requests and Limits policies.kyverno.io/category: Resource Management policies.kyverno.io/severity: mediumspec: validationFailureAction: Enforce background: true rules: - name: require-requests-and-limits match: any: - resources: kinds: - Pod validate: message: >- All containers must declare CPU and memory requests and limits. CleanStart images are minimal — start with cpu: 100m/500m and memory: 128Mi/512Mi and adjust based on metrics. pattern: spec: containers: - resources: requests: memory: "?*" cpu: "?*" limits: memory: "?*" cpu: "?*" - name: limit-memory-ceiling match: any: - resources: kinds: - Pod validate: message: >- Memory limit cannot exceed 4Gi. CleanStart images are minimal — if you need more than 4Gi, review your application. deny: conditions: any: - key: "{{ request.object.spec.containers[].resources.limits.memory }}" operator: GreaterThan value: "4Gi"Policy 6: Block Latest Tags
The :latest tag is mutable—it can point to different images at different times. This breaks reproducibility and auditing. By enforcing versioned tags, you guarantee that deployments remain stable even when new images are pushed to the registry, and you create an immutable record of exactly which image version was deployed at which time.
# policy-block-latest-tag.yamlapiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: block-latest-tag annotations: policies.kyverno.io/title: Block Latest Image Tag policies.kyverno.io/category: Supply Chain Security policies.kyverno.io/severity: medium policies.kyverno.io/description: >- Images must use a specific version tag or digest, not :latest. Mutable tags break reproducibility and supply chain verification.spec: validationFailureAction: Enforce background: true rules: - name: deny-latest-tag match: any: - resources: kinds: - Pod validate: message: >- Image tag ':latest' is not allowed. Use a specific version tag (e.g., registry.cleanstart.com/python3:3.12.5-prod) or a digest (e.g., registry.cleanstart.com/python3@sha256:abc...). pattern: spec: containers: - image: "!*:latest" =(initContainers): - image: "!*:latest"Policy 7: Auto-Inject Security Defaults (Mutation)
Instead of rejecting pods that miss security settings, this mutation policy automatically adds CleanStart security defaults. Use this in development and staging environments; use validation (enforce) in production. This approach dramatically improves developer experience by eliminating rejection loops while still maintaining security baselines through automatic injection.
# policy-mutate-security-defaults.yamlapiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: inject-cleanstart-security-defaults annotations: policies.kyverno.io/title: Auto-Inject CleanStart Security Defaults policies.kyverno.io/category: Pod Security policies.kyverno.io/severity: medium policies.kyverno.io/description: >- Automatically injects CleanStart security defaults into pods that don't specify them. Adds non-root, read-only FS, capability drop, and resource defaults.spec: rules: # Inject pod-level security context - name: inject-pod-security-context match: any: - resources: kinds: - Pod mutate: patchStrategicMerge: spec: securityContext: +(runAsNonRoot): true +(runAsUser): 65532 +(runAsGroup): 65532 +(fsGroup): 65532 +(seccompProfile): type: RuntimeDefault # Inject container-level security context - name: inject-container-security-context match: any: - resources: kinds: - Pod mutate: foreach: - list: "request.object.spec.containers" patchStrategicMerge: spec: containers: - (name): "{{ element.name }}" securityContext: +(readOnlyRootFilesystem): true +(allowPrivilegeEscalation): false +(privileged): false +(capabilities): drop: - ALL # Inject default resource requests - name: inject-resource-defaults match: any: - resources: kinds: - Pod mutate: foreach: - list: "request.object.spec.containers" patchStrategicMerge: spec: containers: - (name): "{{ element.name }}" resources: +(requests): cpu: "100m" memory: "128Mi" +(limits): cpu: "500m" memory: "512Mi" # Add CleanStart labels - name: inject-labels match: any: - resources: kinds: - Pod mutate: patchStrategicMerge: metadata: labels: +(cleanstart.dev/enforced): "true" +(cleanstart.dev/security-profile): "hardened"Test the mutation:
# Apply mutation policykubectl apply -f policy-mutate-security-defaults.yaml # Deploy a bare pod (no security context specified)kubectl run bare-pod \ --image=registry.cleanstart.com/python3:3.12.5-prod \ --dry-run=server -o yaml # The output will show injected securityContext:# securityContext:# runAsNonRoot: true# runAsUser: 65532# readOnlyRootFilesystem: true# allowPrivilegeEscalation: false# capabilities:# drop: ["ALL"]Policy 8: Require Image Digest (Not Tag)
For maximum supply chain integrity, require image references by digest (immutable) rather than tag (mutable). Digests are content-addressed identifiers that represent the exact image you verified—if even a single byte changes, the digest changes, preventing accidental rollouts to different images due to tag reassignment or image registry compromises.
# policy-require-image-digest.yamlapiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: require-image-digest annotations: policies.kyverno.io/title: Require Image Digest policies.kyverno.io/category: Supply Chain Security policies.kyverno.io/severity: high policies.kyverno.io/description: >- In production, images must be referenced by digest (@sha256:...) not by tag. Tags are mutable; digests are immutable and guarantee you're running exactly the image you verified.spec: validationFailureAction: Enforce background: true rules: - name: require-digest match: any: - resources: kinds: - Pod namespaces: - production - prod-* validate: message: >- In production namespaces, images must use a digest reference (e.g., registry.cleanstart.com/python3@sha256:abc123...), not a tag. Get the digest: crane digest registry.cleanstart.com/python3:3.12.5-prod pattern: spec: containers: - image: "*@sha256:*"Get image digests:
# Get the digest for a CleanStart imagecrane digest registry.cleanstart.com/python3:3.12.5-prod# Output: sha256:a1b2c3d4e5f6... # Use in your deploymentimage: registry.cleanstart.com/python3@sha256:a1b2c3d4e5f6...Deploying All Policies Together
Recommended Rollout Strategy
Don't enforce everything at once. Roll out in phases, progressively tightening controls as teams adapt and compliance improves.
# Phase 1: Audit mode (see violations without blocking)# Change all policies to:# validationFailureAction: Audit kubectl apply -f policy-require-cleanstart-registry.yamlkubectl apply -f policy-block-latest-tag.yamlkubectl apply -f policy-require-resource-limits.yaml # Check violations (policy reports)kubectl get policyreport -Akubectl get clusterpolicyreport # Phase 2: Enforce non-breaking policies# These won't break existing workloads if already using CleanStart:kubectl apply -f policy-block-latest-tag.yaml # Enforcekubectl apply -f policy-require-resource-limits.yaml # Enforce # Phase 3: Enforce security policieskubectl apply -f policy-enforce-pod-security.yaml # Enforcekubectl apply -f policy-require-cleanstart-registry.yaml # Enforce # Phase 4: Enforce supply chain policieskubectl apply -f policy-verify-image-signatures.yaml # Enforcekubectl apply -f policy-require-sbom-attestation.yaml # Enforce # Phase 5 (production only): Enforce digest requirementkubectl apply -f policy-require-image-digest.yaml # EnforceMonitoring Policy Violations
# View all policy violations across clusterkubectl get policyreport -A -o wide # Detailed violation reportkubectl get policyreport -n default -o yaml # Count violations by policykubectl get clusterpolicyreport -o json | \ jq '.items[].results[] | select(.result=="fail") | .policy' | \ sort | uniq -c | sort -rn # Watch for new violations in real-timekubectl get events --field-selector reason=PolicyViolation -wNamespace Exceptions
Some namespaces (monitoring, kube-system) may need different rules. Rather than creating blanket exceptions, define explicit allowlists for system components:
# Exclude system namespaces from registry restrictionspec: rules: - name: validate-image-registry match: any: - resources: kinds: - Pod exclude: any: - resources: namespaces: - kube-system - kyverno - monitoring - istio-systemComplete CleanStart Policy Bundle
Apply all policies at once with a Kustomization, making your entire policy set version-controlled and deployable as a single unit:
# kustomization.yamlapiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources: - policy-require-cleanstart-registry.yaml - policy-verify-image-signatures.yaml - policy-require-sbom-attestation.yaml - policy-enforce-pod-security.yaml - policy-require-resource-limits.yaml - policy-block-latest-tag.yaml - policy-mutate-security-defaults.yaml # Production only: # - policy-require-image-digest.yaml# Apply all policieskubectl apply -k . # Verify all policies are activekubectl get clusterpolicy# NAME BACKGROUND VALIDATE ACTION READY# require-cleanstart-registry true Enforce True# verify-image-signatures true Enforce True# require-sbom-attestation true Enforce True# enforce-cleanstart-pod-security true Enforce True# require-resource-limits true Enforce True# block-latest-tag true Enforce True# inject-cleanstart-security-defaults TrueTroubleshooting
Policy Not Enforcing
# Check Kyverno logskubectl logs -n kyverno -l app.kubernetes.io/name=kyverno --tail=100 # Verify webhook configurationkubectl get validatingwebhookconfigurations kyverno-resource-validating-webhook-cfg -o yaml # Check policy statuskubectl get clusterpolicy require-cleanstart-registry -o yaml | grep -A5 "status:"Image Verification Timeout
# Increase webhook timeout (default 10s may be too short for signature verification)# In the policy spec:spec: webhookTimeoutSeconds: 30 # Or in Kyverno Helm values:helm upgrade kyverno kyverno/kyverno \ --namespace kyverno \ --set webhookTimeout=30Debugging Denied Pods
# See exactly why a pod was deniedkubectl describe pod <pod-name># Events section shows the denial reason and which policy blocked it # Test a manifest without creating the resourcekubectl apply -f my-pod.yaml --dry-run=server# Shows the admission webhook responseWhat to Read Next
OPA Gatekeeper Policies — Alternative policy engine using Rego. Image Signing with Sigstore — How CleanStart images are signed. CycloneDX SBOM — SBOM format and generation. CIS Hardening — CIS benchmark controls that these policies enforce. Upgrade Patching Playbook — How new image versions are published.
