“Container with privilege escalation should be avoided” means a Kubernetes container does not set securityContext.allowPrivilegeEscalation to false, so its processes can gain privileges through setuid binaries or file capabilities. To fix it, set allowPrivilegeEscalation: false and drop all capabilities on every container. Then enforce the Pod Security Standards restricted profile so new workloads cannot regress.
What the scanner is actually detecting
Two common tools raise this finding under different names:
- Microsoft Defender for Cloud: the Kubernetes data plane recommendation Container with privilege escalation should be avoided (severity Medium). It is backed by the built-in Azure Policy Kubernetes clusters should not allow container privilege escalation, which the Azure Policy add-on for Kubernetes (built on Gatekeeper) evaluates against pods in the cluster. By default that policy excludes the kube-system, gatekeeper-system, azure-arc and azure-extensions-usage-system namespaces.
- Trivy: check KSV-0001 (aliases KSV001, AVD-KSV-0001), titled Can elevate its own privileges, severity Medium. It inspects Pods and workload controllers (Deployments, StatefulSets, DaemonSets, ReplicaSets, ReplicationControllers, Jobs and CronJobs) and fails any container or init container where allowPrivilegeEscalation is not explicitly false. Leaving the field out counts as a failure.
Leaving it out fails because Kubernetes treats a missing value as true. The field directly controls the Linux no_new_privs flag on the container process: with allowPrivilegeEscalation: false, the kernel stops execve from granting extra privileges through setuid or setgid bits and file capabilities.
Real-world risk
This is a hardening gap, not an exploitable vulnerability on its own, and both vendors rate it Medium. The risk shows up after something else goes wrong: an attacker who gets code execution as a non-root user inside the container (through an application bug, for example) can run a setuid-root binary shipped in the image, such as su or a vulnerable setuid helper, and become root inside the container.
Root inside a container is not automatic root on the node. Namespaces, cgroups, seccomp and the capability set still apply. What escalation buys the attacker is a better starting point: root-owned files in mounted volumes, anything the container user was deliberately kept away from, and kernel attack surface that needs root. If the container already runs as root with default capabilities, this setting changes little on its own, which is why it belongs together with runAsNonRoot and capabilities.drop: [“ALL”]. For how posture findings like this fit next to runtime detection, see CSPM vs CWPP vs CNAPP.
How to confirm it on the cluster
List every running container (including init and ephemeral containers) that does not set the field to false:
kubectl get pods -A -o json | jq -r '
.items[] | .metadata.namespace as $ns | .metadata.name as $pod
| (.spec.containers[]?, .spec.initContainers[]?, .spec.ephemeralContainers[]?)
| select(.securityContext.allowPrivilegeEscalation != false)
| "($ns)/($pod) container=(.name) privileged=(.securityContext.privileged // false)"'
Find the controller you actually need to edit, because patching a pod that a controller created is pointless. For a Deployment pod this prints the ReplicaSet; run the same query against that ReplicaSet to reach the Deployment:
kubectl -n <namespace> get pod <pod> -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"n"}'
Check the kernel flag inside a running container. NoNewPrivs: 1 means escalation is blocked, 0 means it is allowed. Distroless images may not ship grep; rely on the manifest checks there:
kubectl -n <namespace> exec <pod> -c <container> -- grep NoNewPrivs /proc/1/status
Preview what the restricted profile would reject in a namespace without enforcing anything. The server returns warnings naming each violating pod:
kubectl label --dry-run=server --overwrite ns <namespace> pod-security.kubernetes.io/enforce=restricted
For manifests in a repository and for a live namespace, run Trivy:
trivy config ./k8s/
trivy k8s --scanners misconfig --include-namespaces <namespace> --report all
How to fix it
Kubernetes workloads (the real fix)
Set the field in the pod template of the owning Deployment, StatefulSet, DaemonSet, Job or CronJob, on every container, including init containers and sidecars. allowPrivilegeEscalation is a container-level field, so it cannot be set once for the whole pod:
spec:
template:
spec:
securityContext: # pod level: inherited by containers
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
initContainers:
- name: migrate
securityContext: # container level: required per container
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
containers:
- name: web
securityContext:
allowPrivilegeEscalation: false
privileged: false
capabilities:
drop: ["ALL"]
# image, ports and other fields omitted
If a process must bind a port below 1024, the restricted profile allows adding back only NET_BIND_SERVICE. Moving the listener to a high port and mapping it in the Service is usually simpler.
For an urgent change on a cluster not managed by GitOps, a strategic merge patch matches containers by name and triggers a rollout:
kubectl -n shop patch deployment web --patch '{"spec":{"template":{"spec":{"containers":[{"name":"web","securityContext":{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]}}}]}}}}'
If Argo CD, Flux or Helm owns the object, change the source manifest or chart values instead, or the next sync reverts your patch. Most charts expose a container security context in values.yaml; check the chart’s documentation for the key name.
Enforce it cluster-wide with Pod Security Admission
Pod Security Admission is built into Kubernetes (stable since v1.25). Start with warn and audit, then enforce once the warnings are gone:
kubectl label --overwrite ns shop
pod-security.kubernetes.io/warn=restricted
pod-security.kubernetes.io/audit=restricted
kubectl label --overwrite ns shop
pod-security.kubernetes.io/enforce=restricted
pod-security.kubernetes.io/enforce-version=latest
Pin enforce-version to your cluster’s minor version (for example v1.33) instead of latest if you want upgrades to leave the policy unchanged.
AKS and Azure Arc: Azure Policy
The built-in policy definition ID is 1c6e92c9-99f0-4e55-9cf2-0c234dc48f99, with effects Audit (default), Deny and Disabled. It requires the Azure Policy add-on or extension on the cluster:
az policy assignment create
--name no-container-priv-esc
--policy 1c6e92c9-99f0-4e55-9cf2-0c234dc48f99
--scope /subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.ContainerService/managedClusters/<cluster>
--params '{"effect":{"value":"Deny"}}'
Gatekeeper or Kyverno
On other distributions, use the Gatekeeper library template K8sPSPAllowPrivilegeEscalationContainer (it supports an exemptImages parameter) or the Kyverno policy disallow-privilege-escalation from the Pod Security Standards (Restricted) set. The Kyverno policy ships in Audit mode; switch it to Enforce after the policy reports are clean.
Plain Docker hosts
The same image run outside Kubernetes gets the equivalent protection from a Docker security option:
docker run --security-opt no-new-privileges -d registry.example.com/web:1.4.2
docker inspect --format '{{.HostConfig.SecurityOpt}}' <container>
How to verify the fix and rescan
- Re-run the jq query above. The fixed workload should no longer be listed.
- Exec into a new pod and confirm NoNewPrivs: 1 in /proc/1/status.
- Re-run trivy config on the manifests and trivy k8s on the namespace; KSV-0001 should be gone for that resource.
- In Defender for Cloud, open the recommendation and check the affected resources list. Assessment is periodic, so a fixed workload can stay unhealthy for a while after rollout.
What can break and how to roll back
- sudo, su and setuid tools stop working inside the container. Entrypoint scripts that call sudo fail.
- File capabilities are ignored. A non-root binary that relies on setcap (for example to bind port 80) loses that ability, and older images that ship ping setuid root can lose ping.
- Dropping ALL capabilities breaks images that start as root and then chown files or switch users, because those steps need CHOWN, SETUID or SETGID.
- The API server rejects the manifest if the container also has privileged: true; Kubernetes also documents the setting as inconsistent with CAP_SYS_ADMIN. Remove the privilege first or handle the workload as a documented exception.
- Enforce mode stalls rollouts quietly. Pod Security Admission enforce applies to pods, not to Deployments, so the Deployment is accepted but its ReplicaSet cannot create pods. Check kubectl -n <namespace> get events.
To roll back, revert the manifest or run kubectl -n <namespace> rollout undo deployment/<name>. To relax admission, remove the enforce label with kubectl label ns <namespace> pod-security.kubernetes.io/enforce- or set it back to baseline, and set the Azure Policy effect back to Audit.
Common false positive reasons
- Windows pods. The field is Linux-only; Kubernetes forbids setting it when spec.os.name is windows. Trivy’s KSV-0001 check does not look at the OS, so it can still flag these.
- Mutation at admission. A Kyverno or Gatekeeper mutation policy may add the field when the pod is created. The live pod is compliant while trivy config on the source manifest still fails.
- Old ReplicaSets. A scanner that inventories ReplicaSets can still see the previous pod template in ReplicaSets kept for rollback history.
- Injected sidecars. A container added by a webhook appears only in the live pod, so it can fail even though your manifest is correct.
- Infrastructure agents such as CNI, CSI or monitoring DaemonSets that genuinely need privileged mode. These are accepted exceptions rather than false positives, and should be documented as such. More patterns are covered in container scanner false positives.
FAQ
Does runAsNonRoot fix this finding?
No. runAsNonRoot controls which user starts the process; allowPrivilegeEscalation controls whether that process can later gain more privileges. Set both.
Can I set allowPrivilegeEscalation at the pod level?
No. It exists only in the container securityContext. Set it on each container, init container and ephemeral container.
My container runs as root. Does this still matter?
Less, because there is little left to escalate to inside the container. Scanners will still flag it, and the better fix is to run as non-root with the field set to false.
Is allowPrivilegeEscalation: false enough for the restricted profile?
No. Restricted also requires non-root, dropping ALL capabilities and a RuntimeDefault or Localhost seccomp profile. Use the server-side dry-run to see every gap.
Tracking this finding across many clusters
Findings like this multiply across namespaces and clusters. If your container scanners are among the 16 that SITEY imports from, the results can sit in one self-hosted list next to your other findings. Duplicates are merged per scanner, not across scanners, so a container flagged by two tools appears twice, and AI triage suggests likely false positives with evidence while a human makes the final call.
Sources
- Microsoft Learn: Container security recommendations in Microsoft Defender for Cloud
- Trivy checks: KSV-0001 Can elevate its own privileges
- Kubernetes: Configure a Security Context for a Pod or Container
- Kubernetes: Pod Security Standards
- Kubernetes: Enforce Pod Security Standards with Namespace Labels