Skip to main content

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​

Remediation​

Manual Steps
  1. List all non-system pods that mount hostPath volumes (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.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 the owning object (Deployment/StatefulSet/DaemonSet/Job, etc.) from the owner= field, then export its manifest (example for a Deployment; run on any machine with kubectl access):

    kubectl -n <namespace> get deployment <name> -o yaml > /tmp/<namespace>-<name>.yaml
  3. Edit the saved manifest to remove hostPath volumes and use compliant alternatives (run on any machine with kubectl access):

    • In spec.template.spec.volumes, delete each entry that contains hostPath:.
    • Add replacement volumes such as:
      • emptyDir: {} for ephemeral storage, or
      • a persistentVolumeClaim: that references a suitable PVC, or
      • a projected/secret/configMap volume as appropriate.
    • Update spec.template.spec.containers[*].volumeMounts to reference the new volume names and paths instead of the removed hostPath volumes. Save the file when done.
  4. Apply the updated manifest and let Kubernetes recreate pods without hostPath (run on any machine with kubectl access):

    kubectl apply -f /tmp/<namespace>-<name>.yaml
  5. For any standalone Pods (no controller owner) that use hostPath, delete and recreate them from corrected manifests that do not define hostPath volumes (run on any machine with kubectl access):

    # Export, edit to remove hostPath and add compliant volumes, then:
    kubectl delete pod -n <namespace> <pod-name>
    kubectl apply -f /path/to/updated-pod.yaml
  6. Verify no remaining non-exempt pods use hostPath volumes (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.volumes // [])[] | select(.hostPath != null) ] as $hp
    | select(($hp | length) > 0)
    ] as $rows
    | if ($rows | length) == 0 then "is_compliant=true" else $rows[] | .metadata.name end'

    The cluster is compliant when the command outputs only is_compliant=true.

Using kubectl

On any machine with kubectl access:

  1. Identify noncompliant pods (excluding AKS control namespaces):
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'
  1. For each offending pod, edit the owning workload (Deployment, StatefulSet, DaemonSet, Job, etc.) to remove hostPath volumes and use compliant alternatives.

Example: replace a hostPath with emptyDir in a Deployment.

a. Fetch the existing Deployment manifest:

kubectl get deploy my-app -n my-namespace -o yaml > my-app-deploy.yaml

b. In my-app-deploy.yaml, find and change:

spec:
template:
spec:
volumes:
- name: app-data
hostPath:
path: /var/lib/my-app
type: DirectoryOrCreate

to, for example, an emptyDir:

spec:
template:
spec:
volumes:
- name: app-data
emptyDir: {}

Or to a PersistentVolumeClaim you have created:

spec:
template:
spec:
volumes:
- name: app-data
persistentVolumeClaim:
claimName: my-app-pvc

c. Apply the updated manifest:

kubectl apply -f my-app-deploy.yaml
  1. For standalone Pods defined by manifests, edit the Pod YAML similarly: remove each hostPath under .spec.volumes[] and replace with emptyDir, a PVC, or another non-hostPath volume type, then re-create the Pod:
kubectl delete pod my-pod -n my-namespace
kubectl apply -f my-pod.yaml
  1. Verification (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.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'
Automation
#!/usr/bin/env bash
#
# Remediation for: Pods Should Not Mount HostPath Volumes (CBP C1.7)
# Scope: any machine with kubectl access to the AKS cluster
#
# This script:
# 1. Detects non-system Pods using hostPath volumes.
# 2. Saves their YAML for manual remediation (no automatic rewrite, as the
# benchmark requires you to choose an appropriate alternative volume type).
# 3. Optionally deletes the offending Pods or their controllers when approved.
# 4. Re-runs the audit query to verify that no remaining Pods use hostPath.
#
# Idempotency:
# - It only acts on currently non-compliant Pods.
# - It skips system namespaces kube-system, kube-public, kube-node-lease.
# - It is safe to re-run; previously handled resources are ignored if removed.
#
# REQUIREMENTS:
# - kubectl configured for the AKS cluster.
# - jq installed.
#
# USAGE:
# ./fix_hostpath_pods.sh # detect and export manifests only
# DRY_RUN=false ./fix_hostpath_pods.sh # also delete Pods/controllers after confirmation
#
set -euo pipefail

DRY_RUN="${DRY_RUN:-true}" # default to detection/export only
EXPORT_DIR="${EXPORT_DIR:-./hostpath_pod_backups}"
mkdir -p "${EXPORT_DIR}"

echo "=== Detecting Pods using hostPath volumes (excluding core system namespaces) ==="

# Capture the raw audit output and a machine-parsable JSON list
AUDIT_OUTPUT="$(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 // []) as $vols
| [ $vols[] | select(.hostPath != null) ] as $hp
| select(($hp | length) > 0)
| {
namespace: $m.namespace,
name: $m.name,
uid: $m.uid,
creationTimestamp: ($m.creationTimestamp // ""),
node: (.spec.nodeName // ""),
labels: ($m.labels // {}),
owner: ([($m.ownerReferences // [])[] | select(.controller)] | first),
hostPaths: [ $hp[] | .hostPath.path ]
}
]')"

if [[ -z "${AUDIT_OUTPUT}" || "${AUDIT_OUTPUT}" == "[]" ]]; then
echo "No non-system Pods with hostPath volumes detected. Cluster is compliant."
exit 0
fi

echo "Non-compliant Pods detected:"
echo "${AUDIT_OUTPUT}" | jq -r '.[] | "- ns=\(.namespace) pod=\(.name) hostPaths=\(.hostPaths | join("+")) ownerKind=\(.owner.kind // "NONE") ownerName=\(.owner.name // "NONE")"'

# Export manifests for manual editing
echo
echo "=== Exporting non-compliant Pod manifests for manual remediation ==="
echo "Manifests will be saved under: ${EXPORT_DIR}"

echo "${AUDIT_OUTPUT}" | jq -c '.[]' | while read -r item; do
ns="$(echo "${item}" | jq -r '.namespace')"
pod="$(echo "${item}" | jq -r '.name')"
owner_kind="$(echo "${item}" | jq -r '.owner.kind // ""')"
owner_name="$(echo "${item}" | jq -r '.owner.name // ""')"

if [[ -n "${owner_kind}" && -n "${owner_name}" ]]; then
# We export the owning controller manifest, not the bare Pod,
# because changes must be made at the controller spec.
# Supported common kinds: Deployment, StatefulSet, DaemonSet, ReplicaSet, Job, CronJob.
# We rely on kubectl to pluralize correctly via 'get <kind>.<group>'.
export_name="${owner_kind,,}-${owner_name}-ns-${ns}.yaml"
if ! kubectl -n "${ns}" get "${owner_kind,,}/${owner_name}" >/dev/null 2>&1; then
echo " [WARN] Owner ${owner_kind}/${owner_name} in ns ${ns} not directly retrievable; exporting Pod instead."
export_name="pod-${pod}-ns-${ns}.yaml"
kubectl -n "${ns}" get pod "${pod}" -o yaml > "${EXPORT_DIR}/${export_name}"
else
echo " Exporting ${owner_kind}/${owner_name} in ns ${ns} to ${EXPORT_DIR}/${export_name}"
kubectl -n "${ns}" get "${owner_kind,,}/${owner_name}" -o yaml > "${EXPORT_DIR}/${export_name}"
fi
else
export_name="pod-${pod}-ns-${ns}.yaml"
echo " Exporting stand-alone Pod ${pod} in ns ${ns} to ${EXPORT_DIR}/${export_name}"
kubectl -n "${ns}" get pod "${pod}" -o yaml > "${EXPORT_DIR}/${export_name}"
fi
done

cat <<'EOF'

NEXT STEPS (manual per benchmark guidance):
1. For each exported manifest under ./hostpath_pod_backups:
- Locate any volumes with type `hostPath`.
- Remove those `hostPath` volumes and all references under `volumeMounts`.
- Replace them with:
* PersistentVolumeClaim-backed volumes when you need persistent storage, or
* `emptyDir` volumes for ephemeral storage, or
* Projected/configMap/secret volumes where appropriate.
2. Apply your updated manifests back to the cluster with:
kubectl apply -f <edited-manifest>.yaml

This script can optionally delete existing non-compliant Pods (or their controllers)
so that only your fixed versions run afterwards.
EOF

if [[ "${DRY_RUN}" == "true" ]]; then
echo
echo "DRY_RUN is true; no deletions will be performed."
echo "Set DRY_RUN=false to enable interactive deletion of offending resources after you apply fixed manifests."
else
echo
read -r -p "DRY_RUN=false: proceed to delete non-compliant Pods/controllers? [y/N]: " ans
if [[ "${ans}" =~ ^[Yy]$ ]]; then
echo
echo "=== Deleting non-compliant Pods or their controllers ==="
echo "${AUDIT_OUTPUT}" | jq -c '.[]' | while read -r item; do
ns="$(echo "${item}" | jq -r '.namespace')"
pod="$(echo "${item}" | jq -r '.name')"
owner_kind="$(echo "${item}" | jq -r '.owner.kind // ""')"
owner_name="$(echo "${item}" | jq -r '.owner.name // ""')"

if [[ -n "${owner_kind}" && -n "${owner_name}" ]]; then
# Delete controller; Kubernetes will recreate Pods from your updated manifest
if kubectl -n "${ns}" get "${owner_kind,,}/${owner_name}" >/dev/null 2>&1; then
echo " Deleting ${owner_kind}/${owner_name} in ns ${ns}"
kubectl -n "${ns}" delete "${owner_kind,,}/${owner_name}"
else
echo " [WARN] Owner ${owner_kind}/${owner_name} not found; deleting Pod ${pod} in ns ${ns} instead."
kubectl -n "${ns}" delete pod "${pod}"
fi
else
echo " Deleting stand-alone Pod ${pod} in ns ${ns}"
kubectl -n "${ns}" delete pod "${pod}"
fi
done
else
echo "Skipping deletions at user request."
fi
fi

echo
echo "=== Verification: re-running audit to confirm compliance ==="
VERIFY_OUTPUT="$(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)
] | length')"

if [[ "${VERIFY_OUTPUT}" == "0" ]]; then
echo "Verification passed: no non-system Pods with hostPath volumes remain."
exit 0
else
echo "Verification FAILED: ${VERIFY_OUTPUT} non-system Pods with hostPath volumes still present."
echo "Re-run the export, update the manifests to remove hostPath, apply them, and run this script again."
exit 1
fi