Skip to main content

Containers Should Disallow Privilege Escalation

More Info:

Verifies allowPrivilegeEscalation is false on every container. It defaults to true, letting a process gain more privileges than its parent.

Risk Level

High

Address

Security

Compliance Standards

  • Cloudanix Best Practice

Triage and Remediation

Remediation

Manual Steps
  1. Identify noncompliant Pods (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.allowPrivilegeEscalation == false) as $ok
    | select($ok | not)
    | "ns=\($m.namespace) pod=\($m.name) container=\(.name) image=\(.image)"
    ][]'
  2. For a stand-alone Pod (no higher-level controller), edit its spec to set allowPrivilegeEscalation: false (any machine with kubectl access):

    kubectl -n <NAMESPACE> edit pod <POD_NAME>

    Under each container (and initContainers if present), add or change:

    securityContext:
    allowPrivilegeEscalation: false

    Save and exit to apply the change (the Pod may be recreated depending on your setup).

  3. For a Pod managed by a controller (Deployment, DaemonSet, StatefulSet, Job, CronJob), find the controller (any machine with kubectl access):

    kubectl -n <NAMESPACE> get pod <POD_NAME> -o jsonpath='{.metadata.ownerReferences[0].kind}{" "}{.metadata.ownerReferences[0].name}{"\n"}'
  4. Edit the controller’s Pod template to set allowPrivilegeEscalation: false for every container and initContainer (any machine with kubectl access). Example for a Deployment:

    kubectl -n <NAMESPACE> edit deployment <DEPLOYMENT_NAME>

    In .spec.template.spec.containers[] (and .spec.template.spec.initContainers[] if present), ensure:

    securityContext:
    allowPrivilegeEscalation: false

    Save and exit; this will roll out new Pods with the updated setting.

  5. For controllers of other kinds, use their specific edit commands (any machine with kubectl access) and apply the same securityContext change:

    # DaemonSet
    kubectl -n <NAMESPACE> edit daemonset <DAEMONSET_NAME>

    # StatefulSet
    kubectl -n <NAMESPACE> edit statefulset <STATEFULSET_NAME>

    # Job
    kubectl -n <NAMESPACE> edit job <JOB_NAME>

    # CronJob (edit the job template)
    kubectl -n <NAMESPACE> edit cronjob <CRONJOB_NAME>

    Add securityContext.allowPrivilegeEscalation: false to every container and initContainer spec in the pod template.

  6. Verify all Pods are now compliant (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.allowPrivilegeEscalation == false) as $ok
    | "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)"
    + " allowPrivilegeEscalation=\(.securityContext.allowPrivilegeEscalation // "unset")"
    + " is_compliant=\(if $ok then "true" else "false" end)"
    ] as $rows
    | if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'

    Confirm that either is_compliant=true is printed or all listed containers show allowPrivilegeEscalation=false is_compliant=true.

Using kubectl

On any machine with kubectl access:

  1. Identify non‑compliant Pods (example using the provided 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.containers // []) + (.spec.initContainers // []))[]
| select(.securityContext.allowPrivilegeEscalation != false)
| "ns=\($m.namespace) pod=\($m.name) container=\(.name)"
][]'
  1. For each workload controller (Deployment, StatefulSet, DaemonSet, Job, CronJob, etc.) that creates the reported Pods, patch the Pod template to set allowPrivilegeEscalation: false on all containers.

Example: Deployment

kubectl -n NAMESPACE edit deployment DEPLOYMENT_NAME

In .spec.template.spec.containers[].securityContext (and .initContainers[] if present), ensure:

securityContext:
allowPrivilegeEscalation: false

Save and exit; Kubernetes will roll out updated Pods.

If you prefer a one‑shot patch (single container example):

kubectl -n NAMESPACE patch deployment DEPLOYMENT_NAME \
--type='json' \
-p='[
{
"op": "add",
"path": "/spec/template/spec/containers/0/securityContext",
"value": { "allowPrivilegeEscalation": false }
}
]'

Repeat with the appropriate resource kind/name and container index for each non‑compliant controller (StatefulSet, DaemonSet, Job, CronJob).

  1. Do not edit Pods that are directly managed by controllers; instead always edit the owning controller. For standalone Pods you manage directly:
kubectl -n NAMESPACE get pod POD_NAME -o yaml > /tmp/pod-POD_NAME.yaml

Edit /tmp/pod-POD_NAME.yaml so each container has:

securityContext:
allowPrivilegeEscalation: false

Then recreate:

kubectl -n NAMESPACE delete pod POD_NAME
kubectl -n NAMESPACE apply -f /tmp/pod-POD_NAME.yaml
  1. Verification (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.allowPrivilegeEscalation == false) as $ok
| select($ok | not)
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'
Automation
#!/usr/bin/env bash
#
# Enforce securityContext.allowPrivilegeEscalation=false on all containers
# in non-system namespaces by patching owning controllers, not Pods.
#
# Requirements:
# - Run on any machine with kubectl access and current-context set
# - kubectl, jq, and yq (https://github.com/mikefarah/yq) must be installed
#
# Idempotent: re-running will keep allowPrivilegeEscalation=false and not break.

