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
Remediation
Manual Steps
-
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"][]' -
For a standalone Pod (no owner/controller), edit the Pod manifest to set
runAsNonRoot: trueat 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.yamlEdit
/tmp/pod-fixed.yaml:- Under
spec:, add or update:securityContext:runAsNonRoot: true - Remove
metadata.resourceVersion,metadata.uid,metadata.creationTimestamp, andstatus:block. Apply the fixed manifest:
kubectl -n <NAMESPACE> delete pod <POD_NAME>kubectl apply -f /tmp/pod-fixed.yaml - Under
-
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 }}]' -
If initContainers are present and not covered by pod-level
securityContext, ensure they also haverunAsNonRoot: 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 }}]' -
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: trueCommit and apply via your normal pipeline so changes persist.
-
Verification (run on any machine with kubectl access): re-run the audit and confirm only
is_compliant=trueremains, or nois_compliant=falselines: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:
- Edit the affected Pod’s owning workload (Deployment, StatefulSet, etc.) manifest and set
runAsNonRoot: trueat 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"