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
-
On any machine with kubectl access, list all pods using hostPath volumes (excluding system namespaces) and note which are controlled by higher-level resources (Deployment/DaemonSet/StatefulSet/Job/etc.):
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| [ ($m.ownerReferences // [])[] | select(.controller) ] | first as $own| select(($hp | length) > 0)| "\($m.namespace) \($m.name) \($own.kind // "Pod") \($own.name // $m.name)"][]' -
For each offending pod that is controlled by a workload resource (e.g., Deployment), edit the controller manifest and remove/replace the
hostPathvolumes. Example for a Deployment:kubectl -n NAMESPACE edit deployment CONTROLLER_NAMEIn the editor:
- Under
spec.template.spec.volumes, delete any entries containinghostPath:or replace them withemptyDir: {}or a PersistentVolume/PVC reference as appropriate. - Under
spec.template.spec.containers[].volumeMounts, remove the correspondingname:entries or adjust them to point to the new volume type. Save and exit; Kubernetes will roll out new pods without hostPath.
- Under
-
For pods created directly (no controller) using hostPath, export, modify, and reapply their manifests:
kubectl -n NAMESPACE get pod POD_NAME -o yaml > /tmp/pod-with-hostpath.yamlEdit
/tmp/pod-with-hostpath.yaml:- Remove the entire
metadata:block fieldsresourceVersion,uid,selfLink,creationTimestamp,status, and anyownerReferences. - Under
spec.volumes, remove or replace any entries containinghostPath:withemptyDir: {}or a PV/PVC. - Update
spec.containers[].volumeMountsto remove or point away from those volume names.
Then delete and recreate the pod:
kubectl -n NAMESPACE delete pod POD_NAMEkubectl -n NAMESPACE apply -f /tmp/pod-with-hostpath.yaml - Remove the entire
-
For workloads that legitimately need node storage, create and use PersistentVolumes and PersistentVolumeClaims instead of hostPath (on any machine with kubectl access). Example (adapt for your case) to create a PVC and reference it from the workload:
cat <<'EOF' > /tmp/pvc.yamlapiVersion: v1kind: PersistentVolumeClaimmetadata:name: app-pvcnamespace: NAMESPACEspec:accessModes:- ReadWriteOnceresources:requests:storage: 10GistorageClassName: standardEOFkubectl apply -f /tmp/pvc.yamlThen in the controller manifest (
kubectl -n NAMESPACE edit deployment CONTROLLER_NAME), use:spec:template:spec:volumes:- name: app-storagepersistentVolumeClaim:claimName: app-pvccontainers:- name: APP_CONTAINERvolumeMounts:- name: app-storagemountPath: /path/in/container -
For any third-party or GKE-managed add-ons that appear to use hostPath, review their documentation and only modify them if they are not required or if the vendor provides a hostPath-free configuration. If you must keep a hostPath-based add-on, document the risk and namespace, labels, and pod names for risk acceptance:
kubectl -n NAMESPACE get pod POD_NAME -o yaml > /tmp/justify-hostpath-POD_NAME.yaml -
Verification (on any machine with kubectl access): rerun the audit command and confirm there are no lines with
is_compliant=falseand nohostPaths=fields: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'The cluster is compliant when the output is either
is_compliant=trueonly, or all listed pods showis_compliant=trueand nohostPaths=.
Using kubectl
On any machine with kubectl access:
- Identify offending pods and their owners
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 line, note:
ns=<namespace>owner=<Kind>/<namespace>/<name>/...if present; otherwise treat it as a standalone Pod.
- Edit the controller manifest to remove
hostPath
For a Deployment (example):
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, delete any entries withhostPath:. - In
spec.template.spec.containers[].volumeMounts, remove mounts that referenced thosehostPathvolumes. - If needed, add a safer volume type, for example:
spec:
template:
spec:
volumes:
- name: app-data
emptyDir: {}
containers:
- name: app
volumeMounts:
- name: app-data
mountPath: /app/data
Apply the updated manifest:
kubectl apply -f /tmp/deploy-no-hostpath.yaml
Repeat the same pattern for other controller types as applicable:
- ReplicaSet:
kubectl -n NAMESPACE get rs NAME -o yaml > /tmp/rs-no-hostpath.yaml - StatefulSet:
kubectl -n NAMESPACE get sts NAME -o yaml > /tmp/sts-no-hostpath.yaml - DaemonSet:
kubectl -n NAMESPACE get ds NAME -o yaml > /tmp/ds-no-hostpath.yaml - Job/CronJob:
kubectl -n NAMESPACE get job|cronjob NAME -o yaml > /tmp/job-no-hostpath.yaml
Then edit to remove hostPath volumes and corresponding mounts, replace with emptyDir, PersistentVolumeClaims, or projected volumes, and:
kubectl apply -f /tmp/<edited-file>.yaml
- For standalone Pods (no controller)
Export, edit, and recreate without hostPath:
kubectl -n NAMESPACE get pod POD_NAME -o yaml \
| sed '/^[ ]*resourceVersion:/d;/^[ ]*uid:/d;/^[ ]*creationTimestamp:/d;/^[ ]*selfLink:/d;/^[ ]*managedFields:/d;/^[ ]*status:/,$d' \
> /tmp/pod-no-hostpath.yaml
Edit /tmp/pod-no-hostpath.yaml to:
- Remove
hostPathentries underspec.volumes. - Remove corresponding
volumeMounts. - Optionally add safer volume types as above.
Delete and recreate:
kubectl -n NAMESPACE delete pod POD_NAME
kubectl apply -f /tmp/pod-no-hostpath.yaml
- Verification
Run the original audit command and confirm either is_compliant=true only, or that no lines show hostPaths=:
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 script for: Pods Should Not Mount HostPath Volumes (CBP C1.7)
# Platform: GKE (managed)
# Surface: Kubernetes API objects via kubectl/manifests
#
# Runs on: any machine with kubectl access to the cluster/context.
#
# Behavior:
# - Scans all namespaces (except kube-system, kube-public, kube-node-lease)
# for pods using hostPath volumes.
# - For each owning controller (Deployment/DaemonSet/StatefulSet/Job/CronJob/ReplicaSet/ReplicationController)
# it writes a patched manifest with hostPath volumes removed into ./backup-hostpath-fix
# but DOES NOT APPLY ANY CHANGES AUTOMATICALLY.
# - Prints next-step instructions.
# - Re-runs the audit command to verify once you have applied the changes.
#
# This is idempotent: re-running overwrites the backup/patch files for the
# same controllers and re-runs the audit.
#
# IMPORTANT:
# - Removal of hostPath volumes is an application-level change and must be
# evaluated by the owning team. This script only prepares manifests to edit.
# - You must manually replace hostPath with a safer volume type
# (PersistentVolume, emptyDir, projected volumes, etc.) before applying.
set -euo pipefail
BACKUP_DIR="./backup-hostpath-fix"
mkdir -p "${BACKUP_DIR}"
echo "=== Step 1: Discover pods using hostPath volumes (excluding system namespaces) ==="
# Store audit output for later comparison
AUDIT_BEFORE="${BACKUP_DIR}/audit-before.txt"
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' | tee "${AUDIT_BEFORE}"
# Extract controllers that own non-compliant pods
echo
echo "=== Step 2: Identify owning controllers for non-compliant pods ==="
# jq filter to output: namespace kind name for each unique controller
CONTROLLERS_FILE="${BACKUP_DIR}/controllers.txt"
grep "is_compliant=false" "${AUDIT_BEFORE}" || true
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)
| [ (.spec.volumes // [])[] | select(.hostPath != null) ] as $hp
| select(($hp | length) > 0)
| [ (.metadata.ownerReferences // [])[] | select(.controller) ] | first as $own
| .metadata.namespace as $ns
| if $own == null then
# Standalone pod
"\($ns) Pod \(.metadata.name)"
else
"\($ns) \($own.kind) \($own.name)"
end
' | sort -u > "${CONTROLLERS_FILE}"
if [[ ! -s "${CONTROLLERS_FILE}" ]]; then
echo "No non-compliant pods discovered (or only in system namespaces)."
echo "Cluster appears compliant for this control."
exit 0
fi
echo "Controllers (or standalone Pods) owning pods with hostPath volumes:"
cat "${CONTROLLERS_FILE}"
echo
echo "=== Step 3: Export current manifests for review and patch preparation ==="
echo "Manifests will be written under: ${BACKUP_DIR}"
echo
while read -r NS KIND NAME; do
SAFE_KIND="${KIND}"
# Normalize common aliases to full kinds (kubectl handles short names but keep it explicit in filenames)
case "${KIND}" in
ds|daemonset|Daemonset) SAFE_KIND="DaemonSet" ;;
deploy|deployment|Deployment) SAFE_KIND="Deployment" ;;
sts|statefulset|Statefulset) SAFE_KIND="StatefulSet" ;;
rs|replicaset|ReplicaSet) SAFE_KIND="ReplicaSet" ;;
rc|replicationcontroller|Replicationcontroller) SAFE_KIND="ReplicationController" ;;
job|Job) SAFE_KIND="Job" ;;
cj|cronjob|Cronjob) SAFE_KIND="CronJob" ;;
pod|Pod) SAFE_KIND="Pod" ;;
esac
OUT_BASE="${BACKUP_DIR}/${NS}__${SAFE_KIND}__${NAME}"
MANIFEST_ORIG="${OUT_BASE}-original.yaml"
MANIFEST_PATCH="${OUT_BASE}-hostpath-removed.yaml"
echo "Exporting ${SAFE_KIND} ${NS}/${NAME} to ${MANIFEST_ORIG}"
# Export current manifest
if ! kubectl get "${SAFE_KIND}" "${NAME}" -n "${NS}" -o yaml > "${MANIFEST_ORIG}" 2>/dev/null; then
echo "WARNING: Failed to get ${SAFE_KIND} ${NS}/${NAME}. It may have been deleted or the kind is unsupported. Skipping."
continue
fi
# Prepare a version with hostPath volumes removed, for manual review/edit.
# This removes any volumes with hostPath and their corresponding volumeMounts in containers.
echo "Preparing tentative hostPath-removed manifest at ${MANIFEST_PATCH}"
# Use yq if available, otherwise just copy and let user edit manually.
if command -v yq >/dev/null 2>&1; then
yq '
# Remove hostPath volumes
(.. | select(has("volumes"))).volumes |=
( . // [] | map(select(.hostPath == null)) ) |
# For each container, remove volumeMounts that refer to deleted volumes
. as $root |
def strip_mounts(path):
(.spec.template.spec.containers // []) as $c |
(.spec.template.spec.volumes // []) as $v |
.spec.template.spec.containers |=
( $c
| map(
.volumeMounts |=
( . // [] |
map(select(.name as $n | ($v | map(.name) | index($n)) != null))
)
)
) ;
# For standalone Pod
if .kind == "Pod" then
(.spec.containers // []) as $c |
(.spec.volumes // []) as $v |
.spec.containers |=
( $c
| map(
.volumeMounts |=
( . // [] |
map(select(.name as $n | ($v | map(.name) | index($n)) != null))
)
)
)
else
strip_mounts(.)
end
' "${MANIFEST_ORIG}" > "${MANIFEST_PATCH}"
else
cp "${MANIFEST_ORIG}" "${MANIFEST_PATCH}"
echo "NOTE: yq not found; ${MANIFEST_PATCH} is a copy of the original."
echo " Manually edit it to remove hostPath volumes and corresponding mounts."
fi
done < "${CONTROLLERS_FILE}"
cat <<'EOF'
=== Step 4: Manual review & application required ===
For each generated *-hostpath-removed.yaml file in ./backup-hostpath-fix:
1. Open the file and ensure all `hostPath` volumes have been removed.
2. Replace them with safer volume types where needed:
- PersistentVolumeClaim (PVC) backed storage
- `emptyDir`
- projected/configMap/secret volumes
3. Confirm the resulting spec is acceptable for the workload owner.
4. Apply the manifest to the cluster, for example:
kubectl apply -f ./backup-hostpath-fix/<namespace>__<Kind>__<name>-hostpath-removed.yaml
This will update the controller and cause pods to be recreated without hostPath volumes.
EOF
echo "=== Step 5: Verification (re-run audit) ==="
echo "After you have applied the updated manifests, run the following command to verify:"
echo
cat <<'EOF'
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'
EOF
echo
echo "Compliance target: no line with 'is_compliant=false' outside system namespaces."