> ## 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 non-compliant Pods and their 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
             | select($ok | not)
             | "ns=\($m.namespace) pod=\($m.name) container=\(.name) image=\(.image)"
               + (if $own == null then "" else " ownerKind=\($own.kind) ownerName=\($own.name)" end)
             ][]'
           ```

        2. For each affected controller (Deployment, StatefulSet, DaemonSet, Job, CronJob, etc.), edit the manifest to set `readOnlyRootFilesystem: true` (run on any machine with kubectl access). Example for a Deployment:
           ```bash theme={null}
           kubectl -n NAMESPACE edit deployment DEPLOYMENT_NAME
           ```
           In each affected container (and initContainer if present) under `spec.template.spec.containers[]` (or `initContainers[]`), ensure:
           ```yaml theme={null}
           securityContext:
             readOnlyRootFilesystem: true
           ```
           If `securityContext` already exists, add only the `readOnlyRootFilesystem: true` line.

        3. If a container needs a writable path, add an `emptyDir` and mount it (same `kubectl edit` session as step 2):
           * Under `spec.template.spec.volumes`:
             ```yaml theme={null}
             - name: writable-tmp
               emptyDir: {}
             ```
           * Under the appropriate container:
             ```yaml theme={null}
             volumeMounts:
               - name: writable-tmp
                 mountPath: /path/that/must/be/writable
             ```
           Adjust `name` and `mountPath` as needed for each workload.

        4. For standalone Pods that are not managed by a higher-level controller, export, edit, and re-apply (run on any machine with kubectl access):
           ```bash theme={null}
           kubectl -n NAMESPACE get pod POD_NAME -o yaml > /tmp/pod-POD_NAME.yaml
           ```
           Edit `/tmp/pod-POD_NAME.yaml`:
           * Remove `metadata.uid`, `metadata.resourceVersion`, `metadata.creationTimestamp`, `metadata.ownerReferences`, `status` and other server-populated fields.
           * Under `spec.containers[]` and `spec.initContainers[]` set:
             ```yaml theme={null}
             securityContext:
               readOnlyRootFilesystem: true
             ```
             and, if needed, add `emptyDir` + `volumeMounts` as in step 3.
             Apply:
           ```bash theme={null}
           kubectl -n NAMESPACE delete pod POD_NAME
           kubectl -n NAMESPACE apply -f /tmp/pod-POD_NAME.yaml
           ```

        5. Wait for updated workloads to roll out and confirm Pods are running (run on any machine with kubectl access):
           ```bash theme={null}
           kubectl -n NAMESPACE get pods -w
           ```

        6. Verification (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.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'
           ```
           Confirm the output is `is_compliant=true` or that all listed containers show `is_compliant=true`.
      </Accordion>

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

        1. Identify non‑compliant Pods (and their controllers)

        ```bash theme={null}
        kubectl get pods --all-namespaces -o wide
        ```

        Use the audit output you already have to see the `owner=` field (e.g. Deployment/StatefulSet/Job). You must edit the owning controller, not the live Pod.

        2. Patch a controller to set `readOnlyRootFilesystem: true`

        Example: patch a Deployment in namespace `my-app` named `web` so all containers use a read‑only root filesystem:

        ```bash theme={null}
        kubectl -n my-app patch deployment web \
          --type='json' \
          -p='[
            {
              "op": "add",
              "path": "/spec/template/spec/containers/0/securityContext",
              "value": {
                "readOnlyRootFilesystem": true
              }
            }
          ]'
        ```

        If `securityContext` already exists, use:

        ```bash theme={null}
        kubectl -n my-app patch deployment web \
          --type='json' \
          -p='[
            {
              "op": "add",
              "path": "/spec/template/spec/containers/0/securityContext/readOnlyRootFilesystem",
              "value": true
            }
          ]'
        ```

        Repeat for each non‑compliant controller (Deployment, StatefulSet, DaemonSet, Job, CronJob), adjusting the resource type, name, namespace, and container index as needed.

        3. Provide writable paths via `emptyDir` (when needed)

        If an application needs to write to specific paths, mount `emptyDir` volumes there instead of leaving the root filesystem writable. Example manifest snippet:

        ```yaml theme={null}
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: web
          namespace: my-app
        spec:
          replicas: 1
          selector:
            matchLabels:
              app: web
          template:
            metadata:
              labels:
                app: web
            spec:
              volumes:
                - name: tmp
                  emptyDir: {}
              containers:
                - name: web
                  image: gcr.io/my-project/web:1.0.0
                  securityContext:
                    readOnlyRootFilesystem: true
                  volumeMounts:
                    - name: tmp
                      mountPath: /tmp
        ```

        Apply the manifest:

        ```bash theme={null}
        kubectl apply -f /absolute/path/to/web-deployment.yaml
        ```

        4. Verification

        After controllers have rolled out new Pods, re‑run the audit command from any machine with kubectl:

        ```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
        #
        # Enforce readOnlyRootFilesystem=true on Pods' owning controllers in a GKE cluster.
        # Applies to all namespaces except kube-system, kube-public, kube-node-lease.
        #
        # REQUIREMENTS (run on any machine with kubectl access):
        #   - kubectl
        #   - jq
        #
        # LIMITATIONS:
        #   - Only edits controllers (Deployment, StatefulSet, DaemonSet, ReplicaSet, Job, CronJob).
        #   - Skips naked Pods and controllers you lack RBAC to patch.
        #   - For containers that must write, you must manually add an emptyDir and mount it.

        set -euo pipefail

        echo "[INFO] Discovering non-compliant containers..."
        NON_COMPLIANT_JSON="$(kubectl get pods --all-namespaces -o json | jq -c '
          .items[]
          | select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
          | (.metadata.ownerReferences // []) as $owns
          | [ $owns[] | select(.controller == true) ] as $ctrls
          | if ($ctrls | length) == 0 then empty else
              . as $pod
              | $ctrls[0] as $own
              | ((.spec.containers // []) + (.spec.initContainers // []))[]
              | select(.securityContext.readOnlyRootFilesystem != true)
              | {
                  podNamespace: $pod.metadata.namespace,
                  podName: $pod.metadata.name,
                  ownerKind: $own.kind,
                  ownerName: $own.name,
                  ownerUid: $own.uid,
                  containerName: .name
                }
            end
        ')"

        if [[ -z "${NON_COMPLIANT_JSON}" ]]; then
          echo "[INFO] All checked containers already have readOnlyRootFilesystem=true (excluding system namespaces)."
          exit 0
        fi

        # Build a unique list of controllers to patch
        echo "[INFO] Building list of owning controllers to patch..."
        CONTROLLERS_JSON="$(printf '%s\n' "${NON_COMPLIANT_JSON}" | jq -cs '
          . as $items
          | reduce $items[] as $i ({}; .[
              ($i.podNamespace + "|" + $i.ownerKind + "|" + $i.ownerName)
            ] = 1)
          | keys[]
          | split("|")
          | { namespace: .[0], kind: .[1], name: .[2] }
        ')"

        if [[ -z "${CONTROLLERS_JSON}" ]]; then
          echo "[INFO] No controllers found to patch (only naked Pods are affected)."
          echo "[INFO] Review and recreate affected Pods with readOnlyRootFilesystem=true in their Pod specs."
          exit 0
        fi

        echo "[INFO] Controllers to patch:"
        printf '%s\n' "${CONTROLLERS_JSON}" | jq -r '.namespace + " " + .kind + " " + .name'

        patch_controller() {
          local ns="$1"
          local kind="$2"
          local name="$3"

          echo "[INFO] Patching ${kind}/${ns}/${name} to set readOnlyRootFilesystem=true..."

          # Determine the path to pod template containers depending on kind/apiVersion
          # Strategy: fetch as JSON, transform with jq, then apply via kubectl apply -f -
          local tmpfile
          tmpfile="$(mktemp)"

          if ! kubectl -n "${ns}" get "${kind}" "${name}" -o json > "${tmpfile}"; then
            echo "[WARN] Failed to fetch ${kind}/${ns}/${name}; skipping."
            rm -f "${tmpfile}"
            return
          fi

          # Detect spec path
          local jq_filter
          case "${kind}" in
            Deployment|ReplicaSet|StatefulSet|DaemonSet)
              jq_filter='
                .spec.template.spec as $spec
                | .spec.template.spec.containers |=
                    (map(.securityContext.readOnlyRootFilesystem = true))
                | if ($spec.initContainers // null) != null then
                    .spec.template.spec.initContainers |=
                      (map(.securityContext.readOnlyRootFilesystem = true))
                  else .
                  end
              '
              ;;
            Job)
              jq_filter='
                .spec.template.spec as $spec
                | .spec.template.spec.containers |=
                    (map(.securityContext.readOnlyRootFilesystem = true))
                | if ($spec.initContainers // null) != null then
                    .spec.template.spec.initContainers |=
                      (map(.securityContext.readOnlyRootFilesystem = true))
                  else .
                  end
              '
              ;;
            CronJob)
              jq_filter='
                .spec.jobTemplate.spec.template.spec as $spec
                | .spec.jobTemplate.spec.template.spec.containers |=
                    (map(.securityContext.readOnlyRootFilesystem = true))
                | if ($spec.initContainers // null) != null then
                    .spec.jobTemplate.spec.template.spec.initContainers |=
                      (map(.securityContext.readOnlyRootFilesystem = true))
                  else .
                  end
              '
              ;;
            *)
              echo "[WARN] Kind ${kind} not handled by this script; skipping ${kind}/${ns}/${name}."
              rm -f "${tmpfile}"
              return
              ;;
          esac

          local patched
          if ! patched="$(jq "${jq_filter}" "${tmpfile}")"; then
            echo "[WARN] Failed to transform ${kind}/${ns}/${name}; skipping."
            rm -f "${tmpfile}"
            return
          fi

          # Idempotent: applying the same spec again is safe
          if ! printf '%s\n' "${patched}" | kubectl apply -f - >/dev/null; then
            echo "[WARN] Failed to apply patch for ${kind}/${ns}/${name}; skipping."
            rm -f "${tmpfile}"
            return
          fi

          rm -f "${tmpfile}"
          echo "[INFO] Patched ${kind}/${ns}/${name}."
        }

        # Patch each controller
        printf '%s\n' "${CONTROLLERS_JSON}" | jq -c '.' | while read -r ctrl; do
          ns="$(printf '%s\n' "${ctrl}" | jq -r '.namespace')"
          kind="$(printf '%s\n' "${ctrl}" | jq -r '.kind')"
          name="$(printf '%s\n' "${ctrl}" | jq -r '.name')"
          patch_controller "${ns}" "${kind}" "${name}"
        done

        echo "[INFO] Waiting for pods to be recreated with updated specs..."
        sleep 10

        echo "[INFO] Re-running compliance 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.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 | map(select(. | test("is_compliant=false$"))) | length) == 0
            then "is_compliant=true"
            else $rows[]
            end
        '
        ```
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
