#!/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)."