Skip to main content

Containers Should Run As Non-Root

More Info:

Verifies runAsNonRoot is set at pod or container level. Running as root inside a container widens the impact of a container escape.

Risk Level

High

Address

Security

Compliance Standards

  • Cloudanix Best Practice

Triage and Remediation

Remediation

Manual Steps
  1. Identify non-compliant 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.securityContext.runAsNonRoot // false) as $podNonRoot
    | ((.spec.containers // []) + (.spec.initContainers // []))[]
    | ($podNonRoot or (.securityContext.runAsNonRoot // 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) runAsNonRoot=\($ok)"
    + " is_compliant=false"
    ][]'
  2. For a standalone Pod (no owner/controller), edit the Pod manifest to set runAsNonRoot: true at pod level (preferred) or per container (run on any machine with kubectl access):

    kubectl -n <NAMESPACE> get pod <POD_NAME> -o yaml > /tmp/pod-fixed.yaml

    Edit /tmp/pod-fixed.yaml:

    • Under spec:, add or update:
      securityContext:
      runAsNonRoot: true
    • Remove metadata.resourceVersion, metadata.uid, metadata.creationTimestamp, and status: block. Apply the fixed manifest:
    kubectl -n <NAMESPACE> delete pod <POD_NAME>
    kubectl apply -f /tmp/pod-fixed.yaml
  3. For pods managed by a controller (Deployment/StatefulSet/DaemonSet/Job/CronJob), patch the controller spec so all current and future pods inherit runAsNonRoot: true (run on any machine with kubectl access). Example for a Deployment:

    kubectl -n <NAMESPACE> patch deployment <DEPLOYMENT_NAME> \
    --type merge \
    -p '{
    "spec": {
    "template": {
    "spec": {
    "securityContext": {
    "runAsNonRoot": true
    }
    }
    }
    }
    }'

    If you must set it per container (e.g., pod-level context not desired), patch each container by name:

    kubectl -n <NAMESPACE> patch deployment <DEPLOYMENT_NAME> \
    --type json \
    -p '[{
    "op": "add",
    "path": "/spec/template/spec/containers/0/securityContext",
    "value": { "runAsNonRoot": true }
    }]'
  4. If initContainers are present and not covered by pod-level securityContext, ensure they also have runAsNonRoot: true (run on any machine with kubectl access). Example for the first initContainer:

    kubectl -n <NAMESPACE> patch deployment <DEPLOYMENT_NAME> \
    --type json \
    -p '[{
    "op": "add",
    "path": "/spec/template/spec/initContainers/0/securityContext",
    "value": { "runAsNonRoot": true }
    }]'
  5. For GitOps or manifest-driven environments, update the source manifests instead of live objects (run on any machine with repo access): in each PodTemplate (Deployment/StatefulSet/DaemonSet/Job/CronJob), set:

    spec:
    template:
    spec:
    securityContext:
    runAsNonRoot: true

    Commit and apply via your normal pipeline so changes persist.

  6. Verification (run on any machine with kubectl access): re-run the audit and confirm only is_compliant=true remains, or no is_compliant=false lines:

    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.securityContext.runAsNonRoot // false) as $podNonRoot
    | ((.spec.containers // []) + (.spec.initContainers // []))[]
    | ($podNonRoot or (.securityContext.runAsNonRoot // 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) runAsNonRoot=\($ok)"
    + " is_compliant=\(if $ok then "true" else "false" end)"
    ] as $rows
    | if ($rows | length) == 0 then "is_compliant=true" else $rows[] end' | grep "is_compliant=false" || echo "All non-excluded pods are compliant"
Using kubectl

On any machine with kubectl access:

  1. Edit the affected Pod’s owning workload (Deployment, StatefulSet, etc.) manifest and set runAsNonRoot: true at pod level.

Example for a Deployment:

kubectl -n <NAMESPACE> get deploy <DEPLOYMENT_NAME> -o yaml > /tmp/deploy-run-as-non-root.yaml

Edit /tmp/deploy-run-as-non-root.yaml and ensure the pod template has:

spec:
template:
spec:
securityContext:
runAsNonRoot: true
containers:
- name: <CONTAINER_NAME>
image: <IMAGE>
# per-container override if desired:
# securityContext:
# runAsNonRoot: true

Apply the updated manifest:

kubectl apply -f /tmp/deploy-run-as-non-root.yaml

For a naked Pod (no controller), edit and apply similarly:

kubectl -n <NAMESPACE> get pod <POD_NAME> -o yaml > /tmp/pod-run-as-non-root.yaml
# edit /tmp/pod-run-as-non-root.yaml as above
kubectl delete pod -n <NAMESPACE> <POD_NAME>
kubectl apply -f /tmp/pod-run-as-non-root.yaml

Note: Updating the pod template for controllers will recreate pods; direct Pod changes require deletion and re-creation.

Verification (same machine with kubectl):

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.securityContext.runAsNonRoot // false) as $podNonRoot
| ((.spec.containers // []) + (.spec.initContainers // []))[]
| ($podNonRoot or (.securityContext.runAsNonRoot // 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) runAsNonRoot=\($ok)"
+ " is_compliant=\(if $ok then "true" else "false" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'
Automation
#!/usr/bin/env bash
#
# Enforce securityContext.runAsNonRoot: true on Pods and template owners
# Scope: any machine with kubectl access to the cluster
# Requirements: kubectl, jq, yq (https://mikefarah.gitbook.io/yq)

set -euo pipefail

if ! command -v kubectl >/dev/null 2>&1; then
echo "ERROR: kubectl not found in PATH" >&2
exit 1
fi

if ! command -v jq >/dev/null 2>&1; then
echo "ERROR: jq not found in PATH" >&2
exit 1
fi

if ! command -v yq >/dev/null 2>&1; then
echo "ERROR: yq not found in PATH (mikefarah yq v4+)" >&2
exit 1
fi

# Namespaces to exclude (as per audit command)
EXCLUDED_NS_REGEX='^(kube-system|kube-public|kube-node-lease)$'

timestamp() {
date -u +"%Y%m%dT%H%M%SZ"
}

BACKUP_DIR="runAsNonRoot-backups-$(timestamp)"
mkdir -p "${BACKUP_DIR}"

echo "Backing up original manifests to: ${BACKUP_DIR}"

# Helper: patch any workload with a Pod template, setting runAsNonRoot at pod level
patch_workload() {
local ns kind name
ns="$1"
kind="$2"
name="$3"

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

local backup_file="${BACKUP_DIR}/${ns}-${kind}-${name}.yaml"
kubectl get "${kind}" "${name}" -n "${ns}" -o yaml > "${backup_file}"

# Use yq to ensure .spec.template.spec.securityContext.runAsNonRoot: true
# Idempotent: setting the same value repeatedly is safe
yq -y '
.spec.template.spec.securityContext //= {} |
.spec.template.spec.securityContext.runAsNonRoot = true
' "${backup_file}" > "${backup_file}.patched"

# Apply patched manifest
kubectl apply -f "${backup_file}.patched"
}

# 1. Fix template-based owners (Deployments, StatefulSets, DaemonSets, Jobs, CronJobs, ReplicaSets, ReplicationControllers)
echo "Discovering namespaced workloads with Pod templates (excluding system namespaces)..."

kubectl api-resources --namespaced=true --verbs=list --output=name | while read -r res; do
# Filter for common workload types that own Pod templates
case "${res}" in
deployments.apps|statefulsets.apps|daemonsets.apps|jobs.batch|cronjobs.batch|replicasets.apps|replicationcontrollers)
:
;;
*)
continue
;;
esac

kubectl get "${res}" --all-namespaces -o json | \
jq -r --arg re "${EXCLUDED_NS_REGEX}" '
.items[]
| select(.metadata.namespace | test($re) | not)
| [.metadata.namespace, .kind, .metadata.name]
| @tsv
' | while IFS=$'\t' read -r ns kind name; do
patch_workload "${ns}" "${kind}" "${name}"
done
done

# 2. For standalone Pods without an owning controller, patch the Pod spec directly
echo "Discovering standalone Pods (no controller ownerReferences) to patch..."

kubectl get pods --all-namespaces -o json | \
jq -r --arg re "${EXCLUDED_NS_REGEX}" '
.items[]
| select(.metadata.namespace | test($re) | not)
| select(((.metadata.ownerReferences // []) | map(select(.controller == true)) | length) == 0)
| [.metadata.namespace, .metadata.name]
| @tsv
' | while IFS=$'\t' read -r ns name; do
echo "Patching standalone Pod ${ns}/${name}"
kubectl patch pod "${name}" -n "${ns}" --type='merge' -p '
{
"spec": {
"securityContext": {
"runAsNonRoot": true
}
}
}' || echo "WARNING: Failed to patch Pod ${ns}/${name}; it may be terminating or immutable."
done

echo "Initial remediation complete. Verifying using benchmark audit..."

# 3. Verification: run the provided audit command and show non-compliant lines (if any)
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.securityContext.runAsNonRoot // false) as $podNonRoot
| ((.spec.containers // []) + (.spec.initContainers // []))[]
| ($podNonRoot or (.securityContext.runAsNonRoot // 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) runAsNonRoot=\($ok)"
+ " is_compliant=\(if $ok then "true" else "false" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end' | \
awk '/is_compliant=false/ || /is_compliant=true/'

echo
echo "If any lines above show is_compliant=false, investigate those workloads and update their manifests in source control to include:"
echo " spec:"
echo " template:"
echo " spec:"
echo " securityContext:"
echo " runAsNonRoot: true"