Skip to main content

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​

Manual Steps
  1. 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)"
    ][]'
  2. For each affected pod, determine the owning controller (any machine with kubectl access). Replace NAMESPACE and POD_NAME:

    kubectl get pod POD_NAME -n NAMESPACE -o jsonpath='{.metadata.ownerReferences}'
    • If empty: pod is standalone.
    • If present: note .kind and .name to edit the right controller (e.g., Deployment, DaemonSet, StatefulSet).
  3. Edit the owning controller or pod manifest to remove privileged mode (any machine with kubectl access). Examples (run one appropriate command per affected workload):

    • Deployment:
      kubectl -n NAMESPACE edit deployment DEPLOYMENT_NAME
    • DaemonSet:
      kubectl -n NAMESPACE edit daemonset DAEMONSET_NAME
    • StatefulSet:
      kubectl -n NAMESPACE edit statefulset STATEFULSET_NAME
    • Standalone Pod:
      kubectl -n NAMESPACE edit pod POD_NAME

    In the opened manifest, locate each container (including initContainers) and remove or change:

    securityContext:
    privileged: true

    to either remove privileged entirely or explicitly set:

    securityContext:
    privileged: false

    If specific kernel capabilities are required, add only those instead of privileged:

    securityContext:
    capabilities:
    add:
    - NET_ADMIN
    - SYS_TIME
  4. If workloads are managed via manifest files or GitOps, update the source manifests (on your config/IaC repo workstation) so changes are not reverted. In each relevant YAML file, apply the same securityContext edits as in step 3, then re-apply:

    kubectl apply -f path/to/manifest.yaml
  5. Allow pods to be recreated with the new spec. For controllers this happens automatically; for standalone pods you may need to delete and recreate them if the spec is immutable (any machine with kubectl access):

    kubectl -n NAMESPACE delete pod POD_NAME
    # Recreate from updated manifest if needed
    kubectl -n NAMESPACE apply -f updated-pod.yaml
  6. Verify no containers are running privileged (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'

    Ensure the output is either is_compliant=true or that every listed line ends with is_compliant=true.

Using kubectl

On any machine with kubectl access:

  1. Identify the owning controller of the privileged pod

Use the audit output to see owner=.... If you only have the pod name, re-fetch it:

kubectl get pod <POD_NAME> -n <NAMESPACE> -o jsonpath='{.metadata.ownerReferences[*].kind}{" "}{.metadata.ownerReferences[*].name}{" "}{.metadata.ownerReferences[*].uid}{"\n"}'

If this prints a kind such as Deployment/StatefulSet/DaemonSet/Job, you must edit that controller, not the pod.

  1. Export the current manifest for the owning controller

Example for a Deployment (substitute the correct kind/name/namespace):

kubectl get deployment <DEPLOYMENT_NAME> -n <NAMESPACE> -o yaml > /tmp/<DEPLOYMENT_NAME>.yaml

For other controller types:

kubectl get statefulset <STATEFULSET_NAME> -n <NAMESPACE> -o yaml > /tmp/<STATEFULSET_NAME>.yaml
kubectl get daemonset <DAEMONSET_NAME> -n <NAMESPACE> -o yaml > /tmp/<DAEMONSET_NAME>.yaml
kubectl get job <JOB_NAME> -n <NAMESPACE> -o yaml > /tmp/<JOB_NAME>.yaml
  1. Edit the manifest to remove privileged mode and (optionally) add specific capabilities

Open the exported file and, for every container (including initContainers) that has securityContext.privileged: true, remove that field or set it to false. If you need specific kernel capabilities, set them explicitly.

Minimal example patch in the pod template:

spec:
template:
spec:
containers:
- name: <CONTAINER_NAME>
image: <IMAGE>
securityContext:
# remove this line if present:
# privileged: true
# optionally add only required capabilities:
capabilities:
add:
- NET_ADMIN
- SYS_TIME
# if any initContainers are privileged, fix them similarly:
initContainers:
- name: <INIT_CONTAINER_NAME>
image: <IMAGE>
securityContext:
# privileged: true # remove this
capabilities:
add:
- NET_ADMIN

Ensure no container has privileged: true under securityContext.

  1. Apply the updated manifest
kubectl apply -f /tmp/<CONTROLLER_FILE>.yaml

This will cause a rolling update for Deployments/StatefulSets/DaemonSets; pods will be recreated with the new security context.

  1. (Optional) Delete any existing non-compliant pods to accelerate rollout

For a Deployment:

kubectl delete pod -n <NAMESPACE> -l app=<APP_LABEL>

Adjust the label selector to match your workload.

  1. Verification

After the rollout completes, rerun the benchmark audit command 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 is_compliant=true, or that the output is is_compliant=true.

Automation
#!/usr/bin/env bash
# Purpose: Remove privileged=true from all non-system Pods (and their controllers)
# Platform: GKE (any machine with kubectl access)

set -euo pipefail

# REQUIREMENTS:
# - kubectl configured with sufficient RBAC to get/patch resources cluster-wide
# - jq installed

echo "[INFO] Discovering privileged pods (excluding kube-system, kube-public, kube-node-lease)..."

privileged_pods_json="$(
kubectl get pods --all-namespaces -o json | jq '
.items[]
| select(.metadata.namespace as $n
| ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| . as $pod
| ((.spec.containers // []) + (.spec.initContainers // []))[]
| select(.securityContext.privileged == true)
| {
ns: $pod.metadata.namespace,
pod: $pod.metadata.name,
ownerRefs: ($pod.metadata.ownerReferences // []),
container: .name
}'
)"

if [[ -z "$privileged_pods_json" ]]; then
echo "[INFO] No privileged containers found."
else
echo "[INFO] Found privileged containers. Grouping by owning controller..."
fi

# Build a unique list of owner controllers and standalone pods
# Output format: kind ns name
controllers_file="$(mktemp)"
pods_file="$(mktemp)"

# Iterate over each privileged container record
jq -c '.' <<< "$privileged_pods_json" | while read -r rec; do
ns=$(jq -r '.ns' <<< "$rec")
pod=$(jq -r '.pod' <<< "$rec")

# Determine if pod has a controller
owner_json=$(kubectl get pod "$pod" -n "$ns" -o json | jq -c '
[(.metadata.ownerReferences // [])[] | select(.controller == true)] | first // null
')
if [[ "$owner_json" == "null" ]]; then
echo "Pod $ns/$pod" >> "$pods_file"
else
kind=$(jq -r '.kind' <<< "$owner_json")
name=$(jq -r '.name' <<< "$owner_json")
echo "$kind $ns $name" >> "$controllers_file"
fi
done

# De-duplicate
if [[ -s "$controllers_file" ]]; then
sort -u "$controllers_file" -o "$controllers_file"
fi
if [[ -s "$pods_file" ]]; then
sort -u "$pods_file" -o "$pods_file"
fi

patch_template="$(mktemp)"
cat > "$patch_template" <<'EOF'
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "__ALL__",
"securityContext": {
"privileged": false
}
}
],
"initContainers": [
{
"name": "__ALL__",
"securityContext": {
"privileged": false
}
}
]
}
}
}
}
EOF

echo "[INFO] Patching controller objects to remove privileged=true..."

# Helper: patch a controller
patch_controller() {
local kind="$1" ns="$2" name="$3"

# Use jsonpatch to avoid wiping other fields; set privileged to false where present.
# For idempotence, failures (e.g., resource gone) are tolerated.
kubectl get "$kind" "$name" -n "$ns" -o json | \
jq '
(.. | objects | select(has("securityContext")) | select(has("privileged"))) |=
(.privileged = false)
' | kubectl apply -f - >/dev/null 2>&1 || true
}

if [[ -s "$controllers_file" ]]; then
while read -r kind ns name; do
echo "[INFO] Patching controller: kind=$kind ns=$ns name=$name"
patch_controller "$kind" "$ns" "$name"
done < "$controllers_file"
else
echo "[INFO] No controllers to patch."
fi

echo "[INFO] Patching standalone pods to remove privileged=true (will not affect controller-managed pods)."

if [[ -s "$pods_file" ]]; then
while read -r _ns_pod; do
ns="${_ns_pod%% *}"
pod="${_ns_pod##* }"

# Patch the live Pod only. This is best-effort; controller will override on reschedule.
kubectl get pod "$pod" -n "$ns" -o json | \
jq '
(.. | objects | select(has("securityContext")) | select(has("privileged"))) |=
(.privileged = false)
' | kubectl apply -f - >/dev/null 2>&1 || true
echo "[INFO] Patched standalone pod: ns=$ns pod=$pod"
done < <(sed 's/^Pod //g' "$pods_file")
else
echo "[INFO] No standalone pods to patch."
fi

rm -f "$controllers_file" "$pods_file" "$patch_template"

echo "[INFO] Waiting for updated workloads to roll out..."
kubectl rollout status deployment --all --all-namespaces >/dev/null 2>&1 || true
kubectl rollout status daemonset --all --all-namespaces >/dev/null 2>&1 || true
kubectl rollout status statefulset --all --all-namespaces >/dev/null 2>&1 || true

echo "[INFO] Verification: re-running privileged container audit..."
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 | map(select(. | contains("privileged=true"))) | length) == 0
then "is_compliant=true"
else ($rows[] | select(. | contains("privileged=true")))
end
'