#!/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