This issue has been assessed as high severity. Review affected configurations immediately.
The most common Kubernetes misconfigurations — privileged containers, host network access, containers running as root, writable root filesystems — are not detection problems. They are policy enforcement problems. Each one is visible in the pod spec before the workload is admitted to the cluster. Admission controllers are the mechanism that evaluates that spec against policy and blocks or mutates it before any container runs.
Without admission control, security depends on developers not making mistakes and on post-admission scanning catching problems in running workloads. With admission control, violations are blocked at deployment time — the cheapest possible moment to catch them.
How Admission Controllers Work
Kubernetes processes admission webhooks during the admission control phase of the API server request lifecycle. When a resource (Pod, Deployment, Ingress, etc.) is created or updated, the API server serialises the request and sends it to configured webhook endpoints. The webhook evaluates the resource against its policy set and returns either an admission or a denial — optionally with mutation applied before admission.
Two types exist:
- Validating admission webhooks — evaluate and accept or reject
- Mutating admission webhooks — modify the resource (add labels, inject sidecars, set defaults) then admit
OPA Gatekeeper and Kyverno both operate as dynamic validating (and optionally mutating) admission webhooks deployed inside the cluster.
OPA Gatekeeper
Gatekeeper extends Kubernetes with two custom resources: ConstraintTemplate (defines the policy logic in Rego) and Constraint (instantiates a template against selected resources with parameters).
ConstraintTemplate — No Privileged Containers:
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8snoprivilegedcontainer
spec:
crd:
spec:
names:
kind: K8sNoPrivilegedContainer
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8snoprivilegedcontainer
violation[{"msg": msg}] {
c := input_containers[_]
c.securityContext.privileged == true
msg := sprintf("Container <%v> must not run as privileged", [c.name])
}
input_containers[c] {
c := input.review.object.spec.containers[_]
}
input_containers[c] {
c := input.review.object.spec.initContainers[_]
}
Constraint — Apply to All Namespaces Except System:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sNoPrivilegedContainer
metadata:
name: no-privileged-containers
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- kube-system
- gatekeeper-system
Gatekeeper also supports audit mode, which continuously evaluates existing resources against constraints and surfaces violations in the Constraint status. This allows brownfield adoption without immediately blocking existing workloads.
# Check current violations across all constraints
kubectl get constraints -A -o json | jq '.items[] | {name: .metadata.name, violations: .status.totalViolations}'
Kyverno
Kyverno uses a YAML-native policy language without Rego, making it more accessible for teams less comfortable with a separate policy language. Policies are Kubernetes resources with validate, mutate, generate, and verify images rules.
Kyverno Policy — Require Non-Root User:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-non-root-user
spec:
validationFailureAction: Enforce
background: true
rules:
- name: check-run-as-non-root
match:
any:
- resources:
kinds:
- Pod
exclude:
any:
- resources:
namespaces:
- kube-system
validate:
message: "Containers must run as non-root. Set securityContext.runAsNonRoot: true and runAsUser > 0."
pattern:
spec:
=(initContainers):
- =(securityContext):
runAsNonRoot: true
runAsUser: ">0"
containers:
- securityContext:
runAsNonRoot: true
runAsUser: ">0"
Kyverno Policy — Disallow Host Networking and Host PID:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-host-namespaces
spec:
validationFailureAction: Enforce
background: true
rules:
- name: host-namespaces
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Sharing the host network, PID, or IPC namespace is not allowed."
pattern:
spec:
=(hostPID): "false"
=(hostIPC): "false"
=(hostNetwork): "false"
Mutating policy — Automatically add read-only root filesystem:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-readonly-rootfs
spec:
rules:
- name: add-readonly-rootfs
match:
any:
- resources:
kinds:
- Pod
mutate:
patchStrategicMerge:
spec:
containers:
- (name): "*"
securityContext:
+(readOnlyRootFilesystem): true
Policies Every Production Cluster Should Have
The following covers the highest-impact attack surface for Kubernetes workloads:
| Policy | Purpose | Risk if missing |
|---|---|---|
| No privileged containers | Prevents container escape | Full node compromise from any container |
| Require non-root | Reduces blast radius of container escape | Root in container → root on node |
| Read-only root filesystem | Limits persistence and tooling installation | Attacker can modify container files |
| No host network/PID/IPC | Prevents namespace escapes | Access to host network stack and processes |
| Restrict volume types | Prevents hostPath mounts to sensitive paths | Read/write access to host filesystem |
| Require resource limits | Prevents DoS | Single workload can exhaust node resources |
Disallow latest image tag | Forces explicit, auditable image versions | Unpredictable deployments, supply chain risk |
| Require image signatures | Verifies image provenance (Cosign/Notary) | Unsigned/compromised images admitted |
Auditing Existing Clusters
Before enforcing policies, audit what you already have:
# Find all privileged containers in the cluster
kubectl get pods -A -o json | jq -r '
.items[] |
.metadata.namespace as $ns |
.metadata.name as $pod |
.spec.containers[] |
select(.securityContext.privileged == true) |
[$ns, $pod, .name] | join(" / ")
'
# Find pods running as root (runAsUser: 0 or no securityContext)
kubectl get pods -A -o json | jq -r '
.items[] |
.metadata.namespace as $ns |
.metadata.name as $pod |
.spec.containers[] |
select(.securityContext == null or .securityContext.runAsNonRoot != true) |
[$ns, $pod, .name] | join(" / ")
'
# Find pods with hostNetwork: true
kubectl get pods -A -o json | jq -r '
.items[] |
select(.spec.hostNetwork == true) |
[.metadata.namespace, .metadata.name] | join(" / ")
'
Gatekeeper vs Kyverno: Choosing Between Them
Both tools enforce policy effectively. The practical differentiators:
Choose Gatekeeper if:
- Your team already knows Rego (OPA is used elsewhere — Styra, Conftest, Terraform)
- You need the Gatekeeper constraint library’s extensive pre-built policy set
- You want tight OPA ecosystem integration
Choose Kyverno if:
- Your team prefers YAML-native policy without learning a new language
- You want richer mutation and generation capabilities out of the box
- You’re adopting admission control for the first time and want faster time-to-value
Running both is possible but operationally complex. Most teams pick one and build their policy library around it. Neither choice is wrong — enforcement is what matters.
CI/CD Integration
Admission controllers catch violations at deploy time. Shift-left further by running the same policies in CI before the manifest reaches the cluster.
# Kyverno CLI — validate manifests against policies in CI
kyverno apply ./policies/ --resource ./manifests/
# Conftest with OPA — for Gatekeeper Rego policies
conftest test manifests/ --policy policies/
# Checkov — built-in K8s security checks without custom policies
checkov -d ./manifests/ --framework kubernetes
Catching a privileged container in a pull request costs nothing. Catching it after it reaches production — and discovering why it was needed — costs a sprint.