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 and their owning controllers (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| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own| ((.spec.containers // []) + (.spec.initContainers // []))[]| (.securityContext.readOnlyRootFilesystem == true) as $ok| "ns=\($m.namespace) pod=\($m.name) container=\(.name) image=\(.image)"+ (if $own == null then "" else " ownerKind=\($own.kind) ownerName=\($own.name)" end)+ " readOnlyRootFilesystem=\(if .securityContext.readOnlyRootFilesystem == null then "unset" else .securityContext.readOnlyRootFilesystem end)"+ " is_compliant=\(if $ok then "true" else "false" end)"][]' | grep 'is_compliant=false' -
For each affected controller (Deployment/DaemonSet/StatefulSet/Job/CronJob), export its manifest (run on any machine with kubectl access; adjust kind/name/namespace):
kubectl -n <namespace> get <kind> <name> -o yaml > /tmp/<namespace>-<kind>-<name>.yaml -
Edit the manifest to set
readOnlyRootFilesystem: truefor each affected container, and, if writes are needed, define anemptyDirvolume and mount it at the writable path (run on any machine with kubectl access):vi /tmp/<namespace>-<kind>-<name>.yamlExample changes inside each affected container spec:
spec:containers:- name: <container-name>securityContext:readOnlyRootFilesystem: truevolumeMounts:- name: writable-tmpmountPath: /tmpvolumes:- name: writable-tmpemptyDir: {}Apply the same pattern to
initContainersif present. -
Apply the updated controller manifest so new pods use a read-only root filesystem (run on any machine with kubectl access):
kubectl apply -f /tmp/<namespace>-<kind>-<name>.yaml -
If necessary, recreate existing pods so they pick up the new security context (run on any machine with kubectl access):
kubectl -n <namespace> delete pod -l <label-selector-of-controller>The controller will create new pods with
readOnlyRootFilesystem: true. -
Verify all non-system pods now have
readOnlyRootFilesystemset to true (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| "ns=\($m.namespace) pod=\($m.name) container=\(.name)"+ " readOnlyRootFilesystem=\(if .securityContext.readOnlyRootFilesystem == null then "unset" else .securityContext.readOnlyRootFilesystem end)"+ " is_compliant=\(if $ok then "true" else "false" end)"] as $rows| if ($rows | map(select(. | test("is_compliant=false"))) | length) == 0then "is_compliant=true"else $rows[]end'
Using kubectl
On any machine with kubectl access:
- Identify a noncompliant Pod and its owning workload (Deployment/DaemonSet/Job, etc.):
kubectl get pods --all-namespaces -o wide
kubectl get pod -n <NAMESPACE> <POD_NAME> -o jsonpath='{.metadata.ownerReferences[0].kind}{"\n"}{.metadata.ownerReferences[0].name}{"\n"}'
- Export the owning workload manifest:
# Example for a Deployment
kubectl get deployment -n <NAMESPACE> <DEPLOYMENT_NAME> -o yaml > /tmp/deployment-readonly-fix.yaml
- Edit containers to use a read-only root filesystem and add a writable
emptyDirfor any paths that must be writable.
Example patch in /tmp/deployment-readonly-fix.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: my-namespace
spec:
template:
spec:
volumes:
- name: tmp-dir
emptyDir: {}
containers:
- name: app
image: my-registry/my-image:tag
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp-dir
mountPath: /tmp
initContainers:
- name: init-app
image: my-registry/my-init-image:tag
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp-dir
mountPath: /tmp
Apply the same securityContext.readOnlyRootFilesystem: true and emptyDir pattern to every container and initContainer that currently needs a writable path.
- Apply the updated manifest:
kubectl apply -f /tmp/deployment-readonly-fix.yaml
- (If the Pod is not controlled by a higher-level object, edit it directly; note this is not persistent across restarts):
kubectl edit pod -n <NAMESPACE> <POD_NAME>
Add under each container / initContainer:
securityContext:
readOnlyRootFilesystem: true
And define any emptyDir volumes and volumeMounts needed for writable paths.
- Verification (same scope: any machine with
kubectl):
Run the audit and confirm readOnlyRootFilesystem=true and is_compliant=true for all containers:
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
# Remediate: Ensure containers use a read-only root filesystem
# Scope: Any machine with kubectl access to the OKE cluster
# Requirements: kubectl, jq, yq (https://mikefarah.gitbook.io/yq/) available in PATH
# Idempotent: can be re-run; only touches non-compliant workloads
set -euo pipefail
# Namespace filter: exclude Kubernetes control namespaces per check definition
EXCLUDED_NAMESPACES_REGEX='^(kube-system|kube-public|kube-node-lease)$'
timestamp() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
log() {
echo "[$(timestamp)] $*"
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "ERROR: Required command '$1' not found in PATH" >&2
exit 1
}
}
need_cmd kubectl
need_cmd jq
need_cmd yq
# 1) Discover non-compliant Pods (for reporting only; fix is applied on controllers)
log "Discovering non-compliant Pods (for visibility)..."
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 // []) | map(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) image=\(.image) readOnlyRootFilesystem=\(if .securityContext.readOnlyRootFilesystem == null then "unset" else .securityContext.readOnlyRootFilesystem end)"
][]' || true
# 2) Target higher-level controllers to make fix durable
# We patch: Deployments, StatefulSets, DaemonSets, ReplicaSets, Jobs, CronJobs
KINDS=("Deployment" "StatefulSet" "DaemonSet" "ReplicaSet" "Job" "CronJob")
log "Identifying non-compliant controllers and patching with readOnlyRootFilesystem: true ..."
for kind in "${KINDS[@]}"; do
# Get all objects of this kind (may not exist in all clusters)
if ! kubectl get "$kind" --all-namespaces >/dev/null 2>&1; then
continue
fi
# JSONPath to list namespace/name pairs
mapfile -t resources < <(kubectl get "$kind" --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}{end}' 2>/dev/null || true)
for entry in "${resources[@]}"; do
ns=$(awk '{print $1}' <<<"$entry")
name=$(awk '{print $2}' <<<"$entry")
[[ -z "$ns" || -z "$name" ]] && continue
[[ "$ns" =~ $EXCLUDED_NAMESPACES_REGEX ]] && continue
# Export the resource as YAML
tmpfile="$(mktemp)"
kubectl get "$kind" "$name" -n "$ns" -o yaml > "$tmpfile"
# Build a patched YAML in-memory using yq:
# - For each normal container and init container:
# * ensure securityContext exists
# * set readOnlyRootFilesystem: true
patched="$(yq eval '
# Helper: set readOnlyRootFilesystem on a container entry
def set_rofs:
. as $root |
if $root == null then . else
.securityContext = ($root.securityContext // {}) |
.securityContext.readOnlyRootFilesystem = true
end;
( .spec.template.spec.containers |= (map( set_rofs )) ) |
( .spec.template.spec.initContainers |= (map( set_rofs )) )' "$tmpfile")"
# Check if anything changed (idempotency)
if diff -q <(yq eval '.' "$tmpfile") <(echo "$patched") >/dev/null 2>&1; then
rm -f "$tmpfile"
continue
fi
log "Patching $kind $ns/$name to set readOnlyRootFilesystem: true on all containers..."
# Apply the patched manifest
# --server-side helps with managed fields; can be changed to client-side apply if desired
printf '%s\n' "$patched" | kubectl apply -f -
rm -f "$tmpfile"
done
done
log "Waiting for updated workloads to roll out..."
# This is a best-effort wait; non-fatal if some remain progressing
for kind in Deployment StatefulSet DaemonSet; do
if ! kubectl get "$kind" --all-namespaces >/dev/null 2>&1; then
continue
fi
mapfile -t resources < <(kubectl get "$kind" --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}{end}' 2>/dev/null || true)
for entry in "${resources[@]}"; do
ns=$(awk '{print $1}' <<<"$entry")
name=$(awk '{print $2}' <<<"$entry")
[[ -z "$ns" || -z "$name" ]] && continue
[[ "$ns" =~ $EXCLUDED_NAMESPACES_REGEX ]] && continue
log "Waiting for $kind $ns/$name to be ready (timeout 300s)..."
kubectl rollout status "$kind/$name" -n "$ns" --timeout=300s || true
done
done
# 3) Verification: rerun the benchmark-style audit and show only remaining failures
log "Verification: listing any remaining containers without readOnlyRootFilesystem=true..."
out="$(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)
| "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=false"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end')"
echo "$out"
if [[ "$out" == "is_compliant=true" ]]; then
log "All evaluated Pods are now compliant (readOnlyRootFilesystem=true)."
else
log "Some Pods remain non-compliant. These may be:
- Managed by controllers not covered by this script
- Standalone Pods created directly
- System/third-party components that require manual review.
Review each listed Pod and its owning resource; if the container truly requires writes,
mount an emptyDir to the specific paths needing write access instead of keeping the root
filesystem writable."
fi