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. Identify pods using 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' \
    | grep 'is_compliant=false'
  2. Export the owning workload manifest(s)
    For each non‑compliant pod line, note the owner= field (e.g., Deployment/ns/name/uid). Then export that owner resource. Examples:
    Run on: any machine with kubectl access

    # Deployment
    kubectl -n <NAMESPACE> get deployment <NAME> -o yaml > /tmp/<NAMESPACE>-<NAME>-deploy.yaml

    # StatefulSet
    kubectl -n <NAMESPACE> get statefulset <NAME> -o yaml > /tmp/<NAMESPACE>-<NAME>-sts.yaml

    # DaemonSet
    kubectl -n <NAMESPACE> get daemonset <NAME> -o yaml > /tmp/<NAMESPACE>-<NAME>-ds.yaml

    # If pod has no controller, export the pod spec directly
    kubectl -n <NAMESPACE> get pod <POD_NAME> -o yaml > /tmp/<NAMESPACE>-<POD_NAME>-pod.yaml
  3. Edit manifests to remove hostPath volumes and use safer alternatives
    Run on: any machine with kubectl access (local file edits)
    Open each exported YAML file and in the spec.template.spec.volumes (or spec.volumes for standalone pods), remove entries that define hostPath. Example before/after:

    Before:

    volumes:
    - name: data
    hostPath:
    path: /var/lib/myapp
    type: DirectoryOrCreate

    Replace with an approved alternative, such as emptyDir or a persistentVolumeClaim (if appropriate for the application):

    emptyDir example:

    volumes:
    - name: data
    emptyDir: {}

    PVC example (requires an existing PVC or one you create separately):

    volumes:
    - name: data
    persistentVolumeClaim:
    claimName: myapp-data-pvc

    Ensure matching volumeMounts refer 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).

  4. Apply the updated manifests
    Run on: any machine with kubectl access

    # Apply each modified file
    kubectl apply -f /tmp/<NAMESPACE>-<NAME>-deploy.yaml
    kubectl apply -f /tmp/<NAMESPACE>-<NAME>-sts.yaml
    kubectl apply -f /tmp/<NAMESPACE>-<NAME>-ds.yaml
    kubectl apply -f /tmp/<NAMESPACE>-<POD_NAME>-pod.yaml

    The workloads will be recreated/rolled out with the new volume configuration; expect pod restarts as part of this change.

  5. Confirm there are no remaining hostPath volumes in non‑system namespaces
    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'

    Compliance is achieved when the output is either a single line is_compliant=true or no lines contain is_compliant=false.

Using kubectl

On any machine with kubectl access:

  1. 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'
  1. For each violating controller (Deployment, DaemonSet, StatefulSet, Job, etc.), edit the manifest to remove hostPath volumes and replace them with emptyDir, 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
  1. 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 under spec.volumes.
  • Adjust containers’ volumeMounts to 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
  1. 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