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
-
On any machine with kubectl access, list the non‑compliant pods and pick the one(s) to fix:
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)| "ns=\($m.namespace) name=\($m.name) container=\(.name) image=\(.image)"][]' -
For a pod created from a higher‑level controller (Deployment, DaemonSet, StatefulSet, Job, CronJob), edit the controller manifest so the setting persists. For example, for a Deployment (run on any kubectl machine):
kubectl -n NAMESPACE edit deployment DEPLOYMENT_NAMEUnder
spec.template.spec, add or update a pod‑level security context:spec:template:spec:securityContext:runAsNonRoot: trueIf you need per‑container overrides instead (e.g., only some containers must be non‑root), set on each container:
spec:template:spec:containers:- name: CONTAINER_NAMEsecurityContext:runAsNonRoot: trueSave and exit; Kubernetes will roll out new pods.
-
For standalone Pods not managed by a controller, first export the manifest (any kubectl machine):
kubectl -n NAMESPACE get pod POD_NAME -o yaml > /tmp/pod-POD_NAME.yamlIn
/tmp/pod-POD_NAME.yaml, delete runtime‑only fields likestatus:,metadata.resourceVersion,metadata.uid,metadata.creationTimestamp, andmetadata.managedFields. Underspec, add either:spec:securityContext:runAsNonRoot: trueor, per container:
spec:containers:- name: CONTAINER_NAMEsecurityContext:runAsNonRoot: true -
Apply the corrected standalone Pod manifest and recreate the pod (any kubectl machine):
kubectl -n NAMESPACE delete pod POD_NAMEkubectl apply -f /tmp/pod-POD_NAME.yaml -
For OKE workloads managed via IaC or GitOps, mirror the same
securityContext.runAsNonRoot: truechanges in the source manifests or Helm charts (e.g., in your Git repository) so future deployments do not revert the setting. Commit and redeploy via your normal pipeline. -
Verify all pods now comply (any kubectl machine):
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'Confirm that either the output is exactly
is_compliant=trueor every listed container line ends withis_compliant=true.
Using kubectl
On any machine with kubectl access:
- Identify non-compliant pods
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.securityContext.runAsNonRoot // false) as $podNonRoot
| ((.spec.containers // []) + (.spec.initContainers // []))[]
| ($podNonRoot or (.securityContext.runAsNonRoot // false)) as $ok
| select($ok | not)
| "ns=\($m.namespace) name=\($m.name)"
] | unique[]'
- For a pod controlled by a Deployment/ReplicaSet/DaemonSet/Job/etc., patch the controller (example: Deployment)
kubectl -n <namespace> get deployment <name> -o yaml > /tmp/deployment-<name>.yaml
Edit /tmp/deployment-<name>.yaml and under spec.template.spec add (or adjust) the pod-level security context:
apiVersion: apps/v1
kind: Deployment
metadata:
name: <name>
namespace: <namespace>
spec:
template:
spec:
securityContext:
runAsNonRoot: true
containers:
- name: <container-name>
image: <image>
# container-specific securityContext is optional if pod-level is set
# securityContext:
# runAsNonRoot: true
Apply the updated manifest:
kubectl apply -f /tmp/deployment-<name>.yaml
If you prefer to set it per container instead of pod-level:
spec:
template:
spec:
containers:
- name: <container-name>
image: <image>
securityContext:
runAsNonRoot: true
- For standalone Pods (no ownerReferences.controller), edit and re-apply
kubectl -n <namespace> get pod <pod-name> -o yaml > /tmp/pod-<pod-name>.yaml
Edit /tmp/pod-<pod-name>.yaml so the pod spec has one of:
Pod-level:
spec:
securityContext:
runAsNonRoot: true
containers:
- name: <container-name>
image: <image>
Or per container:
spec:
containers:
- name: <container-name>
image: <image>
securityContext:
runAsNonRoot: true
Then recreate the pod:
kubectl delete -n <namespace> pod <pod-name>
kubectl apply -f /tmp/pod-<pod-name>.yaml
- Verification (same audit logic as check)
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.securityContext.runAsNonRoot // false) as $podNonRoot
| ((.spec.containers // []) + (.spec.initContainers // []))[]
| ($podNonRoot or (.securityContext.runAsNonRoot // false)) as $ok
| "kind=Pod ns=\($m.namespace) name=\($m.name) container=\(.name) runAsNonRoot=\($ok)"
+ " 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'
Automation
#!/usr/bin/env bash
#
# Enforce securityContext.runAsNonRoot: true on all non-excluded Pods
# by updating their owning workload manifests via kubectl.
#
# Runs from: any machine with kubectl access to the OKE cluster.
# Requirements: bash, kubectl, jq, yq (https://github.com/mikefarah/yq)
#
# Notes:
# - This is idempotent: re-running only re-applies the same settings.
# - It patches the OWNER workload (Deployment, StatefulSet, DaemonSet,
# Job, CronJob, ReplicaSet) rather than the live Pod.
# - Pods in kube-system, kube-public, kube-node-lease are skipped,
# matching the audit command.
# - You MUST review and test in non‑prod first; some images may REQUIRE root.
#
set -euo pipefail
# Fail fast if required tools not present
for bin in kubectl jq yq; do
if ! command -v "$bin" >/dev/null 2>&1; then
echo "ERROR: $bin not found in PATH. Please install it before running this script." >&2
exit 1
fi
done
# Get non-compliant pod rows from the audit query
echo "Discovering non-compliant containers (runAsNonRoot=false or unset)..."
mapfile -t NON_COMPLIANT_ROWS < <(
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"
] as $rows
| if ($rows | length) == 0 then empty else $rows[] end
'
)
if [ "${#NON_COMPLIANT_ROWS[@]}" -eq 0 ]; then
echo "No non-compliant containers found. Nothing to do."
exit 0
fi
echo "Found ${#NON_COMPLIANT_ROWS[@]} non-compliant container entries."
# Build a unique list of owners (kind, namespace, name)
declare -A OWNERS
for row in "${NON_COMPLIANT_ROWS[@]}"; do
# owner=Kind/ns/name/uid
owner_field=$(grep -o 'owner=[^ ]*' <<<"$row" || true)
if [ -z "$owner_field" ]; then
# Standalone Pod (no controller) – patch the Pod template directly
# Key format: Pod|<namespace>|<name>
ns=$(grep -o 'ns=[^ ]*' <<<"$row" | cut -d= -f2)
podname=$(grep -o 'name=[^ ]*' <<<"$row" | cut -d= -f2)
key="Pod|${ns}|${podname}"
else
owner_val=${owner_field#owner=}
IFS=/ read -r owner_kind owner_ns owner_name _uid <<<"$owner_val"
key="${owner_kind}|${owner_ns}|${owner_name}"
fi
OWNERS["$key"]=1
done
echo "Will patch ${#OWNERS[@]} owning resources (Pods/Controllers)."
# Function: patch a workload YAML to set runAsNonRoot: true
patch_manifest_run_as_non_root() {
local yaml_file=$1
# Pod spec path may vary by kind
# - Pod, Deployment, DaemonSet, StatefulSet, ReplicaSet, Job: .spec.template.spec or .spec
# - CronJob: .spec.jobTemplate.spec.template.spec
# We’ll set both pod-level and per-container for safety.
yq -i '
# Detect base path for pod spec
( .kind == "Pod" )
as $isPod |
( .kind == "CronJob" )
as $isCron |
# Helper for setting runAsNonRoot on a pod spec node
def set_runnr(path):
. as $root
| (path | $root) as $pod
| if $pod == null then $root
else
$root
| (path + ".securityContext.runAsNonRoot") |= (true)
| (path + ".containers[]?.securityContext.runAsNonRoot") |= (true)
| (path + ".initContainers[]?.securityContext.runAsNonRoot") |= (true)
end;
# For Pod
if $isPod then
set_runnr(".spec")
# For CronJob: jobTemplate.spec.template.spec
elif $isCron then
set_runnr(".spec.jobTemplate.spec.template.spec")
# For other workload types: spec.template.spec
else
set_runnr(".spec.template.spec")
end
' "$yaml_file"
}
# Process each unique owner
for key in "${!OWNERS[@]}"; do
IFS='|' read -r kind ns name <<<"$key"
echo "Processing owner: kind=${kind} ns=${ns} name=${name}"
tmp_yaml=$(mktemp)
trap 'rm -f "$tmp_yaml"' EXIT
if [ "$kind" = "Pod" ]; then
# Standalone Pod: get and patch the Pod manifest
if ! kubectl get pod "$name" -n "$ns" -o yaml >"$tmp_yaml"; then
echo "WARNING: Pod ${ns}/${name} not found, skipping."
continue
fi
else
if ! kubectl get "$kind" "$name" -n "$ns" -o yaml >"$tmp_yaml"; then
echo "WARNING: ${kind} ${ns}/${name} not found, skipping."
continue
fi
fi
# Patch the manifest locally
patch_manifest_run_as_non_root "$tmp_yaml"
# Apply back to the cluster
echo " Applying patched manifest for ${kind} ${ns}/${name}..."
kubectl apply -f "$tmp_yaml"
rm -f "$tmp_yaml"
trap - EXIT
done
echo "Patching complete. Waiting for workloads to reconcile..."
sleep 10
echo "Re-running compliance audit to verify..."
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
' | tee /tmp/runAsNonRoot_verification.txt
echo "Verification output saved to /tmp/runAsNonRoot_verification.txt"
echo "Review any lines with is_compliant=false; those require manual analysis (image may require root or owner kind unsupported by this script)."