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
CriticalAddress
SecurityCompliance Standards
- Cloudanix Best Practice
Triage and Remediation
- Remediation
Remediation
Manual Steps
Manual Steps
-
Identify all privileged containers
- 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) | "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)" ][]'
- Run on any machine with kubectl access:
-
For each offending pod, locate and edit the owning workload manifest
- Run on any machine with kubectl access, example for a Deployment owner from step 1:
# Get the current manifest kubectl -n <NAMESPACE> get deployment <DEPLOYMENT_NAME> -o yaml > /tmp/deployment-<DEPLOYMENT_NAME>.yaml - In
/tmp/deployment-<DEPLOYMENT_NAME>.yaml, under the affected container or initContainer, remove or change:securityContext: privileged: true - If specific kernel access is required, add only needed capabilities instead of privileged:
securityContext: capabilities: add: - NET_ADMIN # example; adjust to actual need
- Run on any machine with kubectl access, example for a Deployment owner from step 1:
-
Apply the updated manifest
- Run on any machine with kubectl access:
kubectl apply -f /tmp/deployment-<DEPLOYMENT_NAME>.yaml
- Run on any machine with kubectl access:
-
If the pod is not managed by a higher-level controller (standalone Pod)
- Run on any machine with kubectl access:
kubectl -n <NAMESPACE> get pod <POD_NAME> -o yaml > /tmp/pod-<POD_NAME>.yaml - Edit
/tmp/pod-<POD_NAME>.yamlto removesecurityContext.privileged: true(and optionally add minimalsecurityContext.capabilities.addas needed), then re-create the pod:kubectl -n <NAMESPACE> delete pod <POD_NAME> kubectl apply -f /tmp/pod-<POD_NAME>.yaml
- Run on any machine with kubectl access:
-
Repeat for all other controllers (DaemonSet, StatefulSet, Job, CronJob)
- Use the owner information from step 1 (e.g.
owner=DaemonSet/ns/name/uid) and follow steps 2–3, replacingdeploymentwith the appropriate kind:kubectl -n <NAMESPACE> get daemonset <DAEMONSET_NAME> -o yaml > /tmp/daemonset-<DAEMONSET_NAME>.yaml # edit to remove privileged and optionally add only required capabilities kubectl apply -f /tmp/daemonset-<DAEMONSET_NAME>.yaml
- Use the owner information from step 1 (e.g.
-
Verify no containers are still privileged
- 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.containers // []) + (.spec.initContainers // []))[] | (.securityContext.privileged // false) as $priv | "privileged=\($priv)" ] as $rows | if ([.[] | select(. == "privileged=true")] | length) == 0 then "is_compliant=true" else $rows[] end' - Confirm the output is
is_compliant=trueand that no line showsprivileged=true.
- Run on any machine with kubectl access:
Using kubectl
Using kubectl
On any machine with kubectl access:Decide whether to:Edit If specific capabilities are required, replace with a minimal capability set, for example:Apply the updated manifest:Repeat this pattern for other controller types, e.g.:For a standalone Pod that is not managed by a controller (uncommon in production), recreate it without The cluster is compliant when the output is only:
- Identify the offending pod and its owner
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
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| ((.spec.containers // []) + (.spec.initContainers // []))[]
| select(.securityContext.privileged == true)
| "ns=\($m.namespace) pod=\($m.name) ownerKind=\($own.kind) ownerName=\($own.name)"
][]'
- Edit a standalone Pod, or
- Edit the owning controller (Deployment/DaemonSet/StatefulSet/Job/CronJob, etc.).
- Remove
securityContext.privileged: truefrom the controller manifest
my-deployment in namespace my-namespace:kubectl -n my-namespace get deploy my-deployment -o yaml > /tmp/my-deployment.yaml
/tmp/my-deployment.yaml and, for each offending container (including initContainers), remove or change:securityContext:
privileged: true
securityContext:
capabilities:
add:
- NET_ADMIN
- SYS_TIME
kubectl apply -f /tmp/my-deployment.yaml
kubectl -n my-namespace get daemonset my-daemonset -o yaml > /tmp/my-daemonset.yaml
# edit to remove privileged: true, then:
kubectl apply -f /tmp/my-daemonset.yaml
securityContext.privileged: true:kubectl -n my-namespace get pod my-pod -o yaml > /tmp/my-pod.yaml
# edit: remove privileged: true and delete fields status, metadata.uid, metadata.resourceVersion, metadata.creationTimestamp, metadata.managedFields
kubectl delete pod -n my-namespace my-pod
kubectl apply -f /tmp/my-pod.yaml
- 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 | length) == 0 then "is_compliant=true" else $rows[] end'
is_compliant=true
Automation
Automation
#!/usr/bin/env bash
#
# Remediation: Ensure no container runs with securityContext.privileged: true
# Scope: Any machine with kubectl access and cluster-admin privileges
#
# Behavior:
# - Scans all non-system namespaces for Pods with privileged containers/initContainers
# - For each Pod, identifies its owning controller (Deployment/StatefulSet/DaemonSet/Job/CronJob/ReplicaSet/ReplicationController)
# - Patches the controller template to remove privileged=true and optionally keep/add capabilities
# - Deletes the non-compliant Pod so the controller recreates it from the fixed template
# - Skips Pods without a controller (requires manual review)
# - Re-runs the audit command at the end
#
# Idempotency:
# - Patches only containers that currently have privileged=true
# - Safe to re-run; no changes are made when nothing is privileged
set -euo pipefail
# --- Configuration ---
# Namespace filter for system namespaces we should NOT touch
SYSTEM_NAMESPACES=("kube-system" "kube-public" "kube-node-lease")
# Temporary working directory
WORKDIR="$(mktemp -d /tmp/k8s-privileged-fix.XXXXXX)"
trap 'rm -rf "$WORKDIR"' EXIT
# --- Helper functions ---
is_system_namespace() {
local ns="$1"
for s in "${SYSTEM_NAMESPACES[@]}"; do
if [[ "$ns" == "$s" ]]; then
return 0
fi
done
return 1
}
jq_filter_remove_privileged='
# Given a containers array, remove privileged=true; preserve any capabilities
map(
if (.securityContext // null) != null and (.securityContext.privileged // false) == true then
.securityContext |= (
.privileged = false
| with_entries(select(.key != "privileged"))
)
else
.
end
)
'
# --- Discover privileged pods ---
echo "[*] Discovering non-compliant pods (privileged containers) ..."
audit_output_file="${WORKDIR}/audit.json"
kubectl get pods --all-namespaces -o json > "${audit_output_file}"
non_compliant_pods_json="$(
jq --argjson sysns "$(printf '%s\n' "${SYSTEM_NAMESPACES[@]}" | jq -R . | jq -s .)" '
.items[]
| select(.metadata.namespace as $n | $sysns | index($n) | not)
| select(
((.spec.containers // []) + (.spec.initContainers // []))
| map(.securityContext.privileged // false)
| any(. == true)
)
' "${audit_output_file}"
)"
if [[ -z "$non_compliant_pods_json" ]]; then
echo "[*] No privileged containers found outside system namespaces. Cluster appears compliant."
echo
echo "[*] Running verification 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 | length) == 0 then "is_compliant=true" else $rows[] end'
exit 0
fi
echo "[*] Privileged pods detected. Beginning remediation ..."
# Save detailed list for possible manual review
echo "${non_compliant_pods_json}" > "${WORKDIR}/non_compliant_pods.json"
# --- Patch controllers ---
# Extract unique owner references (controller) for non-compliant pods
owners_file="${WORKDIR}/owners.json"
jq '
[
.
| {
ns: .metadata.namespace,
pod: .metadata.name,
owner: ([ (.metadata.ownerReferences // [])[] | select(.controller) ] | first)
}
| select(.owner != null)
]
' "${WORKDIR}/non_compliant_pods.json" > "${owners_file}"
# Build a unique set of controllers to patch
controllers_file="${WORKDIR}/controllers.json"
jq '
map({kind: .owner.kind, name: .owner.name, ns: .ns})
| unique
' "${owners_file}" > "${controllers_file}"
controllers_count="$(jq 'length' "${controllers_file}")"
if [[ "${controllers_count}" -eq 0 ]]; then
echo "[!] All privileged pods lack a controlling workload (e.g., standalone Pods)."
echo " These must be fixed manually by editing their Pod specs/manifests to remove securityContext.privileged: true."
else
echo "[*] Found ${controllers_count} owning controllers to patch."
fi
# Function to patch a single controller
patch_controller() {
local kind="$1"
local name="$2"
local ns="$3"
echo "[*] Processing controller: ${kind} ${ns}/${name}"
# Determine the path to the Pod template based on kind
local template_jsonpath=''
case "$kind" in
Deployment)
template_jsonpath='.spec.template'
;;
StatefulSet)
template_jsonpath='.spec.template'
;;
DaemonSet)
template_jsonpath='.spec.template'
;;
ReplicaSet)
template_jsonpath='.spec.template'
;;
ReplicationController)
template_jsonpath='.spec.template'
;;
Job)
template_jsonpath='.spec.template'
;;
CronJob)
# v1 CronJob
template_jsonpath='.spec.jobTemplate.spec.template'
;;
*)
echo "[!] Unsupported controller kind: ${kind}. Skipping."
return
;;
esac
# Fetch the controller as JSON
local ctrl_file="${WORKDIR}/${kind}_${ns}_${name}.json"
if ! kubectl get "${kind}" "${name}" -n "${ns}" -o json > "${ctrl_file}" 2>/dev/null; then
echo "[!] Failed to fetch ${kind} ${ns}/${name}. It may have been deleted. Skipping."
return
fi
# Patch containers and initContainers in the template
local patched_file="${WORKDIR}/${kind}_${ns}_${name}.patched.json"
jq --argjson emptyobj '{}' "
. as \$root
| ${template_jsonpath} as \$tmpl
| \$root
| ${template_jsonpath}.spec.containers |= (${jq_filter_remove_privileged})
| if (${template_jsonpath}.spec.initContainers // null) != null then
${template_jsonpath}.spec.initContainers |= (${jq_filter_remove_privileged})
else
.
end
" "${ctrl_file}" > "${patched_file}"
# Check if there is any diff; if no change, skip apply
if diff -q "${ctrl_file}" "${patched_file}" >/dev/null 2>&1; then
echo " No privileged containers found in template for ${kind} ${ns}/${name}; nothing to patch."
return
fi
echo " Applying patched template to ${kind} ${ns}/${name} ..."
kubectl apply -f "${patched_file}"
}
# Patch all controllers
for i in $(seq 0 $((controllers_count - 1))); do
kind="$(jq -r ".[$i].kind" "${controllers_file}")"
name="$(jq -r ".[$i].name" "${controllers_file}")"
ns="$(jq -r ".[$i].ns" "${controllers_file}")"
patch_controller "${kind}" "${name}" "${ns}"
done
# --- Handle standalone pods (no controller) ---
standalone_pods_file="${WORKDIR}/standalone_pods.json"
jq '
[
.
| {
ns: .metadata.namespace,
name: .metadata.name
}
]
' "${WORKDIR}/non_compliant_pods.json" > "${standalone_pods_file}.tmp"
# Filter out pods that had a controller
jq '
. as $pods
| $pods
| map(
. as $p
| if (input // []) as $owners
| any(.ns == $p.ns and .pod == $p.name; .)
then empty
else $p
end
)
' "${standalone_pods_file}.tmp" "${owners_file}" > "${standalone_pods_file}"
standalone_count="$(jq 'length' "${standalone_pods_file}")"
if [[ "${standalone_count}" -gt 0 ]]; then
echo "[!] Detected ${standalone_count} privileged Pod(s) without a controlling workload."
echo " These must be remediated manually by updating their manifests or recreating them without privileged=true."
echo " List of such pods:"
jq -r '.[] | "- ns=\(.ns) name=\(.name)"' "${standalone_pods_file}"
fi
# --- Delete existing non-compliant pods so controllers recreate them ---
echo "[*] Deleting non-compliant pods so patched controllers can recreate them ..."
pods_to_delete_file="${WORKDIR}/pods_to_delete.txt"
jq -r '[.[] | "\(.metadata.namespace) \(.metadata.name)"] | .[]' "${WORKDIR}/non_compliant_pods.json" > "${pods_to_delete_file}"
while read -r ns name; do
echo " Deleting pod ${ns}/${name} ..."
kubectl delete pod "${name}" -n "${ns}" --ignore-not-found
done < "${pods_to_delete_file}"
echo "[*] Waiting for replacements to become Ready ..."
sleep 10
# --- Verification (re-run audit) ---
echo
echo "[*] 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 | length) == 0 then "is_compliant=true" else $rows[] end'
echo
echo "[*] Automation completed. Review any remaining non-compliant lines above and fix manually (e.g., standalone Pods)."

