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 pods (run on 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)| "ns=\($m.namespace) pod=\($m.name) container=\(.name) ownerKind=\($own.kind) ownerName=\($own.name)"][]' -
For each affected pod that is part of a higher‑level controller (Deployment, DaemonSet, StatefulSet, Job, CronJob), edit the controller manifest to remove privileged mode (run on any machine with kubectl access, replace KIND/NS/NAME accordingly):
kubectl -n NAMESPACE edit KIND NAMEIn the opened spec, under each affected
containersorinitContainersentry:- Remove the line:
or, ifsecurityContext:privileged: true
securityContexthas multiple fields, remove only:privileged: true - If the workload truly needs some low‑level access, add only the required capabilities instead:
securityContext:capabilities:add:- NET_ADMIN # example, adjust as needed- SYS_TIME
- Remove the line:
-
For affected standalone Pods without an owning controller, edit the Pod spec directly (run on any machine with kubectl access):
kubectl -n NAMESPACE edit pod POD_NAMEIn each affected container’s
securityContext, removeprivileged: trueas in step 2, optionally replacing it with minimalcapabilities.addentries if required. Save and exit to apply. -
If any privileged setting comes from a shared Pod template (e.g., Helm chart or GitOps manifest), locate and update that source manifest so changes persist (run where your manifests live):
- Open the manifest defining the controller (e.g.,
deployment.yaml,daemonset.yaml). - Edit the container spec to remove
securityContext.privileged: trueor replace with minimalsecurityContext.capabilities.addas in step 2. - Re‑apply if you manage manifests manually:
kubectl apply -f path/to/manifest.yaml
- Open the manifest defining the controller (e.g.,
-
Wait for non‑privileged replacements to be running and old pods terminated (run on any machine with kubectl access):
kubectl get pods --all-namespacesConfirm newly created pods for each modified workload are in
Running(or expected) state withoutprivilegedenabled in their spec. -
Verify remediation (run on 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
is_compliant=trueand no line showsprivileged=true is_compliant=falsefor non‑system namespaces.
Using kubectl
On any machine with kubectl access to the cluster:
-
Identify the owning controller (from the audit output’s
owner=field). If the pod is controlled by a Deployment/DaemonSet/StatefulSet/etc., you must edit that controller, not the pod. -
Edit the controller manifest to remove privileged mode and (optionally) add only required capabilities.
Example for a Deployment (replace values as needed):
kubectl -n my-namespace get deploy my-app -o yaml > /tmp/my-app-deploy.yamlIn
/tmp/my-app-deploy.yaml, under each affected container (and/or initContainer), change:securityContext:privileged: trueto either remove
privilegedentirely:securityContext: {}or, if specific capabilities are required, replace with:
securityContext:privileged: falsecapabilities:add:- NET_ADMIN- SYS_TIME(Adjust capability names to the minimal set actually needed, or remove
capabilitiesentirely if none are required.)Then apply:
kubectl -n my-namespace apply -f /tmp/my-app-deploy.yaml -
If the pod is not controlled by any higher-level object (
owner=is empty), edit the pod spec directly and recreate it:kubectl -n my-namespace get pod my-pod -o yaml > /tmp/my-pod.yamlEdit
/tmp/my-pod.yaml:- Remove
metadata.resourceVersion,metadata.uid,metadata.creationTimestamp,metadata.managedFields,metadata.ownerReferences, andstatussections. - Under each affected container/initContainer, change/remove
securityContext.privileged: trueas described above.
Then delete and recreate:
kubectl -n my-namespace delete pod my-podkubectl -n my-namespace apply -f /tmp/my-pod.yaml - Remove
-
Verification (on 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 no line shows
privileged=trueand overall output isis_compliant=true.
Automation
#!/usr/bin/env bash
#
# Remediate privileged containers in an EKS cluster by updating their owning workload manifests.
# Runs from: any machine with kubectl access and jq installed.
# Requirements: kubectl, jq, bash 4+
#
# Behavior:
# - Scans all namespaces except kube-system, kube-public, kube-node-lease
# - Identifies pods with containers or initContainers having securityContext.privileged=true
# - Determines the controlling owner (Deployment, StatefulSet, DaemonSet, Job, CronJob, ReplicaSet, ReplicationController)
# - Patches the owner spec templates to remove privileged: true and optionally add explicit capabilities instead
# - Is idempotent: safe to run multiple times
# - Verifies: re-runs the benchmark audit filter at the end
set -euo pipefail
# ---- configuration ----
# If you want to also explicitly drop privileged securityContext blocks entirely (not just privileged: true),
# set this to "true". Otherwise, only privileged: true will be removed.
STRIP_FULL_PRIV_BLOCK="${STRIP_FULL_PRIV_BLOCK:-false}"
# Optionally define specific capabilities to add instead of full privilege
# Example:
# DEFAULT_CAPS='["NET_ADMIN","SYS_PTRACE"]'
# Leave empty ("") to skip adding capabilities.
DEFAULT_CAPS="${DEFAULT_CAPS:-""}"
# ---- helper functions ----
need_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "ERROR: '$1' is required but not found in PATH" >&2
exit 1
fi
}
need_cmd kubectl
need_cmd jq
echo "Discovering pods with privileged containers (excluding kube-system, kube-public, kube-node-lease)..."
# Collect pods with privileged containers and their owners in a stable JSON array
PODS_JSON="$(kubectl get pods --all-namespaces -o json | jq -c '
[
.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)
| {
namespace: $m.namespace,
podName: $m.name,
ownerKind: ($own.kind // "Pod"),
ownerName: ($own.name // $m.name),
ownerUID: ($own.uid // $m.uid)
}
] | unique
')"
if [[ "$(jq 'length' <<<"$PODS_JSON")" -eq 0 ]]; then
echo "No privileged containers found; cluster is already compliant."
exit 0
fi
echo "Found $(jq 'length' <<<"$PODS_JSON") pods with privileged containers. Identifying owning workloads..."
# Build a unique list of owners to patch
OWNERS_JSON="$(jq -c '
[
.[]
| {namespace, kind: .ownerKind, name: .ownerName}
] | unique
' <<<"$PODS_JSON")"
echo "Unique owners to patch:"
jq -r '.[] | "\(.kind) \(.namespace)/\(.name)"' <<<"$OWNERS_JSON"
patch_owner() {
local ns="$1"
local kind="$2"
local name="$3"
echo "Patching ${kind} ${ns}/${name}..."
# Get the current manifest
if ! kubectl get "$kind" "$name" -n "$ns" -o json >/tmp/owner.json 2>/dev/null; then
echo " WARNING: Unable to fetch ${kind} ${ns}/${name}, skipping." >&2
return
fi
# Work out where pod templates may live: spec.template (for most controllers),
# jobTemplate.spec.template (for CronJobs), etc.
# We patch all known template locations defensively.
PATCH="$(jq -c --argjson caps "${DEFAULT_CAPS:-null}" --arg stripFull "${STRIP_FULL_PRIV_BLOCK}" '
def strip_priv(sc):
if sc == null then null
else
(if (.privileged? == true) then del(.privileged) end)
| (if ($stripFull == "true" and (.privileged? // null) == null and (.capabilities? // null) == null and (.runAsUser? // null) == null and (.runAsGroup? // null) == null and (.runAsNonRoot? // null) == null and (.seLinuxOptions? // null) == null and (.windowsOptions? // null) == null) then empty else . end);
end;
def fix_containers(obj):
if obj == null then null
else obj
| (if has("containers") then .containers |=
map(
if (.securityContext.privileged? // false) == true then
.securityContext |= (
strip_priv(.)
| (if $caps != null and ($caps | length) > 0 then
.capabilities |= (. // {}) | .capabilities.add |= (($caps | unique) // [])
else .
end)
)
else . end
)
else . end)
| (if has("initContainers") then .initContainers |=
map(
if (.securityContext.privileged? // false) == true then
.securityContext |= (
strip_priv(.)
| (if $caps != null and ($caps | length) > 0 then
.capabilities |= (. // {}) | .capabilities.add |= (($caps | unique) // [])
else .
end)
)
else . end
)
else . end)
end;
{
"spec": (
.spec
# Deployment, StatefulSet, DaemonSet, ReplicaSet, ReplicationController, Job
| (if has("template") then .template |=
(. // {} | .spec |= fix_containers(.spec // {}))
else . end)
# CronJob: spec.jobTemplate.spec.template
| (if has("jobTemplate") then .jobTemplate |=
(. // {} | .spec |=
(. // {} | .template |=
(. // {} | .spec |= fix_containers(.spec // {}))
)
)
else . end)
)
}
' /tmp/owner.json)"
# If patch does not change anything (no privileged fields), skip
if [[ "$(jq -S '.' /tmp/owner.json)" == "$(kubectl get "$kind" "$name" -n "$ns" -o json | jq -S '.')" ]]; then
echo " No change detected for ${kind} ${ns}/${name}, skipping apply."
return
fi
# Apply patch
kubectl patch "$kind" "$name" -n "$ns" --type merge -p "$PATCH" >/dev/null
echo " Patched ${kind} ${ns}/${name}."
}
# Iterate owners and patch
echo "$OWNERS_JSON" | jq -c '.[]' | while read -r owner; do
ns="$(jq -r '.namespace' <<<"$owner")"
kind="$(jq -r '.kind' <<<"$owner")"
name="$(jq -r '.name' <<<"$owner")"
# Normalize common owner kinds to their canonical resources
case "$kind" in
ReplicaSet) kind="ReplicaSet" ;;
ReplicationController) kind="ReplicationController" ;;
Deployment) kind="Deployment" ;;
StatefulSet) kind="StatefulSet" ;;
DaemonSet) kind="DaemonSet" ;;
Job) kind="Job" ;;
CronJob) kind="CronJob" ;;
Pod) kind="Pod" ;; # bare pods (will be patched directly)
*)
echo " WARNING: Unsupported owner kind '$kind' for ${ns}/${name}, skipping." >&2
continue
;;
esac
patch_owner "$ns" "$kind" "$name"
done
echo "Waiting for workloads to roll out updated, non-privileged pods..."
# For deployments, statefulsets, daemonsets, and cronjobs/jobs we can at least wait for rollout where supported.
# This is best-effort and will not block forever due to timeout (300s) per resource.
echo "$OWNERS_JSON" | jq -c '.[]' | while read -r owner; do
ns="$(jq -r '.namespace' <<<"$owner")"
kind="$(jq -r '.kind' <<<"$owner")"
name="$(jq -r '.name' <<<"$owner")"
case "$kind" in
Deployment|StatefulSet|DaemonSet)
echo " Waiting for ${kind} ${ns}/${name} rollout..."
kubectl rollout status "$kind" "$name" -n "$ns" --timeout=300s || \
echo " WARNING: Rollout status for ${kind} ${ns}/${name} did not complete within timeout." >&2
;;
*)
# Jobs/CronJobs/Pods/RS/RC – no generic rollout wait
:
;;
esac
done
echo "Re-running privileged container audit for verification..."
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[]
end
'