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. List all non-system pods and identify non-compliant ones (run on any machine with kubectl access):

    kubectl get pods --all-namespaces \
    --field-selector metadata.namespace!=kube-system,metadata.namespace!=kube-public,metadata.namespace!=kube-node-lease \
    -o json | jq -r '
    .items[]
    | .metadata as $m
    | ((.spec.containers // []) + (.spec.initContainers // []))[]
    | select(.securityContext.allowPrivilegeEscalation != false)
    | "ns=\($m.namespace) pod=\($m.name) container=\(.name) ape=\(if .securityContext.allowPrivilegeEscalation==null then "unset" else .securityContext.allowPrivilegeEscalation end)"'
  2. For a pod you need to fix, find its owning controller (Deployment, StatefulSet, DaemonSet, Job, etc.) (run on any machine with kubectl access):

    NAMESPACE="<namespace-of-pod>"
    POD_NAME="<pod-name>"

    kubectl get pod "${POD_NAME}" -n "${NAMESPACE}" -o jsonpath='{.metadata.ownerReferences}'

    Note the kind and name. You will edit that controller, not the pod itself.

  3. Edit the owning controller manifest to set allowPrivilegeEscalation: false on every container (run on any machine with kubectl access):

    KIND="<Deployment|StatefulSet|DaemonSet|Job|CronJob>"
    NAME="<controller-name>"
    NAMESPACE="<namespace>"

    kubectl get "${KIND}" "${NAME}" -n "${NAMESPACE}" -o yaml > /tmp/${KIND}-${NAME}.yaml

    Open /tmp/${KIND}-${NAME}.yaml in an editor and, under each container (and initContainer) in spec.template.spec.containers and spec.template.spec.initContainers, ensure:

    securityContext:
    allowPrivilegeEscalation: false

    If securityContext exists, add only the allowPrivilegeEscalation: false line; do not remove other fields.

  4. Apply the updated controller manifest so new pods are compliant (run on any machine with kubectl access):

    kubectl apply -f /tmp/${KIND}-${NAME}.yaml
  5. (Optional but recommended) Rotate existing non-compliant pods so they are recreated from the updated controller (run on any machine with kubectl access):

    # For a Deployment
    kubectl rollout restart deployment "${NAME}" -n "${NAMESPACE}"

    # For a DaemonSet
    # kubectl rollout restart daemonset "${NAME}" -n "${NAMESPACE}"

    # For a StatefulSet
    # kubectl rollout restart statefulset "${NAME}" -n "${NAMESPACE}"
  6. 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.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=\(if .securityContext.allowPrivilegeEscalation == null then "unset" else .securityContext.allowPrivilegeEscalation end)"
    + " 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'

    Confirm the output is is_compliant=true or that all listed containers show is_compliant=true.

Using kubectl

On any machine with kubectl access:

  1. Identify non-compliant Pods (for context)
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)
| "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=\(if .securityContext.allowPrivilegeEscalation == null then "unset" else .securityContext.allowPrivilegeEscalation end)"
+ " is_compliant=false"
][]'
  1. Edit the owning workload manifest to set allowPrivilegeEscalation: false on every container.

Example Deployment spec snippet (applies to any workload kind: Deployment, StatefulSet, DaemonSet, Job, CronJob, etc.):

apiVersion: apps/v1
kind: Deployment
metadata:
name: example
namespace: default
spec:
template:
spec:
containers:
- name: app
image: nginx:1.27
securityContext:
allowPrivilegeEscalation: false
initContainers:
- name: init-app
image: busybox:1.36
securityContext:
allowPrivilegeEscalation: false

Apply the updated manifest:

kubectl apply -f deployment-example.yaml

Repeat for every workload so that each spec.template.spec.containers[] and spec.template.spec.initContainers[] entry has:

securityContext:
allowPrivilegeEscalation: false
  1. For naked Pods created directly (no controller), recreate them from a manifest that sets the field:
apiVersion: v1
kind: Pod
metadata:
name: example-pod
namespace: default
spec:
containers:
- name: app
image: nginx:1.27
securityContext:
allowPrivilegeEscalation: false
initContainers:
- name: init-app
image: busybox:1.36
securityContext:
allowPrivilegeEscalation: false

Apply:

kubectl delete pod -n default example-pod --wait=true
kubectl apply -f pod-example.yaml
  1. 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.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=\(if .securityContext.allowPrivilegeEscalation == null then "unset" else .securityContext.allowPrivilegeEscalation end)"
+ " is_compliant=\(if $ok then "true" else "false" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'

You are compliant when the output is is_compliant=true or all listed containers show is_compliant=true.

Automation
#!/usr/bin/env bash
set -euo pipefail

# Applies securityContext.allowPrivilegeEscalation: false
# to every container and initContainer in non-system Pods
# by patching their owning workload objects via kubectl.
#
# Run on: any machine with kubectl access to the OKE cluster.
# Requirements: kubectl, jq, yq (https://github.com/mikefarah/yq)

if ! command -v kubectl >/dev/null 2>&1; then
echo "kubectl not found in PATH" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "jq not found in PATH" >&2
exit 1
fi
if ! command -v yq >/dev/null 2>&1; then
echo "yq not found in PATH (https://github.com/mikefarah/yq)" >&2
exit 1
fi

WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT

