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
-
Identify all non-system pods using hostPath (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 its managing controller (any machine with kubectl access). Replace NAMESPACE and OWNER_NAME as needed:
kubectl -n NAMESPACE get pod -o json POD_NAME | jq '.metadata.ownerReferences'If
.ownerReferencesis empty, you must edit the Pod manifest source in your own deployment process; do not edit live pods directly as they are not persistent. -
Export the controller manifest that creates the offending pods (any machine with kubectl access). Use the kind/name from ownerReferences, for example:
- Deployment:
kubectl -n NAMESPACE get deployment OWNER_NAME -o yaml > deployment-OWNER_NAME.yaml
- StatefulSet:
kubectl -n NAMESPACE get statefulset OWNER_NAME -o yaml > statefulset-OWNER_NAME.yaml
- DaemonSet:
kubectl -n NAMESPACE get daemonset OWNER_NAME -o yaml > daemonset-OWNER_NAME.yaml
- Deployment:
-
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 containhostPath:. - Update corresponding
spec.template.spec.containers[].volumeMounts[]to either:- Remove the mount completely, or
- Point to a replacement volume, such as:
volumes:- name: dataemptyDir: {}# or a PersistentVolumeClaim:# - name: data# persistentVolumeClaim:# claimName: pvc-name
Save the file.
- In
-
Apply the updated controller manifest so new pods are created without hostPath (any machine with kubectl access):
kubectl apply -f deployment-OWNER_NAME.yaml# or statefulset-OWNER_NAME.yaml / daemonset-OWNER_NAME.yaml as appropriateThen, force recreation of existing pods that were using hostPath:
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 -
Verification (any machine with kubectl access): rerun the audit and confirm all remaining rows show
is_compliant=trueand none havehostPaths=: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'
Using kubectl
On any machine with kubectl access:
- Identify Pods using
hostPath(and their controllers)
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
]'
- For each affected workload, edit the owning object (Deployment, StatefulSet, DaemonSet, etc.) to remove
hostPathand use a safer volume type such asemptyDiror a PersistentVolumeClaim.
Example: replace a hostPath volume with emptyDir in a Deployment.
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:
- name: data
hostPath:
path: /var/data
type: Directory
- Replace with:
- name: data
emptyDir: {}
- Ensure containers’
volumeMountsstill referencename: dataonly (no change usually needed).
Apply the updated manifest:
kubectl apply -f /tmp/deploy-no-hostpath.yaml
- For standalone Pods not managed by a controller, recreate them from a manifest without
hostPath:
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,statusfields. - In
.spec.volumes[], deletehostPathvolumes and replace withemptyDiror a PVC-backed volume, for example:
volumes:
- name: data
emptyDir: {}
Delete and recreate the Pod:
kubectl -n NAMESPACE delete pod POD_NAME
kubectl apply -f /tmp/pod-no-hostpath.yaml
- Verification (same 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)
] | if (length) == 0 then "is_compliant=true" else . end'
Automation
#!/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
'