> ## Documentation Index
> Fetch the complete documentation index at: https://cloudanix.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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

<Tabs>
  <Tab title="Remediation">
    ### Remediation

    <AccordionGroup>
      <Accordion title="Manual Steps" defaultOpen="true">
        1. Identify noncompliant pods and their owning controllers (run on any machine with kubectl access):
           ```bash theme={null}
           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'
           ```

        2. For each affected controller (Deployment/DaemonSet/StatefulSet/Job/CronJob), export its manifest (run on any machine with kubectl access; adjust kind/name/namespace):
           ```bash theme={null}
           kubectl -n <namespace> get <kind> <name> -o yaml > /tmp/<namespace>-<kind>-<name>.yaml
           ```

        3. Edit the manifest to set `readOnlyRootFilesystem: true` for each affected container, and, if writes are needed, define an `emptyDir` volume and mount it at the writable path (run on any machine with kubectl access):
           ```bash theme={null}
           vi /tmp/<namespace>-<kind>-<name>.yaml
           ```
           Example changes inside each affected container spec:
           ```yaml theme={null}
           spec:
             containers:
               - name: <container-name>
                 securityContext:
                   readOnlyRootFilesystem: true
                 volumeMounts:
                   - name: writable-tmp
                     mountPath: /tmp
             volumes:
               - name: writable-tmp
                 emptyDir: {}
           ```
           Apply the same pattern to `initContainers` if present.

        4. Apply the updated controller manifest so new pods use a read-only root filesystem (run on any machine with kubectl access):
           ```bash theme={null}
           kubectl apply -f /tmp/<namespace>-<kind>-<name>.yaml
           ```

        5. If necessary, recreate existing pods so they pick up the new security context (run on any machine with kubectl access):
           ```bash theme={null}
           kubectl -n <namespace> delete pod -l <label-selector-of-controller>
           ```
           The controller will create new pods with `readOnlyRootFilesystem: true`.

        6. Verify all non-system pods now have `readOnlyRootFilesystem` set to true (run on any machine with kubectl access):
           ```bash theme={null}
           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) == 0
               then "is_compliant=true"
               else $rows[]
               end'
           ```
      </Accordion>

      <Accordion title="Using kubectl">
        On any machine with `kubectl` access:

        1. Identify a noncompliant Pod and its owning workload (Deployment/DaemonSet/Job, etc.):

        ```bash theme={null}
        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"}'
        ```

        2. Export the owning workload manifest:

        ```bash theme={null}
        # Example for a Deployment
        kubectl get deployment -n <NAMESPACE> <DEPLOYMENT_NAME> -o yaml > /tmp/deployment-readonly-fix.yaml
        ```

        3. Edit containers to use a read-only root filesystem and add a writable `emptyDir` for any paths that must be writable.

        Example patch in `/tmp/deployment-readonly-fix.yaml`:

        ```yaml theme={null}
        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.

        4. Apply the updated manifest:

        ```bash theme={null}
        kubectl apply -f /tmp/deployment-readonly-fix.yaml
        ```

        5. (If the Pod is not controlled by a higher-level object, edit it directly; note this is not persistent across restarts):

        ```bash theme={null}
        kubectl edit pod -n <NAMESPACE> <POD_NAME>
        ```

        Add under each container / initContainer:

        ```yaml theme={null}
        securityContext:
          readOnlyRootFilesystem: true
        ```

        And define any `emptyDir` volumes and `volumeMounts` needed for writable paths.

        6. Verification (same scope: any machine with `kubectl`):

        Run the audit and confirm `readOnlyRootFilesystem=true` and `is_compliant=true` for all containers:

        ```bash theme={null}
        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'
        ```
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/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
        ```
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
