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
Remediation​
Manual Steps
-
List all non-system pods that mount
hostPathvolumes (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' -
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 -
Edit the saved manifest to remove
hostPathvolumes and use compliant alternatives (run on any machine with kubectl access):- In
spec.template.spec.volumes, delete each entry that containshostPath:. - 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[*].volumeMountsto reference the new volume names and paths instead of the removedhostPathvolumes. Save the file when done.
- In
-
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 -
For any standalone Pods (no controller owner) that use
hostPath, delete and recreate them from corrected manifests that do not definehostPathvolumes (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 -
Verify no remaining non-exempt pods use
hostPathvolumes (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:
- 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'
- For each offending pod, edit the owning workload (Deployment, StatefulSet, DaemonSet, Job, etc.) to remove
hostPathvolumes 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
- For standalone Pods defined by manifests, edit the Pod YAML similarly: remove each
hostPathunder.spec.volumes[]and replace withemptyDir, 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
- 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