set -euo pipefail

# Fail fast if required tools are missing
for bin in kubectl jq yq; do
if ! command -v "$bin" >/dev/null 2>&1; then
echo "ERROR: '$bin' is required but not found in PATH" >&2
exit 1
fi
done

echo "=== Discovering non-system namespaces with non-compliant Pods ==="

# Get all Pod records as JSON, excluding core system namespaces
PODS_JSON="$(kubectl get pods --all-namespaces -o json)"

# Identify owning controllers (kind, name, namespace, apiVersion) for non-compliant containers
# We skip kube-system, kube-public, kube-node-lease as per the audit.
CONTROLLERS_JSON="$(
echo "$PODS_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
| select($own != null) # only pods with a controller
| .spec as $spec
| ($spec.containers // [] + $spec.initContainers // []) as $allc
| select( any($allc[]?; (.securityContext.allowPrivilegeEscalation // true) != false) )
| {
apiVersion: $own.apiVersion,
kind: $own.kind,
name: $own.name,
namespace: $m.namespace
}
]
| unique
'
)"

if [[ -z "$CONTROLLERS_JSON" || "$CONTROLLERS_JSON" == "[]" ]]; then
echo "No non-compliant controller-owned Pods found outside core system namespaces."
else
echo "=== Patching controllers to set allowPrivilegeEscalation=false ==="
echo "$CONTROLLERS_JSON" | jq -c '.[]' | while read -r ctrl; do
apiVersion=$(echo "$ctrl" | jq -r '.apiVersion')
kind=$(echo "$ctrl" | jq -r '.kind')
name=$(echo "$ctrl" | jq -r '.name')
namespace=$(echo "$ctrl" | jq -r '.namespace')

echo "Processing ${kind}/${namespace}/${name}"

# Fetch full controller YAML
tmpfile="$(mktemp)"
kubectl get "$kind" "$name" -n "$namespace" -o yaml > "$tmpfile"

# Patch containers and initContainers: ensure securityContext.allowPrivilegeEscalation=false
# while preserving any existing securityContext fields.
yq -i '
.spec.template.spec.containers //= [] |
.spec.template.spec.initContainers //= [] |
(.spec.template.spec.containers[] |= (
.securityContext //= {} |
.securityContext.allowPrivilegeEscalation = false
)) |
(.spec.template.spec.initContainers[] |= (
.securityContext //= {} |
.securityContext.allowPrivilegeEscalation = false
))
' "$tmpfile"

# Apply the patched controller
kubectl apply -n "$namespace" -f "$tmpfile" >/dev/null

rm -f "$tmpfile"
done
fi

echo "=== Patching standalone Pods (no controller owner) ==="

# Handle Pods that are not owned by a controller (directly created Pods).
STANDALONE_PODS_JSON="$(
echo "$PODS_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
| select($own == null)
| .spec as $spec
| ($spec.containers // [] + $spec.initContainers // []) as $allc
| select( any($allc[]?; (.securityContext.allowPrivilegeEscalation // true) != false) )
| { name: $m.name, namespace: $m.namespace }
]
| unique
'
)"

if [[ -z "$STANDALONE_PODS_JSON" || "$STANDALONE_PODS_JSON" == "[]" ]]; then
echo "No non-compliant standalone Pods found outside core system namespaces."
else
echo "$STANDALONE_PODS_JSON" | jq -c '.[]' | while read -r pod; do
pod_name=$(echo "$pod" | jq -r '.name')
pod_ns=$(echo "$pod" | jq -r '.namespace')
echo "Patching Pod/${pod_ns}/${pod_name}"

tmpfile="$(mktemp)"
kubectl get pod "$pod_name" -n "$pod_ns" -o yaml > "$tmpfile"

# Note: editing a running Pod spec directly will recreate it if it is not controlled
# by a higher-level object; for an existing Pod, kubectl replace will delete/recreate.
# This has operational impact: the Pod will be restarted.
yq -i '
.spec.containers //= [] |
.spec.initContainers //= [] |
(.spec.containers[] |= (
.securityContext //= {} |
.securityContext.allowPrivilegeEscalation = false
)) |
(.spec.initContainers[] |= (
.securityContext //= {} |
.securityContext.allowPrivilegeEscalation = false
))
' "$tmpfile"

kubectl replace -n "$pod_ns" -f "$tmpfile" >/dev/null

rm -f "$tmpfile"
done
fi

echo "=== Verification (re-running audit) ==="

# Re-run the provided audit to confirm compliance
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.allowPrivilegeEscalation == false) as $ok
| "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)"
+ " allowPrivilegeEscalation=\(.securityContext.allowPrivilegeEscalation // "unset")"
+ " is_compliant=\(if $ok then "true" else "false" end)"
] as $rows
| if ($rows | map(select(. | contains("is_compliant=false"))) | length) == 0
then "is_compliant=true"
else $rows[]
end
'

echo "=== Done ==="