Containers Should Not Run In Privileged Mode
More Info:​
Verifies no container sets securityContext.privileged=true. A privileged container can compromise the node and every other pod scheduled on it.
Risk Level​
Critical
Address​
Security
Compliance Standards​
- Cloudanix Best Practice
Triage and Remediation​
- Remediation
Remediation​
Manual Steps
-
Identify all privileged containers (any machine with kubectl access)
kubectl get pods --all-namespaces -o json | jq -r '[ .items[]| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)| .metadata as $m| (.spec.nodeName // "") as $node| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own| ((.spec.containers // []) + (.spec.initContainers // []))[]| (.securityContext.privileged // false) as $priv| select($priv == true)| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)+ (if $node == "" then "" else " node=\($node)" end)+ (if $labels == "" then "" else " labels=\($labels)" end)+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)+ " container=\(.name) image=\(.image) privileged=\($priv)"+ " is_compliant=false"][]' -
Determine the owning workload for each offending pod (any machine with kubectl access)
For a specific pod from the list (replace placeholders with actual values from step 1):NAMESPACE="example-namespace"POD="example-pod"kubectl get pod "$POD" -n "$NAMESPACE" -o jsonpath='{.metadata.ownerReferences}'- If there is an ownerReference (Deployment, StatefulSet, DaemonSet, Job, etc.), plan to edit that controller.
- If there is no ownerReference, the pod is standalone; edit or recreate that Pod manifest.
-
Edit the controller or pod manifest to remove privileged and optionally add specific capabilities (any machine with kubectl access)
a) For a Deployment (similar for StatefulSet/DaemonSet/Job, adjust kind):kubectl -n example-namespace edit deployment example-deploymentIn the opened manifest, for each affected container (including any
initContainers):- Locate and remove or change:
securityContext:privileged: true
- If the workload needs specific kernel capabilities, replace with only those capabilities, for example:
securityContext:privileged: falsecapabilities:add:- NET_ADMIN- SYS_TIME
b) For a standalone Pod (not recommended for long-lived workloads but sometimes present):
kubectl -n example-namespace get pod example-pod -o yaml > /tmp/example-pod.yamlEdit
/tmp/example-pod.yamland, for each offending container, removesecurityContext.privileged: trueand optionally add minimal required capabilities as above. Then recreate (pods themselves are immutable):kubectl -n example-namespace delete pod example-podkubectl -n example-namespace apply -f /tmp/example-pod.yaml - Locate and remove or change:
-
For Helm-managed workloads, update values instead of live-editing (any machine with kubectl and helm access)
- Identify Helm release and chart:
kubectl -n example-namespace get pod example-pod -o jsonpath='{.metadata.labels.helm\.sh/release}'
- Fetch current values, update to remove any
privileged: truesetting and replace with a minimalsecurityContext.capabilities.addblock if needed:helm -n example-namespace get values example-release > /tmp/example-release-values.yaml# Edit /tmp/example-release-values.yaml to remove privileged: true and add only necessary capabilitieshelm -n example-namespace upgrade example-release example-chart-repo/example-chart \-f /tmp/example-release-values.yaml
- Identify Helm release and chart:
-
For workloads that genuinely require broad host interaction, review design instead of defaulting to privileged (any machine with kubectl access)
- Inspect container permissions and behavior to see what it actually needs:
kubectl -n example-namespace exec -it example-pod -c example-container -- idkubectl -n example-namespace exec -it example-pod -c example-container -- capsh --print || true
- Prefer combinations of:
- Specific
securityContext.capabilities.addentries. hostPathvolumes with tightpathandreadOnly: truewhere possible.runAsNonRoot: true,readOnlyRootFilesystem: truewhen compatible.
Only retain privileged mode if a documented, risk-accepted exception is granted.
- Specific
- Inspect container permissions and behavior to see what it actually needs:
-
Verify no remaining privileged containers (any machine with kubectl access)
kubectl get pods --all-namespaces -o json | jq -r '[ .items[]| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)| .metadata as $m| (.spec.nodeName // "") as $node| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own| ((.spec.containers // []) + (.spec.initContainers // []))[]| (.securityContext.privileged // false) as $priv| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)+ (if $node == "" then "" else " node=\($node)" end)+ (if $labels == "" then "" else " labels=\($labels)" end)+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)+ " container=\(.name) image=\(.image) privileged=\($priv)"+ " is_compliant=\(if $priv then "false" else "true" end)"] as $rows| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'Confirm that either the output is exactly
is_compliant=trueor that all listed lines end withprivileged=false is_compliant=true.
Using kubectl
On any machine with kubectl access:
- Identify the offending pod and its controller
Use the audit output line for the failing pod to see the owner= field. Example:
kind=Pod ns=app-namespace name=app-pod-123 ... owner=Deployment/app-namespace/app-deploy/...
If owner= is present, you must edit that owner (Deployment, StatefulSet, DaemonSet, Job, CronJob). If there is no owner, edit the Pod manifest directly (or the Git/IaC source that owns it).
- Export the current manifest for the owner
Example for a Deployment:
kubectl -n app-namespace get deploy app-deploy -o yaml > app-deploy.yaml
(StatefulSet: get statefulset, DaemonSet: get daemonset, Job: get job, CronJob: get cronjob. For an unmanaged Pod: get pod.)
- Edit the manifest to remove privileged and, if needed, add fine-grained capabilities
Open the file and, for every affected container (including initContainers), remove the privileged: true setting under securityContext. Optionally add only the specific capabilities required.
Example patch inside the Deployment spec:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-deploy
namespace: app-namespace
spec:
template:
spec:
containers:
- name: app-container
image: myregistry.azurecr.io/app:1.0
securityContext:
# REMOVE this line:
# privileged: true
# OPTIONAL: replace with only required capabilities:
capabilities:
add:
- NET_ADMIN
- SYS_TIME
initContainers:
- name: init-sidecar
image: myregistry.azurecr.io/init:1.0
securityContext:
# REMOVE this line:
# privileged: true
capabilities:
add:
- NET_RAW
Ensure there is no remaining privileged: true under any containers or initContainers.
- Apply the updated manifest
kubectl apply -f app-deploy.yaml
For an unmanaged Pod:
kubectl delete -n app-namespace pod app-pod-123
kubectl apply -f app-pod.yaml
(Deleting and recreating is required because Pods are immutable.)
- Verification
After the controllers have recreated pods, re-run the audit from any machine with kubectl access:
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| (.spec.nodeName // "") as $node
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| ((.spec.containers // []) + (.spec.initContainers // []))[]
| (.securityContext.privileged // false) as $priv
| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ (if $node == "" then "" else " node=\($node)" end)
+ (if $labels == "" then "" else " labels=\($labels)" end)
+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)
+ " container=\(.name) image=\(.image) privileged=\($priv)"
+ " is_compliant=\(if $priv then "false" else "true" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'
Confirm that all listed containers show privileged=false and the final output is is_compliant=true.
Automation
#!/usr/bin/env bash
set -euo pipefail
# Remediation for: Containers Should Not Run In Privileged Mode (CBP C1.1)
# Scope: Any machine with kubectl access to the AKS cluster
# Requirement: kubectl, jq, and yq (v4, https://github.com/mikefarah/yq) installed and in PATH
# This script:
# 1. Finds all pods (excluding system namespaces) with privileged containers.
# 2. Identifies the owning workload (Deployment/DaemonSet/StatefulSet/Job/CronJob) where possible.
# 3. Patches those workloads to remove securityContext.privileged=true from containers and initContainers.
# 4. Prints manual follow-up for pods that cannot be auto-fixed (e.g., bare Pods).
# 5. Re-runs the audit command to verify.
# ----- config -----
WORK_DIR="$(pwd)/privileged-remediation-$(date +%Y%m%d-%H%M%S)"
mkdir -p "${WORK_DIR}"
echo "Working directory: ${WORK_DIR}"
# ----- helper: run audit and capture list of offending pods -----
echo "Discovering pods with privileged containers..."
AUDIT_JSON="${WORK_DIR}/pods.json"
kubectl get pods --all-namespaces -o json > "${AUDIT_JSON}"
# Build a JSON list of offending containers with ownership data
OFFENDERS_JSON="${WORK_DIR}/offenders.json"
jq '
.items[]
| select(.metadata.namespace as $n
| ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| [ ($m.ownerReferences // [])[] | select(.controller) ] | first as $own
| (.spec.containers // [] + .spec.initContainers // [])[]
| select((.securityContext.privileged // false) == true)
| {
podNamespace: $m.namespace,
podName: $m.name,
containerName: .name,
ownerKind: ($own.kind // null),
ownerName: ($own.name // null)
}
' "${AUDIT_JSON}" | jq -s '.' > "${OFFENDERS_JSON}"
if [[ "$(jq 'length' "${OFFENDERS_JSON}")" -eq 0 ]]; then
echo "No privileged containers found outside system namespaces. Cluster is compliant."
exit 0
fi
echo "Found $(jq 'length' "${OFFENDERS_JSON}") privileged container entries. Beginning remediation..."
# ----- function: patch a specific workload to remove privileged -----
patch_workload() {
local ns="$1"
local kind="$2"
local name="$3"
local base="${WORK_DIR}/${ns}-${kind}-${name}"
local orig_yaml="${base}-orig.yaml"
local patched_yaml="${base}-patched.yaml"
# Retrieve the workload manifest
if ! kubectl get "${kind}" "${name}" -n "${ns}" -o yaml > "${orig_yaml}" 2>/dev/null; then
echo "WARN: Failed to get ${kind}/${ns}/${name}; skipping."
return
fi
cp "${orig_yaml}" "${patched_yaml}"
# Remove .securityContext.privileged from all containers and initContainers
# in .spec.template.spec (covers Deployments, DS, SS, Jobs, CronJobs)
yq eval '
(.. | select(has("containers")).containers[]? // {}) |= (
.securityContext |= ( . // {} | with(.privileged; . = null) | with_entries(select(.value != null)))
) |
(.. | select(has("initContainers")).initContainers[]? // {}) |= (
.securityContext |= ( . // {} | with(.privileged; . = null) | with_entries(select(.value != null)))
)
' "${patched_yaml}" > "${patched_yaml}.tmp" && mv "${patched_yaml}.tmp" "${patched_yaml}"
# If no change, skip apply
if diff -q "${orig_yaml}" "${patched_yaml}" >/dev/null; then
echo "No privileged fields found in ${kind}/${ns}/${name}; nothing to patch."
return
fi
echo "Patching ${kind}/${ns}/${name} to remove privileged=true from containers..."
kubectl apply -f "${patched_yaml}"
}
# ----- main remediation loop -----
# Collect unique owner objects to patch
OWNERS_JSON="${WORK_DIR}/owners.json"
jq '
map(select(.ownerKind != null and .ownerName != null))
| map({podNamespace, ownerKind, ownerName})
| unique
' "${OFFENDERS_JSON}" > "${OWNERS_JSON}"
OWNERS_COUNT="$(jq 'length' "${OWNERS_JSON}")"
if [[ "${OWNERS_COUNT}" -gt 0 ]]; then
echo "Patching ${OWNERS_COUNT} owning workloads (Deployments/DaemonSets/StatefulSets/Jobs/CronJobs)..."
for i in $(seq 0 $((OWNERS_COUNT - 1))); do
ns="$(jq -r ".[$i].podNamespace" "${OWNERS_JSON}")"
kind="$(jq -r ".[$i].ownerKind" "${OWNERS_JSON}")"
name="$(jq -r ".[$i].ownerName" "${OWNERS_JSON}")"
# Only patch expected workload kinds
case "${kind}" in
Deployment|DaemonSet|StatefulSet|Job|CronJob)
patch_workload "${ns}" "${kind}" "${name}"
;;
*)
echo "WARN: Unsupported owner kind ${kind} for ${ns}/${name}; manual review required."
;;
esac
done
else
echo "No controller-owned workloads detected; all offending pods appear to be bare Pods."
fi
# ----- manual follow-up for bare Pods or unsupported owners -----
BARE_PODS_TXT="${WORK_DIR}/manual-bare-pods.txt"
jq -r '
map(select(.ownerKind == null or .ownerName == null))
| group_by(.podNamespace, .podName)
| .[]
| "Pod " + .[0].podNamespace + "/" + .[0].podName
' "${OFFENDERS_JSON}" | sort -u > "${BARE_PODS_TXT}" || true
if [[ -s "${BARE_PODS_TXT}" ]]; then
echo
echo "The following Pods have privileged containers but are not owned by a supported controller."
echo "They must be fixed manually by editing/recreating the Pod manifests:"
cat "${BARE_PODS_TXT}"
echo
echo "For each listed Pod, obtain the manifest, remove securityContext.privileged: true from"
echo "all containers and initContainers (and, if needed, add only specific capabilities via"
echo "securityContext.capabilities.add), then recreate the Pod from a controller or as needed."
fi
# ----- verification (re-run the authoritative audit) -----
echo
echo "Re-running compliance audit to verify remediation..."
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| (.spec.nodeName // "") as $node
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| ((.spec.containers // []) + (.spec.initContainers // []))[]
| (.securityContext.privileged // false) as $priv
| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ (if $node == "" then "" else " node=\($node)" end)
+ (if $labels == "" then "" else " labels=\($labels)" end)
+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)
+ " container=\(.name) image=\(.image) privileged=\($priv)"
+ " is_compliant=\(if $priv then "false" else "true" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'