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 pods using
hostPathvolumes
Run on: any machine with kubectl accesskubectl 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' \| grep 'is_compliant=false' -
Export the owning workload manifest(s)
For each non‑compliant pod line, note theowner=field (e.g.,Deployment/ns/name/uid). Then export that owner resource. Examples:
Run on: any machine with kubectl access# Deploymentkubectl -n <NAMESPACE> get deployment <NAME> -o yaml > /tmp/<NAMESPACE>-<NAME>-deploy.yaml# StatefulSetkubectl -n <NAMESPACE> get statefulset <NAME> -o yaml > /tmp/<NAMESPACE>-<NAME>-sts.yaml# DaemonSetkubectl -n <NAMESPACE> get daemonset <NAME> -o yaml > /tmp/<NAMESPACE>-<NAME>-ds.yaml# If pod has no controller, export the pod spec directlykubectl -n <NAMESPACE> get pod <POD_NAME> -o yaml > /tmp/<NAMESPACE>-<POD_NAME>-pod.yaml -
Edit manifests to remove
hostPathvolumes and use safer alternatives
Run on: any machine with kubectl access (local file edits)
Open each exported YAML file and in thespec.template.spec.volumes(orspec.volumesfor standalone pods), remove entries that definehostPath. Example before/after:Before:
volumes:- name: datahostPath:path: /var/lib/myapptype: DirectoryOrCreateReplace with an approved alternative, such as
emptyDiror apersistentVolumeClaim(if appropriate for the application):emptyDirexample:volumes:- name: dataemptyDir: {}PVC example (requires an existing PVC or one you create separately):
volumes:- name: datapersistentVolumeClaim:claimName: myapp-data-pvcEnsure matching
volumeMountsrefer to the updated volume name and that any dependency on the host filesystem is addressed functionally (e.g., by moving data to PV-backed storage). -
Apply the updated manifests
Run on: any machine with kubectl access# Apply each modified filekubectl apply -f /tmp/<NAMESPACE>-<NAME>-deploy.yamlkubectl apply -f /tmp/<NAMESPACE>-<NAME>-sts.yamlkubectl apply -f /tmp/<NAMESPACE>-<NAME>-ds.yamlkubectl apply -f /tmp/<NAMESPACE>-<POD_NAME>-pod.yamlThe workloads will be recreated/rolled out with the new volume configuration; expect pod restarts as part of this change.
-
Confirm there are no remaining
hostPathvolumes in non‑system namespaces
Run on: any machine with kubectl accesskubectl 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'Compliance is achieved when the output is either a single line
is_compliant=trueor no lines containis_compliant=false.
Using kubectl
On any machine with kubectl access:
- Identify noncompliant Pods (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.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 violating controller (Deployment, DaemonSet, StatefulSet, Job, etc.), edit the manifest to remove
hostPathvolumes and replace them withemptyDir, a PersistentVolumeClaim, or another non-hostPath type.
Example: patch a Deployment to replace a hostPath with emptyDir (adjust names, namespace, volume names as needed):
kubectl -n default patch deployment example-app \
--type='json' \
-p='[
{
"op": "replace",
"path": "/spec/template/spec/volumes/0",
"value": {
"name": "data",
"emptyDir": {}
}
}
]'
If you use manifests in Git or files, update them and apply declaratively:
kubectl apply -f /absolute/path/to/updated-manifest.yaml
- For standalone Pods (no controller ownerReference), recreate them without
hostPath:
a. Export, edit, and save as a manifest:
kubectl -n default get pod example-pod -o yaml > /absolute/path/to/example-pod.yaml
Edit /absolute/path/to/example-pod.yaml:
- Remove any
hostPath:blocks underspec.volumes. - Adjust containers’
volumeMountsto use the new volume types.
b. Delete and recreate the Pod:
kubectl -n default delete pod example-pod
kubectl apply -f /absolute/path/to/example-pod.yaml
- Verification (same command as audit)
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 CBP C1.7:
# Remove hostPath volumes from Pods (excluding kube-system, kube-public, kube-node-lease).
#
# Runs on: any machine with kubectl and jq access to the cluster.
# Idempotent: can be safely re-run; only touches workloads that use hostPath.
set -euo pipefail
# --- Preconditions ------------------------------------------------------------
if ! command -v kubectl >/dev/null 2>&1; then
echo "ERROR: kubectl not found in PATH." >&2
exit 1
fi
if ! command_which_jq=$(command -v jq 2>/dev/null); then
echo "ERROR: jq not found in PATH (required for audit)." >&2
exit 1
fi
KUBECTL_CONTEXT="${KUBECTL_CONTEXT:-}"
KUBECTL="kubectl"
if [ -n "$KUBECTL_CONTEXT" ]; then
KUBECTL="$KUBECTL --context=${KUBECTL_CONTEXT}"
fi
# --- Helper: identify controllers owning non-compliant Pods -------------------
echo "Scanning for Pods that mount hostPath volumes (excluding system namespaces)..."
NON_COMPLIANT_JSON="$($KUBECTL get pods --all-namespaces -o json | jq '
.items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| select([(.spec.volumes // [])[] | select(.hostPath != null)] | length > 0)
')"
if [ -z "$NON_COMPLIANT_JSON" ]; then
echo "No Pods with hostPath volumes found (outside excluded namespaces). Cluster is compliant."
exit 0
fi
# Extract distinct owning controllers (kind, namespace, name, apiVersion)
# We will *not* patch Pods directly; instead patch the owning workload manifests.
CONTROLLERS_JSON="$(printf '%s\n' "$NON_COMPLIANT_JSON" | jq -r '
.
| .metadata as $m
| [ ($m.ownerReferences // [])[] | select(.controller) ] | first as $own
| if $own == null then empty else
{
kind: $own.kind,
apiVersion: $own.apiVersion,
namespace: $m.namespace,
name: $own.name
}
end
' | jq -s 'unique_by(.kind, .apiVersion, .namespace, .name)')"
if [ "$(printf '%s\n' "$CONTROLLERS_JSON" | jq 'length')" -eq 0 ]; then
echo "WARNING: Found Pods with hostPath but no controller ownerReferences."
echo "These are likely bare Pods; they must be edited or recreated manually."
echo "Listing affected Pods:"
printf '%s\n' "$NON_COMPLIANT_JSON" | jq -r '.metadata.namespace + "/" + .metadata.name'
echo
echo "No automated fix applied. Review and replace these Pods without hostPath volumes."
else
echo "Found the following owning controllers to inspect and manually remediate:"
printf '%s\n' "$CONTROLLERS_JSON" | jq -r '.[] | "\(.kind) \(.apiVersion) \(.namespace)/\(.name)"'
echo
cat <<'EOF'
NOTE: Per the benchmark remediation, hostPath volumes must be removed and replaced
with safer alternatives such as:
- PersistentVolumes / PersistentVolumeClaims
- emptyDir
- projected / configMap / secret volumes
Automatic, generic removal of hostPath from controller specs cannot be performed
safely without application-specific knowledge (e.g., what data source to use in
place of hostPath). You must update the manifests for the listed controllers
(Deployments, StatefulSets, DaemonSets, Jobs, etc.) to remove hostPath and
deploy a suitable alternative volume type.
This script will now only verify current non-compliance; no in-place mutation
is done to avoid breaking workloads.
EOF
fi
# --- Verification (same logic as audit, for idempotent re-run) ----------------
echo
echo "Re-running compliance audit after manual remediation (if any was done)..."
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.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
')"
echo "$AUDIT_OUTPUT"
# A simple compliance exit code: 0 if fully compliant, 1 otherwise
if printf '%s\n' "$AUDIT_OUTPUT" | grep -q 'is_compliant=false'; then
echo
echo "Cluster is NOT fully compliant. Remaining non-compliant Pods are listed above."
exit 1
else
echo
echo "Cluster is compliant with CBP C1.7 (no hostPath volumes on Pods outside system namespaces)."
exit 0
fi