# 1. Discover non-compliant Pods and their controllers
echo "Discovering non-compliant Pods and their controllers..."
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.allowPrivilegeEscalation != false)
| select(.image != "k8s.gcr.io/pause:3.2") # ignore pause-type containers if any
| {
ns: $m.namespace,
pod: $m.name,
ownerKind: ($own.kind // "Pod"),
ownerName: ($own.name // $m.name)
}
] | unique
| .[]
| @tsv "\(.ns)\t\(.pod)\t\(.ownerKind)\t\(.ownerName)"
' > "$WORKDIR/non_compliant_pods.tsv" || true

if [ ! -s "$WORKDIR/non_compliant_pods.tsv" ]; then
echo "No non-compliant Pods found."
else
echo "Found non-compliant Pods:"
cat "$WORKDIR/non_compliant_pods.tsv"
fi

# 2. Build a unique list of owner workloads to patch
echo
echo "Building workload list to patch..."
awk -F'\t' '{print $1"\t"$3"\t"$4}' "$WORKDIR/non_compliant_pods.tsv" 2>/dev/null | sort -u > "$WORKDIR/owners.tsv" || true

if [ ! -s "$WORKDIR/owners.tsv" ]; then
echo "No controller workloads to patch (likely only bare Pods)."
fi

# Helper: patch a single workload manifest (stdin) to set allowPrivilegeEscalation: false
# for all containers and initContainers. Outputs patched manifest to stdout.
patch_manifest() {
yq '
# Ensure securityContext exists for each container
(.spec.template.spec.containers // []) |=
map(
.securityContext.allowPrivilegeEscalation = false
)
|
(.spec.template.spec.initContainers // []) |=
map(
.securityContext.allowPrivilegeEscalation = false
)
'
}

# 3. Patch owners (Deployments, DaemonSets, StatefulSets, Jobs, CronJobs, etc.)
echo
echo "Patching controller workloads to disallow privilege escalation..."
while IFS=$'\t' read -r NS KIND NAME; do
[ -z "$NS" ] && continue
echo "Processing $KIND $NS/$NAME..."

# Detect API group (apps/v1 or batch/v1 / batch/v1beta1 etc.) via kubectl get
# and then re-fetch with -o yaml.
if ! kubectl get "$KIND" -n "$NS" "$NAME" >/dev/null 2>&1; then
echo " Skipping: $KIND $NS/$NAME not found anymore."
continue
fi

ORIG_YAML="$WORKDIR/${NS}_${KIND}_${NAME}_orig.yaml"
PATCHED_YAML="$WORKDIR/${NS}_${KIND}_${NAME}_patched.yaml"

kubectl get "$KIND" -n "$NS" "$NAME" -o yaml > "$ORIG_YAML"

# Some kinds (like CronJob) have spec.jobTemplate.spec.template; handle generically:
# Try to patch spec.template first; if missing, try spec.jobTemplate.spec.template.
if yq '.spec.template' "$ORIG_YAML" >/dev/null 2>&1; then
patch_manifest < "$ORIG_YAML" > "$PATCHED_YAML"
else
# Fallback for CronJob-style objects
yq '
(.spec.jobTemplate.spec.template.spec.containers // []) |=
map(.securityContext.allowPrivilegeEscalation = false)
|
(.spec.jobTemplate.spec.template.spec.initContainers // []) |=
map(.securityContext.allowPrivilegeEscalation = false)
' < "$ORIG_YAML" > "$PATCHED_YAML"
fi

# Apply only if changed (idempotent)
if diff -q "$ORIG_YAML" "$PATCHED_YAML" >/dev/null 2>&1; then
echo " No changes needed (already compliant)."
continue
fi

kubectl apply -n "$NS" -f "$PATCHED_YAML"
echo " Patched and applied."
done < "$WORKDIR/owners.tsv"

# 4. Handle standalone Pods (no controller) by patching Pod spec directly
echo
echo "Patching standalone Pods (no controller)..."
awk -F'\t' '$3=="Pod" {print $1"\t"$2}' "$WORKDIR/non_compliant_pods.tsv" 2>/dev/null | sort -u > "$WORKDIR/standalone_pods.tsv" || true

if [ -s "$WORKDIR/standalone_pods.tsv" ]; then
while IFS=$'\t' read -r NS POD; do
[ -z "$NS" ] && continue
echo "Processing standalone Pod $NS/$POD..."

if ! kubectl get pod -n "$NS" "$POD" >/dev/null 2>&1; then
echo " Skipping: Pod $NS/$POD not found anymore."
continue
fi

ORIG_POD_YAML="$WORKDIR/pod_${NS}_${POD}_orig.yaml"
PATCHED_POD_YAML="$WORKDIR/pod_${NS}_${POD}_patched.yaml"

kubectl get pod -n "$NS" "$POD" -o yaml > "$ORIG_POD_YAML"

yq '
(.spec.containers // []) |=
map(.securityContext.allowPrivilegeEscalation = false)
|
(.spec.initContainers // []) |=
map(.securityContext.allowPrivilegeEscalation = false)
' < "$ORIG_POD_YAML" > "$PATCHED_POD_YAML"

if diff -q "$ORIG_POD_YAML" "$PATCHED_POD_YAML" >/dev/null 2>&1; then
echo " No changes needed (already compliant)."
continue
fi

kubectl replace -n "$NS" -f "$PATCHED_POD_YAML"
echo " Patched Pod."
done < "$WORKDIR/standalone_pods.tsv"
else
echo "No standalone Pods to patch."
fi

# 5. Verification using the authoritative audit command
echo
echo "Re-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.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=\(if .securityContext.allowPrivilegeEscalation == null then "unset" else .securityContext.allowPrivilegeEscalation end)"
+ " 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[] | select(. | contains("is_compliant=false")))
end
'