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 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)"
    ][]'
  2. 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 NAME

    In the opened spec, under each affected containers or initContainers entry:

    • Remove the line:
      securityContext:
      privileged: true
      or, if securityContext has 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
  3. 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_NAME

    In each affected container’s securityContext, remove privileged: true as in step 2, optionally replacing it with minimal capabilities.add entries if required. Save and exit to apply.

  4. 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: true or replace with minimal securityContext.capabilities.add as in step 2.
    • Re‑apply if you manage manifests manually:
      kubectl apply -f path/to/manifest.yaml
  5. Wait for non‑privileged replacements to be running and old pods terminated (run on any machine with kubectl access):

    kubectl get pods --all-namespaces

    Confirm newly created pods for each modified workload are in Running (or expected) state without privileged enabled in their spec.

  6. 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=true and no line shows privileged=true is_compliant=false for non‑system namespaces.

Using kubectl

On any machine with kubectl access to the cluster:

  1. 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.

  2. 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.yaml

    In /tmp/my-app-deploy.yaml, under each affected container (and/or initContainer), change:

    securityContext:
    privileged: true

    to either remove privileged entirely:

    securityContext: {}

    or, if specific capabilities are required, replace with:

    securityContext:
    privileged: false
    capabilities:
    add:
    - NET_ADMIN
    - SYS_TIME

    (Adjust capability names to the minimal set actually needed, or remove capabilities entirely if none are required.)

    Then apply:

    kubectl -n my-namespace apply -f /tmp/my-app-deploy.yaml
  3. 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.yaml

    Edit /tmp/my-pod.yaml:

    • Remove metadata.resourceVersion, metadata.uid, metadata.creationTimestamp, metadata.managedFields, metadata.ownerReferences, and status sections.
    • Under each affected container/initContainer, change/remove securityContext.privileged: true as described above.

    Then delete and recreate:

    kubectl -n my-namespace delete pod my-pod
    kubectl -n my-namespace apply -f /tmp/my-pod.yaml
  4. 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=true and overall output is is_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
'