Containers Should Use A Read-Only Root Filesystem
More Info:​
Verifies readOnlyRootFilesystem is true. A writable root filesystem lets an attacker persist tools or modify binaries inside a running container.
Risk Level​
Medium
Address​
Security
Compliance Standards​
- Cloudanix Best Practice
Triage and Remediation​
- Remediation
Remediation​
Manual Steps
-
Identify noncompliant 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.containers // []) + (.spec.initContainers // []))[]| (.securityContext.readOnlyRootFilesystem == true) as $ok| select($ok | not)| "ns=\($m.namespace) pod=\($m.name) ownerKind=\($own.kind // "Pod") ownerName=\($own.name // $m.name)"] | unique[]' -
For each listed Pod, edit the owning workload manifest (e.g., Deployment) to set a read-only root filesystem (run on any machine with kubectl access; example for a Deployment):
kubectl -n NAMESPACE edit deployment DEPLOYMENT_NAMEIn the opened spec, under each
.spec.template.spec.containers[](and.initContainers[]if present) add or modify:securityContext:readOnlyRootFilesystem: trueSave and exit to apply the change.
-
If the container needs a writable path, add an
emptyDirvolume and mount it there (run on any machine with kubectl access; continue in the same edit):- Under
spec.template.spec.volumes:- name: writable-tmpemptyDir: {} - Under the container that needs write access:
volumeMounts:- name: writable-tmpmountPath: /path/that/must/be/writable
Adjust
nameandmountPathto match the application’s needs. Do not removereadOnlyRootFilesystem: true. - Under
-
For Pods created directly (no controller), update or recreate their manifests (run on any machine with kubectl access):
- Export the Pod spec:
kubectl -n NAMESPACE get pod POD_NAME -o yaml > /tmp/pod-NAMESPACE-POD_NAME.yaml
- Edit the file, adding:
under each container and initContainer, and optionally addsecurityContext:readOnlyRootFilesystem: true
emptyDir+volumeMountsas in step 3. - Delete and recreate:
kubectl -n NAMESPACE delete pod POD_NAMEkubectl apply -f /tmp/pod-NAMESPACE-POD_NAME.yaml
- Export the Pod spec:
-
Allow the workloads to roll out the updated Pods and confirm all Pods are running (run on any machine with kubectl access):
kubectl get pods --all-namespaces -
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.containers // []) + (.spec.initContainers // []))[]| (.securityContext.readOnlyRootFilesystem == true) as $ok| select($ok | not)] as $rows| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'The output should be
is_compliant=truewhen all applicable containers use a read-only root filesystem.
Using kubectl
On any machine with kubectl access:
- Edit the Pod’s controller manifest (Deployment example)
kubectl -n YOUR_NAMESPACE get deploy YOUR_DEPLOYMENT -o yaml > /tmp/deploy.yaml
In /tmp/deploy.yaml, under each container (and initContainer) that should be read‑only, add or update:
spec:
template:
spec:
containers:
- name: your-container
image: your-image
securityContext:
readOnlyRootFilesystem: true
# If the app needs write access:
volumeMounts:
- name: writable-tmp
mountPath: /path/that/must/be/writable
volumes:
- name: writable-tmp
emptyDir: {}
Apply the updated manifest:
kubectl apply -f /tmp/deploy.yaml
Repeat the same pattern for other controllers (StatefulSet, DaemonSet, CronJob, Job) by replacing deploy and the kind in the kubectl get call.
- Directly patch a running Pod (only if it is not controlled by a Deployment/StatefulSet/etc.)
kubectl -n YOUR_NAMESPACE patch pod YOUR_POD \
--type='json' \
-p='[
{"op":"add","path":"/spec/containers/0/securityContext","value":{"readOnlyRootFilesystem":true}}
]'
Adjust the container index /0/ or patch additional containers as needed.
If a Pod-level initContainer also needs the setting:
kubectl -n YOUR_NAMESPACE patch pod YOUR_POD \
--type='json' \
-p='[
{"op":"add","path":"/spec/initContainers/0/securityContext","value":{"readOnlyRootFilesystem":true}}
]'
- Verification
Run the benchmark audit command again from 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.readOnlyRootFilesystem == true) 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)"
+ " readOnlyRootFilesystem=\(if .securityContext.readOnlyRootFilesystem == null then "unset" else .securityContext.readOnlyRootFilesystem end)"
+ " 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
# Purpose: Ensure all non-system Pods use readOnlyRootFilesystem=true on every container
# Scope: Any machine with kubectl access to the AKS cluster
# Impact: Pods will be deleted and recreated as updated Deployments/Workloads roll out
set -euo pipefail
# REQUIREMENTS:
# - kubectl configured to point to the AKS cluster
# - jq installed
timestamp="$(date +%Y%m%d-%H%M%S)"
backup_dir="./readonly-rootfs-backups-${timestamp}"
mkdir -p "${backup_dir}"
echo "==> Discovering non-compliant Pods and their owning workloads..."
# Fetch pods JSON once
pods_json="$(kubectl get pods --all-namespaces -o json)"
# Function: list unique owning controllers (kind, namespace, name, apiVersion)
# for pods with any container that has readOnlyRootFilesystem unset/false
echo "${pods_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
| select($own != null)
| (.spec.containers // [] + .spec.initContainers // []) as $cs
| select(any($cs[];
(.securityContext.readOnlyRootFilesystem // "unset") != true))
| "\($own.apiVersion),\($own.kind),\($m.namespace),\($own.name)"
' \
| sort -u > "${backup_dir}/non_compliant_owners.list"
if [[ ! -s "${backup_dir}/non_compliant_owners.list" ]]; then
echo "No non-compliant workloads found. Cluster is already compliant."
exit 0
fi
echo "Found non-compliant owning workloads:"
cat "${backup_dir}/non_compliant_owners.list"
echo
patch_count=0
while IFS=',' read -r apiVersion kind namespace name; do
# Skip types we cannot confidently patch generically
case "${kind}" in
Deployment|StatefulSet|DaemonSet|ReplicaSet|Job|CronJob)
;;
*)
echo "Skipping ${kind}/${namespace}/${name} (not a supported controller type). Review manually."
continue
;;
esac
res="${kind,,}" # lowercase resource type for kubectl
# Special pluralization
case "${res}" in
deployment) res="deployments" ;;
statefulset) res="statefulsets" ;;
daemonset) res="daemonsets" ;;
replicaset) res="replicasets" ;;
job) res="jobs" ;;
cronjob) res="cronjobs" ;;
esac
echo "==> Processing ${kind}/${namespace}/${name}"
# Backup full manifest
backup_file="${backup_dir}/${namespace}-${kind}-${name}.yaml"
kubectl -n "${namespace}" get "${res}" "${name}" -o yaml > "${backup_file}"
echo " Backup saved to ${backup_file}"
# Build a strategic merge patch that enforces readOnlyRootFilesystem: true
# for every container and initContainer in the pod spec.
# We fetch the current pod template, then construct a patch dynamically.
tmpl_json="$(kubectl -n "${namespace}" get "${res}" "${name}" -o json)"
containers_patch="$(echo "${tmpl_json}" | jq '
.spec.template.spec.containers
| map({
name: .name,
securityContext: (
.securityContext // {} | .readOnlyRootFilesystem = true
)
})
')"
init_containers_patch="$(echo "${tmpl_json}" | jq '
(.spec.template.spec.initContainers // [])
| map({
name: .name,
securityContext: (
.securityContext // {} | .readOnlyRootFilesystem = true
)
})
')"
patch_json="$(jq -n --argjson c "${containers_patch}" --argjson ic "${init_containers_patch}" '
{
"spec": {
"template": {
"spec": {
"containers": $c
}
}
}
+ if ($ic | length) > 0 then
{ "spec": { "template": { "spec": { "initContainers": $ic } } } }
else {} end
}
')"
echo " Applying patch to set readOnlyRootFilesystem=true..."
echo "${patch_json}" | kubectl -n "${namespace}" patch "${res}" "${name}" \
--type merge -p "$(cat)"
patch_count=$((patch_count + 1))
done < "${backup_dir}/non_compliant_owners.list"
if [[ "${patch_count}" -eq 0 ]]; then
echo "No supported controller types were patched. Remaining resources must be reviewed and fixed manually."
else
echo "Patched ${patch_count} workload(s). Waiting for Pods to be updated..."
# Give controllers time to roll out new Pods
sleep 10
fi
echo
echo "==> Verifying compliance (excluding kube-system, kube-public, kube-node-lease)..."
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 // []))[]
| (.securityContext.readOnlyRootFilesystem == true) as $ok
| select($ok | not)
| "ns=\($m.namespace) pod=\($m.name) container=\(.name) readOnlyRootFilesystem=\(if .securityContext.readOnlyRootFilesystem == null then "unset" else .securityContext.readOnlyRootFilesystem end)"
] as $rows
| if ($rows | length) == 0 then
"All evaluated containers have readOnlyRootFilesystem=true (is_compliant=true)"
else
"Non-compliant containers remain:\n" + ($rows | join("\n"))
end
'
echo
echo "Script completed. Review backups in ${backup_dir} if rollback is needed."