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)"
    + " is_compliant=false"
    ][]'
  2. 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.
  3. 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-deployment

    In 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: false
      capabilities:
      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.yaml

    Edit /tmp/example-pod.yaml and, for each offending container, remove securityContext.privileged: true and optionally add minimal required capabilities as above. Then recreate (pods themselves are immutable):

    kubectl -n example-namespace delete pod example-pod
    kubectl -n example-namespace apply -f /tmp/example-pod.yaml
  4. 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: true setting and replace with a minimal securityContext.capabilities.add block 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 capabilities
      helm -n example-namespace upgrade example-release example-chart-repo/example-chart \
      -f /tmp/example-release-values.yaml
  5. 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 -- id
      kubectl -n example-namespace exec -it example-pod -c example-container -- capsh --print || true
    • Prefer combinations of:
      • Specific securityContext.capabilities.add entries.
      • hostPath volumes with tight path and readOnly: true where possible.
      • runAsNonRoot: true, readOnlyRootFilesystem: true when compatible.
        Only retain privileged mode if a documented, risk-accepted exception is granted.
  6. 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=true or that all listed lines end with privileged=false is_compliant=true.

Using kubectl

On any machine with kubectl access:

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

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

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

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

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