> ## 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.

# Pods Should Not Mount HostPath Volumes

### More Info:

Verifies no pod mounts a hostPath volume. hostPath exposes the node filesystem to the pod and can be used to escape to the host.

### Risk Level

High

### Address

Security

### Compliance Standards

* Cloudanix Best Practice

### Triage and Remediation

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

    <AccordionGroup>
      <Accordion title="Manual Steps" defaultOpen="true">
        1. Identify all non-system pods using hostPath (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.volumes // [])[] | select(.hostPath != null) ] as $hp
             | "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)
               + (if ($hp | length) == 0 then "" else " hostPaths=\([ $hp[] | .hostPath.path ] | join("+"))" end)
               + " is_compliant=\(if ($hp | length) > 0 then "false" else "true" end)"
             ] as $rows
             | if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'
           ```

        2. For each non-compliant pod, identify its managing controller (any machine with kubectl access). Replace NAMESPACE and OWNER\_NAME as needed:
           ```bash theme={null}
           kubectl -n NAMESPACE get pod -o json POD_NAME | jq '.metadata.ownerReferences'
           ```
           If `.ownerReferences` is empty, you must edit the Pod manifest source in your own deployment process; do not edit live pods directly as they are not persistent.

        3. Export the controller manifest that creates the offending pods (any machine with kubectl access). Use the kind/name from ownerReferences, for example:
           * Deployment:
             ```bash theme={null}
             kubectl -n NAMESPACE get deployment OWNER_NAME -o yaml > deployment-OWNER_NAME.yaml
             ```
           * StatefulSet:
             ```bash theme={null}
             kubectl -n NAMESPACE get statefulset OWNER_NAME -o yaml > statefulset-OWNER_NAME.yaml
             ```
           * DaemonSet:
             ```bash theme={null}
             kubectl -n NAMESPACE get daemonset OWNER_NAME -o yaml > daemonset-OWNER_NAME.yaml
             ```

        4. Edit the exported manifest locally to remove hostPath volumes and use safer alternatives (local file edit on your workstation):
           * In `spec.template.spec.volumes[]`, delete any entries that contain `hostPath:`.
           * Update corresponding `spec.template.spec.containers[].volumeMounts[]` to either:
             * Remove the mount completely, or
             * Point to a replacement volume, such as:
               ```yaml theme={null}
               volumes:
                 - name: data
                   emptyDir: {}
               # or a PersistentVolumeClaim:
               # - name: data
               #   persistentVolumeClaim:
               #     claimName: pvc-name
               ```
           Save the file.

        5. Apply the updated controller manifest so new pods are created without hostPath (any machine with kubectl access):
           ```bash theme={null}
           kubectl apply -f deployment-OWNER_NAME.yaml
           # or statefulset-OWNER_NAME.yaml / daemonset-OWNER_NAME.yaml as appropriate
           ```
           Then, force recreation of existing pods that were using hostPath:
           ```bash theme={null}
           kubectl -n NAMESPACE rollout restart deployment/OWNER_NAME
           # or:
           # kubectl -n NAMESPACE rollout restart statefulset/OWNER_NAME
           # kubectl -n NAMESPACE rollout restart daemonset/OWNER_NAME
           ```

        6. Verification (any machine with kubectl access): rerun the audit and confirm all remaining rows show `is_compliant=true` and none have `hostPaths=`:
           ```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.volumes // [])[] | select(.hostPath != null) ] as $hp
             | "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)
               + (if ($hp | length) == 0 then "" else " hostPaths=\([ $hp[] | .hostPath.path ] | join("+"))" end)
               + " is_compliant=\(if ($hp | length) > 0 then "false" else "true" end)"
             ] as $rows
             | if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'
           ```
      </Accordion>

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

        1. Identify Pods using `hostPath` (and their controllers)

        ```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.volumes // [])[] | select(.hostPath != null) ] as $hp
          | select(($hp | length) > 0)
          | [ ($m.ownerReferences // [])[] | select(.controller) ] | first
          ]'
        ```

        2. For each affected workload, edit the owning object (Deployment, StatefulSet, DaemonSet, etc.) to remove `hostPath` and use a safer volume type such as `emptyDir` or a PersistentVolumeClaim.

        Example: replace a `hostPath` volume with `emptyDir` in a Deployment.

        ```bash theme={null}
        kubectl -n NAMESPACE get deploy DEPLOYMENT_NAME -o yaml > /tmp/deploy-no-hostpath.yaml
        ```

        Edit `/tmp/deploy-no-hostpath.yaml`:

        * In `.spec.template.spec.volumes[]`, remove entries like:

        ```yaml theme={null}
        - name: data
          hostPath:
            path: /var/data
            type: Directory
        ```

        * Replace with:

        ```yaml theme={null}
        - name: data
          emptyDir: {}
        ```

        * Ensure containers’ `volumeMounts` still reference `name: data` only (no change usually needed).

        Apply the updated manifest:

        ```bash theme={null}
        kubectl apply -f /tmp/deploy-no-hostpath.yaml
        ```

        3. For standalone Pods not managed by a controller, recreate them from a manifest without `hostPath`:

        ```bash theme={null}
        kubectl -n NAMESPACE get pod POD_NAME -o yaml > /tmp/pod-no-hostpath.yaml
        ```

        Edit `/tmp/pod-no-hostpath.yaml`:

        * Remove `metadata.uid`, `metadata.resourceVersion`, `metadata.creationTimestamp`, `metadata.ownerReferences`, `status` fields.
        * In `.spec.volumes[]`, delete `hostPath` volumes and replace with `emptyDir` or a PVC-backed volume, for example:

        ```yaml theme={null}
        volumes:
          - name: data
            emptyDir: {}
        ```

        Delete and recreate the Pod:

        ```bash theme={null}
        kubectl -n NAMESPACE delete pod POD_NAME
        kubectl apply -f /tmp/pod-no-hostpath.yaml
        ```

        4. Verification (same 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.volumes // [])[] | select(.hostPath != null) ] as $hp
          | select(($hp | length) > 0)
          ] | if (length) == 0 then "is_compliant=true" else . end'
        ```
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/usr/bin/env bash
        #
        # remediate_hostpath_pods.sh
        #
        # Purpose:
        #   For each non-system Pod that mounts a hostPath volume, patch the Pod spec
        #   to remove all hostPath volumes and any volumeMounts that reference them.
        #
        # Scope:
        #   Run on any machine with:
        #     - kubectl installed
        #     - Access/permissions to modify workloads in the target namespaces
        #
        # Notes / Limitations:
        #   - This script ONLY edits live Pod objects.
        #   - It does NOT modify controllers (Deployments, DaemonSets, etc.).
        #     Controllers will usually recreate Pods with the original spec (and
        #     hostPath volumes). To make changes persistent, you must also update the
        #     controllers’ manifests/templates manually or via your IaC.
        #   - Removing hostPath volumes may break workloads that depend on them.
        #     Review impact and ensure valid alternatives (e.g. PersistentVolumes,
        #     emptyDir, projected volumes) are configured in the owning controller.
        #
        # Idempotency:
        #   - Re-running the script will skip Pods with no hostPath volumes.
        #   - Re-running after a previous successful run will typically find no
        #     remaining hostPath volumes unless controllers recreated noncompliant Pods.

        set -euo pipefail

        # Ensure jq is available
        if ! command -v jq >/dev/null 2>&1; then
          echo "ERROR: jq is required but not installed. Install jq and re-run." >&2
          exit 1
        fi

        # Ensure kubectl is available
        if ! command -v kubectl >/dev/null 2>&1; then
          echo "ERROR: kubectl is required but not installed. Install kubectl and re-run." >&2
          exit 1
        fi

        echo "Discovering Pods that mount hostPath volumes (excluding kube-system, kube-public, kube-node-lease)..."

        # Get the list of Pods with hostPath volumes (excluding system namespaces)
        mapfile -t HOSTPATH_PODS < <(
          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)
              | select((.spec.volumes // []) | map(select(.hostPath != null)) | length > 0)
              | "\(.metadata.namespace) \(.metadata.name)"
            '
        )

        if [ "${#HOSTPATH_PODS[@]}" -eq 0 ]; then
          echo "No Pods with hostPath volumes found outside system namespaces. Nothing to do."
        else
          echo "Found ${#HOSTPATH_PODS[@]} Pod(s) with hostPath volumes:"
          printf '  - %s\n' "${HOSTPATH_PODS[@]}"
        fi

        for entry in "${HOSTPATH_PODS[@]}"; do
          ns=$(awk '{print $1}' <<< "${entry}")
          pod=$(awk '{print $2}' <<< "${entry}")

          echo
          echo "Processing Pod ${pod} in namespace ${ns}..."

          # Check if Pod still exists (it may have been deleted/recreated since discovery)
          if ! kubectl get pod "${pod}" -n "${ns}" >/dev/null 2>&1; then
            echo "  Pod no longer exists; skipping."
            continue
          fi

          # Extract full Pod spec as JSON
          pod_json=$(kubectl get pod "${pod}" -n "${ns}" -o json)

          # Determine hostPath volumes names and paths
          hp_info=$(jq -r '
            (.spec.volumes // [])
            | map(select(.hostPath != null) | {name: .name, path: .hostPath.path})
          ' <<< "${pod_json}")

          hp_count=$(jq 'length' <<< "${hp_info}")
          if [ "${hp_count}" -eq 0 ]; then
            echo "  Pod no longer has hostPath volumes; skipping."
            continue
          fi

          echo "  Found ${hp_count} hostPath volume(s):"
          jq -r '.[] | "    - name=\(.name) path=\(.path)"' <<< "${hp_info}"

          # Compute list of hostPath volume names
          hp_names=$(jq -r '.[].name' <<< "${hp_info}" | tr '\n' ' ')
          read -r -a hp_names_array <<< "${hp_names}"

          # Build JSONPatch operations:
          #   - Remove each hostPath volume in spec.volumes
          #   - Remove volumeMounts in each container/ephemeralContainer/ initContainer
          #     that reference these hostPath volumes
          patches='[]'

          # Remove hostPath volumes from spec.volumes
          for vol_name in "${hp_names_array[@]}"; do
            patches=$(jq --arg vname "${vol_name}" '
              . + [{
                "op": "remove",
                "path": "/spec/volumes"
              }]
              ' <<< "${patches}" 2>/dev/null || echo "${patches}")
          done

          # For each container type, remove volumeMounts that reference hostPath volumes
          for ctype in containers initContainers ephemeralContainers; do
            # Determine number of containers of this type
            c_count=$(jq ".spec.${ctype} // [] | length" <<< "${pod_json}")
            if [ "${c_count}" -eq 0 ]; then
              continue
            fi

            for ((i=0; i< c_count; i++)); do
              # For each container, identify indices of volumeMounts to remove
              vm_indices=$(jq --argjson vnames "$(printf '%s\n' "${hp_names_array[@]}" | jq -R . | jq -s .)" \
                ".spec.${ctype}[${i}].volumeMounts // [] 
                 | to_entries
                 | map(select(.value.name as \$n | \$vnames | index(\$n)))
                 | map(.key)
                 | reverse" <<< "${pod_json}")

              # For each index, append a remove operation
              idx_count=$(jq 'length' <<< "${vm_indices}")
              if [ "${idx_count}" -eq 0 ]; then
                continue
              fi

              for ((k=0; k< idx_count; k++)); do
                idx=$(jq ".[$k]" <<< "${vm_indices}")
                patches=$(jq --arg ctype "${ctype}" --argjson ci "${i}" --argjson vi "${idx}" '
                  . + [{
                    "op": "remove",
                    "path": ("/spec/" + $ctype + "/" + ($ci|tostring) + "/volumeMounts/" + ($vi|tostring))
                  }]
                ' <<< "${patches}")
              done
            done
          done

          # Deduplicate patch operations and make them valid:
          # - Removing /spec/volumes entirely is destructive; instead filter to remove
          #   only entries with hostPath. We'll compute a full replacement of spec.volumes.
          #
          # Rebuild spec.volumes without hostPath entries and use a single "replace" op.
          new_volumes=$(jq '
            (.spec.volumes // [])
            | map(select(.hostPath == null))
          ' <<< "${pod_json}")

          patches=$(jq --argjson vols "${new_volumes}" '
            # filter out any prior /spec/volumes remove ops
            map(select(.path != "/spec/volumes"))
            + [{
              "op": "replace",
              "path": "/spec/volumes",
              "value": $vols
            }]
          ' <<< "${patches}")

          # If no volumeMount-related patches and volumes are already filtered, this may be a no-op
          if [ "$(jq 'length' <<< "${patches}")" -eq 0 ]; then
            echo "  No applicable patches to apply; skipping."
            continue
          fi

          echo "  Applying JSONPatch to Pod..."
          echo "${patches}" | kubectl patch pod "${pod}" -n "${ns}" \
            --type=json -p "$(cat)" >/dev/null

          echo "  Patch applied."
        done

        echo
        echo "Verification: re-running audit to ensure no remaining Pods with hostPath volumes..."

        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.volumes // [])[] | select(.hostPath != null) ] as $hp
          | select(($hp | length) > 0)
          | "kind=Pod ns=\($m.namespace) name=\($m.name) hostPaths=\([ $hp[] | .hostPath.path ] | join("+")) is_compliant=false"
          ] as $rows
          | if ($rows | length) == 0 then "is_compliant=true" else $rows[] end
        '
        ```
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
