Kubernetes Admission Control: From PodSecurity Standards to Custom OPA/Kyverno Policies
Problem
Without admission control, any user with deployment permissions can run privileged containers, mount the host filesystem, use the host network, run as root, and bypass every security boundary Kubernetes provides. Pod Security Standards (built-in since v1.25) provide a baseline, but production enforcement needs custom policies that go beyond what PSS covers: blocking the latest tag, requiring resource limits, enforcing specific image registries, mandating team labels, and verifying image signatures.
The landscape is fragmented: Pod Security Standards, OPA Gatekeeper, and Kyverno each solve the problem differently. Teams waste time evaluating tools instead of writing policies.
Target systems: Kubernetes 1.29+. Covers Pod Security Standards (built-in), Kyverno 1.12+, and OPA Gatekeeper 3.16+.
Threat Model
- Adversary: Malicious or negligent developer deploying insecure workloads (privileged container, host path mount, running as root), or supply chain attacker injecting a malicious container image from an unauthorized registry.
- Blast radius: A single privileged container can compromise the entire node and all pods on it. A host path mount can read any file on the node, including kubelet credentials and other pods’ secrets.
Configuration
Pod Security Standards (Built-In Baseline)
PSS is built into Kubernetes, no installation needed. Three profiles: privileged (unrestricted), baseline (prevents known privilege escalations), restricted (best practice for hardened workloads).
# Enforce 'restricted' on all application namespaces:
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
# Use 'baseline' for kube-system (some system components need elevated privileges):
kubectl label namespace kube-system \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
What PSS restricted blocks:
- Privileged containers
- Host namespaces (hostNetwork, hostPID, hostIPC)
- Host path mounts
- Running as root
- Privilege escalation
- Non-default seccomp profiles (must use RuntimeDefault or Localhost)
- Capabilities beyond a small allowlist
What PSS does NOT cover (need Kyverno or Gatekeeper):
- Image registry allowlisting
latesttag blocking- Resource limit requirements
- Label requirements
- Image signature verification
- Custom organisation-specific policies
Choosing Kyverno vs OPA Gatekeeper
| Feature | Kyverno | OPA Gatekeeper |
|---|---|---|
| Policy language | YAML (familiar to K8s users) | Rego (dedicated policy language, learning curve) |
| Generate resources | Yes (can create NetworkPolicies, ConfigMaps from policies) | No |
| Mutate resources | Yes (auto-add labels, defaults) | No (admission only) |
| Resource overhead | ~200MB per replica (3 replicas recommended) | ~150MB per replica (3 replicas recommended) |
| Multi-cluster distribution | Nirmata (#53) Enterprise | Styra (#52) DAS |
| Dry-run/audit mode | audit mode (log violations without blocking) |
dryrun enforcement action |
| Image verification | Built-in verifyImages with cosign |
Requires external data provider |
| Community | Growing rapidly; CNCF project | Mature; CNCF graduated |
Recommendation: Kyverno for teams that want YAML policies and need generate/mutate capabilities. Gatekeeper for teams with existing Rego expertise or complex cross-resource policies. Both are production-ready.
Kyverno: Essential Policies
Install Kyverno:
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno kyverno/kyverno \
--namespace kyverno --create-namespace \
--set replicaCount=3 \
--set resources.requests.memory=256Mi \
--set resources.limits.memory=512Mi
Block latest tag:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-latest-tag
spec:
validationFailureAction: Enforce
background: true
rules:
- name: validate-image-tag
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Using the 'latest' tag is not allowed. Specify a version tag."
pattern:
spec:
containers:
- image: "!*:latest & *:*"
=(initContainers):
- image: "!*:latest & *:*"
Require resource limits:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-resource-limits
spec:
validationFailureAction: Enforce
background: true
rules:
- name: validate-resources
match:
any:
- resources:
kinds:
- Pod
validate:
message: "CPU and memory limits are required for all containers."
pattern:
spec:
containers:
- resources:
limits:
memory: "?*"
cpu: "?*"
Restrict image registries:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-image-registries
spec:
validationFailureAction: Enforce
background: true
rules:
- name: validate-registries
match:
any:
- resources:
kinds:
- Pod
validate:
message: >-
Images must be from approved registries:
registry.example.com, ghcr.io/your-org, docker.io/library.
pattern:
spec:
containers:
- image: "registry.example.com/* | ghcr.io/your-org/* | docker.io/library/*"
=(initContainers):
- image: "registry.example.com/* | ghcr.io/your-org/* | docker.io/library/*"
Require team labels:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels
spec:
validationFailureAction: Enforce
background: true
rules:
- name: require-team-label
match:
any:
- resources:
kinds:
- Deployment
- StatefulSet
- DaemonSet
validate:
message: "The label 'team' is required on all workloads."
pattern:
metadata:
labels:
team: "?*"
Verify image signatures (cosign):
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
background: false
rules:
- name: verify-cosign-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "registry.example.com/*"
attestors:
- count: 1
entries:
- keyless:
issuer: "https://token.actions.githubusercontent.com"
subject: "https://github.com/your-org/*"
rekor:
url: "https://rekor.sigstore.dev"
Safe Rollout Strategy
Never switch from audit to enforce without a review period:
# Step 1: Deploy policies in audit mode (log violations, don't block)
# Set validationFailureAction: Audit in all policies
# Step 2: Monitor violations for 1-2 weeks
kubectl get policyreport -A
kubectl get clusterpolicyreport
# Step 3: Review and fix all violating workloads
# Fix manifests to comply with policies
# Step 4: Switch to enforce mode
# Change validationFailureAction: Enforce in each policy
# Step 5: Monitor for blocked deployments
kubectl get events --field-selector reason=PolicyViolation -A
Handling Exceptions
For emergency deployments or legitimate exceptions:
# Exception for a specific namespace (e.g., monitoring needs host network)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-image-registries
spec:
validationFailureAction: Enforce
rules:
- name: validate-registries
match:
any:
- resources:
kinds:
- Pod
exclude:
any:
- resources:
namespaces:
- monitoring # Prometheus needs some non-standard images
validate:
message: "Images must be from approved registries."
pattern:
spec:
containers:
- image: "registry.example.com/* | ghcr.io/your-org/*"
Expected Behaviour
kubectl run test --image=ubuntu:latestreturns admission error (latest tag blocked)- Deployments without resource limits are rejected
- Images from non-allowlisted registries are blocked
- Unsigned images (when signature verification is enabled) are blocked
- Violations logged in PolicyReport resources during audit mode
- All existing workloads pass validation after remediation
kubectl get policyreport -Ashows zero violations after full rollout
Trade-offs
| Tool/Control | Impact | Risk | Mitigation |
|---|---|---|---|
PSS restricted |
Blocks privileged containers, host mounts, running as root | Many Helm charts need modification to comply | Start with warn mode. Fix charts before enforce. |
| Kyverno (3 replicas) | ~600MB total memory; adds 10-50ms to admission latency | Webhook unavailability blocks all pod creation if failurePolicy=Fail |
Set failurePolicy: Ignore for non-critical policies. Use 3 replicas for HA. |
latest tag blocking |
Forces explicit versioning | Developers must update tags for every deployment | Integrate automated image tagging in CI pipeline. |
| Registry allowlisting | Blocks public images not in the allowlist | Breaks charts that use images from other registries (bitnami, quay.io) | Add legitimate registries to the allowlist. Review quarterly. |
| Image signature verification | Adds 100-500ms to admission (signature verification) | Signing infrastructure outage blocks all deployments | Break-glass: Kyverno policy exception for emergency namespace. Time-limited, auto-expiring. |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| Kyverno webhook down | All pod creation hangs or fails (if failurePolicy=Fail) | Pod creation timeout; Kyverno pod not running | Restart Kyverno pods. If unrecoverable: kubectl delete validatingwebhookconfigurations kyverno-resource-validating-webhook-cfg (disables enforcement. emergency only). |
| Policy blocks system component | kube-system pod fails to reschedule after node drain | Control plane components show scheduling errors | Exclude kube-system from all custom policies. PSS should be baseline (not restricted) for kube-system. |
| Audit mode shows 500+ violations | Too many violations to fix before switching to enforce | PolicyReport shows violation count; prioritisation needed | Fix violations by severity: privileged containers first, then registry compliance, then labels. |
| Cosign verification fails for valid image | Signed image rejected due to expired signature or unreachable Rekor | Admission error mentions signature verification failure | Check Rekor availability. Verify the image signature manually with cosign verify. If Rekor is down: temporarily switch policy to audit mode. |
When to Consider a Managed Alternative
Transition point: Policy libraries grow past 50 rules within months. Testing policies against all workloads before enforcement requires a staging cluster. Multi-cluster policy consistency requires centralized distribution that Kyverno/Gatekeeper OSS do not provide.
- Styra (#52) DAS: Enterprise OPA management with policy bundles, decision logging, impact analysis, and multi-cluster distribution.
- Nirmata (#53): Kyverno Enterprise with policy lifecycle management, multi-cluster policy distribution, compliance reporting, and policy-as-code CI integration.
- Snyk (#48) IaC: Scans Kubernetes manifests in CI for policy violations before deployment, complementing runtime admission control.
Premium content pack: OPA/Kyverno policy library. 50+ tested policies covering: image security (registry allowlist, tag validation, signature verification), workload security (resource limits, security context, labels), network security (require network policies per namespace), and compliance (CIS Kubernetes Benchmark controls as admission policies).