“Privileged containers should be avoided” means a Kubernetes pod, Docker container or Amazon ECS task definition runs in privileged mode, giving it nearly all of root’s power on the host. To fix it, set privileged to false or remove it, grant only the capabilities or devices the workload needs, and enforce the Pod Security Standards baseline profile.
What the scanner is actually detecting
Four common checks raise this finding, and each reads a different object:
| Scanner | Finding | What it reads | Fails when | Severity |
|---|---|---|---|---|
| Microsoft Defender for Cloud | Privileged containers should be avoided (Kubernetes data plane) | Pods, through the Azure Policy add-on or extension | A container, init container or ephemeral container sets securityContext.privileged: true | Medium |
| Trivy | KSV-0017 (KSV017) Privileged | Pods and workload manifests (Deployment, StatefulSet, DaemonSet, ReplicaSet, ReplicationController, Job, CronJob) | A container or init container sets privileged: true | High |
| AWS Security Hub | ECS.4 ECS containers should run as non-privileged | Latest active revision of each task definition (AWS Config rule ecs-containers-nonprivileged) | Any container definition has “privileged”: true | High |
| docker-bench-security | 5.5 Ensure that privileged containers are not used | Running containers on the Docker host | docker inspect reports HostConfig.Privileged as true | WARN (CIS check) |
The Defender recommendation is backed by the built-in Azure Policy Kubernetes cluster should not allow privileged containers (definition ID 95edb821-ddaf-4404-9732-666045e056b4). By default it excludes the kube-system, gatekeeper-system, azure-arc and azure-extensions-usage-system namespaces, and its rule skips pods that select Windows nodes through nodeSelector kubernetes.io/os: windows. A missing field passes every check, because privileged defaults to false.
Real-world risk
This one deserves its High rating from Trivy and Security Hub. Docker documents what –privileged does: it enables all Linux capabilities, disables the default seccomp and AppArmor profiles and the SELinux process label, grants access to all host devices, and makes /sys and cgroup mounts read-write. The Kubernetes API reference puts it plainly: processes in privileged containers are essentially equivalent to root on the host.
The finding is not remotely exploitable on its own. An attacker first needs code execution inside the container, through an application bug, a compromised image or kubectl exec rights. After that, escaping needs no exploit: with every host device visible, the attacker can mount the node’s disk and read or change host files. On a Kubernetes node that includes the secrets and service account tokens mounted into every other pod scheduled there. Posture checks catch the configuration and runtime tools watch for the escape; CSPM vs CWPP vs CNAPP explains how the two layers fit.
How to confirm it on the host or cluster
List every running pod container (including init and ephemeral containers) that is privileged:
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.privileged == true)
| "($ns)/($pod) container=(.name)"'
Find the owning workloads, which are what you actually edit:
kubectl get deploy,statefulset,daemonset,job,cronjob -A -o json | jq -r '
.items[] | .kind as $k | .metadata.namespace as $ns | .metadata.name as $n
| (.spec.template.spec // .spec.jobTemplate.spec.template.spec)
| (.containers[]?, .initContainers[]?)
| select(.securityContext.privileged == true)
| "($k) ($ns)/($n) container=(.name)"'
Preview what the baseline profile would reject in a namespace without enforcing anything; the server answers with warnings naming each violating pod:
kubectl label --dry-run=server --overwrite ns <namespace> pod-security.kubernetes.io/enforce=baseline
For Amazon ECS, check each container in the latest revision of a task definition family:
aws ecs describe-task-definition --task-definition web
--query 'taskDefinition.containerDefinitions[].[name,privileged]' --output table
On a Docker host, include stopped containers, because docker-bench only checks running ones:
docker ps -aq | xargs docker inspect --format '{{.Name}} privileged={{.HostConfig.Privileged}}'
sudo sh docker-bench-security.sh -c check_5_5
How to fix it
Kubernetes workloads
First find out why the container is privileged. It usually needs one specific thing that can be granted more narrowly:
- A kernel permission, such as changing network settings: add only that capability (for example NET_ADMIN) with capabilities.add.
- Access to hardware such as a GPU: request it through the vendor’s device plugin resource instead of exposing every host device.
- Nothing identifiable: remove the flag in a test environment and watch what fails. Copied Helm values and old examples sometimes set it for no reason.
Then change the pod template of the owning workload, on every container that sets it:
spec:
template:
spec:
containers:
- name: agent
securityContext:
privileged: false # or delete the line: the default is false
capabilities:
drop: ["ALL"]
add: ["NET_ADMIN"] # only what the process really needs
For an urgent change outside GitOps, this patch matches the container by name and triggers a rollout:
kubectl -n shop patch deployment web --patch '{"spec":{"template":{"spec":{"containers":[{"name":"web","securityContext":{"privileged":false}}]}}}}'
If Argo CD, Flux or Helm owns the object, change the source instead, or the next sync reverts the patch.
Enforce it with Pod Security Admission
Pod Security Admission is built into Kubernetes. The baseline profile allows privileged only when it is unset or false. Start with warn and audit, then enforce once the warnings stop:
kubectl label --overwrite ns shop
pod-security.kubernetes.io/warn=baseline
pod-security.kubernetes.io/audit=baseline
kubectl label --overwrite ns shop
pod-security.kubernetes.io/enforce=baseline
pod-security.kubernetes.io/enforce-version=latest
Baseline also restricts capabilities.add to a short list (including CHOWN, KILL, NET_BIND_SERVICE, SETUID and SETGID). NET_ADMIN and SYS_ADMIN are not on it, so infrastructure workloads that genuinely need them belong in a separate namespace labelled enforce=privileged, with tight RBAC on who can deploy there.
AKS and Azure Arc: Azure Policy
The policy definition accepts the effects Audit, Deny and Disabled, and supports excludedContainers and excludedImages parameters for documented exceptions. It needs the Azure Policy add-on or extension on the cluster:
az policy assignment create
--name no-privileged-containers
--policy 95edb821-ddaf-4404-9732-666045e056b4
--scope /subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.ContainerService/managedClusters/<cluster>
--params '{"effect":{"value":"Deny"}}'
Amazon ECS
Create a new task definition revision without privileged mode, then point the service at it. From the CLI:
aws ecs describe-task-definition --task-definition web --query taskDefinition
| jq 'del(.taskDefinitionArn, .revision, .status, .requiresAttributes,
.compatibilities, .registeredAt, .registeredBy)
| .containerDefinitions |= map(.privileged = false)' > web-td.json
aws ecs register-task-definition --cli-input-json file://web-td.json
aws ecs update-service --cluster prod --service web --task-definition web
If register-task-definition rejects another read-only key, delete it from the file too. Without a revision number, update-service uses the latest ACTIVE revision. If the container needs a narrower permission, use linuxParameters (EC2 capacity only; Fargate only supports adding SYS_PTRACE and does not support devices):
"linuxParameters": {
"capabilities": { "add": ["NET_ADMIN"] },
"devices": [{ "hostPath": "/dev/ttyUSB0", "containerPath": "/dev/ttyUSB0", "permissions": ["read", "write"] }]
}
If CloudFormation, CDK or Terraform manages the task definition, make the change there instead.
Docker and Docker Compose
Privileged mode cannot be switched off on a running container. Recreate it with targeted flags instead:
docker rm -f agent
docker run -d --name agent --cap-add NET_ADMIN --device /dev/ttyUSB0 registry.example.com/agent:2.1
In Compose, delete privileged: true from the service, add cap_add or devices entries if needed, and run docker compose up -d so the changed service is recreated.
How to verify the fix and rescan
- Re-run the jq queries and the baseline dry-run. The workload should not appear, and the namespace should return no warnings.
- Run trivy config ./k8s/ on manifests and trivy k8s –scanners misconfig on the cluster; KSV-0017 should be gone for that resource.
- For ECS, confirm the service runs the new revision with aws ecs describe-services –cluster prod –services web –query ‘services[].taskDefinition’. ECS.4 is change triggered, so it re-evaluates when the new revision is registered.
- For Docker, docker inspect should show privileged=false and check_5_5 should pass. Defender for Cloud updates after its next policy compliance evaluation, which is not instant.
What can break and how to roll back
- Mounting filesystems inside the container fails; Docker’s own example shows mount returning “permission denied” without privileged mode.
- Device access such as USB, serial, FUSE or GPUs stops until you map the specific device or use a device plugin.
- Network and firewall changes (iptables, VPN tunnels, CNI plugins) fail without NET_ADMIN.
- Docker-in-Docker build runners stop working; the official Docker-in-Docker image documents –privileged as required. Move those builds to a builder that does not need it, or to dedicated, isolated build nodes.
- Enforce mode stalls rollouts quietly. Pod Security Admission checks pods, not Deployments, so the Deployment is accepted but its pods are not created. Check kubectl -n <namespace> get events.
To roll back, revert the manifest or run kubectl -n <namespace> rollout undo deployment/<name>; remove the enforce label with kubectl label ns <namespace> pod-security.kubernetes.io/enforce-. On ECS, run update-service with the previous family:revision. On Docker, recreate the container with its old flags.
Common false positive reasons
- Debug containers. kubectl debug –profile=sysadmin adds a privileged ephemeral container. Ephemeral containers cannot be removed, so Defender flags the pod until it is replaced. Trivy’s KSV-0017 does not check ephemeral containers.
- Unused ECS revisions. ECS.4 evaluates the latest active revision, not what services run. A privileged revision nobody deploys still fails; deregister it with aws ecs deregister-task-definition –task-definition web:12.
- Helm chart defaults. Scanning a chart without your values file flags defaults you override in production. Pass your values with –helm-values.
- Infrastructure agents such as CNI, CSI node plugins and some security or monitoring DaemonSets. These are accepted exceptions, not false positives, and should be documented as such. More patterns are in container scanner false positives.
FAQ
Do I need privileged: false, or can I just remove the field?
Either works. Kubernetes defaults it to false and none of the four checks fails on a missing value; an explicit false documents intent.
Is this the same as allowPrivilegeEscalation?
No. privileged hands the container host-level powers from the start; allowPrivilegeEscalation controls whether a process can gain more privileges later through setuid binaries. They are separate findings, and Kubernetes rejects allowPrivilegeEscalation: false on a privileged container.
Can ECS.4 fail on Fargate tasks?
AWS does not support privileged mode for tasks on Fargate or for Windows containers, so a task definition that fails ECS.4 is meant for Linux containers on EC2 or other non-Fargate capacity.
Does adding SYS_ADMIN instead of privileged solve the problem?
It clears this finding, but Trivy flags it separately as KSV-0005 (SYS_ADMIN capability added) and describes SYS_ADMIN as equivalent to root. Treat it as an exception to review, not a fix.
Tracking this finding across many clusters
A privileged flag copied into one chart tends to show up in every cluster that uses it. If your container scanners are among the 16 that SITEY imports findings from, the results can sit in one self-hosted list next to your host vulnerabilities. Duplicates are merged per scanner, not across scanners, so a workload flagged by both Defender and Trivy appears twice, and AI triage suggests likely false positives with evidence while a human makes the final call.