feat(kubernetes): add hostPath volume check (#11837)

Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
This commit is contained in:
Haitao Zheng
2026-07-14 13:23:59 +02:00
committed by GitHub
parent 22401a54a0
commit fa9b2c707b
7 changed files with 172 additions and 0 deletions
@@ -0,0 +1 @@
`core_minimize_hostpath_volume_mounts` check for Kubernetes provider, detecting Pods that use `hostPath` volumes
@@ -0,0 +1,38 @@
{
"Provider": "kubernetes",
"CheckID": "core_minimize_hostpath_volume_mounts",
"CheckTitle": "Pod does not use hostPath volumes",
"CheckType": [],
"ServiceName": "core",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Pod",
"ResourceGroup": "container",
"Description": "**Kubernetes Pods** are evaluated for volumes of type `hostPath`, which mount paths from the node filesystem into a pod.",
"Risk": "`hostPath` volumes weaken the container boundary by exposing node files to workloads. A compromised container can read sensitive host data, tamper with node files, or use writable mounts to escalate privileges and affect node availability.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://kubernetes.io/docs/concepts/storage/volumes/#hostpath",
"https://kubernetes.io/docs/concepts/security/pod-security-standards/"
],
"Remediation": {
"Code": {
"CLI": "kubectl get pod <pod_name> -n <namespace> -o jsonpath='{range .metadata.ownerReferences[*]}{.kind}/{.name}{\"\\n\"}{end}{range .spec.volumes[?(@.hostPath)]}{.name}{\"\\t\"}{.hostPath.path}{\"\\n\"}{end}'\n# If the Pod is managed by a controller, edit the owning workload and remove hostPath volumes from its Pod template:\nkubectl edit <workload_kind>/<workload_name> -n <namespace>\n# For a standalone Pod, export the manifest, remove hostPath volumes, then recreate it:\nkubectl get pod <pod_name> -n <namespace> -o yaml > pod.yaml\nkubectl delete pod <pod_name> -n <namespace>\nkubectl apply -f pod.yaml",
"NativeIaC": "",
"Other": "1. Identify the workload that owns the failing Pod (Deployment/DaemonSet/StatefulSet/Job) or confirm it is a standalone Pod\n2. Edit the Pod template and remove volumes that define `hostPath`\n3. Replace hostPath with a safer volume type such as ConfigMap, Secret, emptyDir, persistentVolumeClaim, or a CSI volume when appropriate\n4. Apply the updated manifest and allow the workload to recreate Pods without hostPath volumes",
"Terraform": "```hcl\nresource \"kubernetes_pod\" \"<example_resource_name>\" {\n metadata {\n name = \"<example_resource_name>\"\n }\n spec {\n container {\n name = \"app\"\n image = \"nginx\"\n }\n # Do not define host_path blocks under volume.\n }\n}\n```"
},
"Recommendation": {
"Text": "Disallow `hostPath` volumes by default with Pod Security Admission or policy-as-code controls. Permit narrow, read-only exceptions only for trusted system workloads, and prefer Kubernetes-native volume types that do not expose the node filesystem.",
"Url": "https://hub.prowler.com/check/core_minimize_hostpath_volume_mounts"
}
},
"Categories": [
"container-security",
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Exceptions for hostPath volumes should be narrowly scoped, read-only where possible, and monitored."
}
@@ -0,0 +1,23 @@
from prowler.lib.check.models import Check, Check_Report_Kubernetes
from prowler.providers.kubernetes.services.core.core_client import core_client
class core_minimize_hostpath_volume_mounts(Check):
def execute(self) -> list[Check_Report_Kubernetes]:
findings = []
for pod in core_client.pods.values():
report = Check_Report_Kubernetes(metadata=self.metadata(), resource=pod)
report.status = "PASS"
report.status_extended = f"Pod {pod.name} does not use hostPath volumes."
for volume in pod.volumes or []:
if volume.get("host_path"):
report.status = "FAIL"
report.status_extended = (
f"Pod {pod.name} uses hostPath volume {volume['name']}."
)
break
findings.append(report)
return findings
@@ -32,6 +32,7 @@ class Core(KubernetesService):
ephemeral_containers = self._build_containers(
pod.spec.ephemeral_containers
)
volumes = self._build_volumes(pod.spec.volumes)
self.pods[pod.metadata.uid] = Pod(
name=pod.metadata.name,
uid=pod.metadata.uid,
@@ -54,6 +55,7 @@ class Core(KubernetesService):
containers=containers,
init_containers=init_containers,
ephemeral_containers=ephemeral_containers,
volumes=volumes,
)
except Exception as error:
logger.error(
@@ -101,6 +103,20 @@ class Core(KubernetesService):
)
return pod_containers
@staticmethod
def _build_volumes(volumes) -> List[dict]:
pod_volumes = []
for volume in volumes or []:
pod_volumes.append(
{
"name": volume.name,
"host_path": (
volume.host_path.to_dict() if volume.host_path else None
),
}
)
return pod_volumes
def _list_config_maps(self):
try:
response = self.client.list_config_map_for_all_namespaces()
@@ -188,6 +204,7 @@ class Pod(BaseModel):
containers: Optional[dict]
init_containers: Optional[dict] = None
ephemeral_containers: Optional[dict] = None
volumes: Optional[List[dict]] = None
class ConfigMap(BaseModel):
@@ -32,6 +32,7 @@ def make_pod(
containers=None,
init_containers=None,
ephemeral_containers=None,
volumes=None,
name="test-pod",
uid="test-pod-uid",
):
@@ -53,6 +54,7 @@ def make_pod(
containers=containers or {},
init_containers=init_containers or {},
ephemeral_containers=ephemeral_containers or {},
volumes=volumes,
)
@@ -0,0 +1,91 @@
from kubernetes import client
from prowler.providers.kubernetes.services.core.core_service import Core
from tests.providers.kubernetes.services.core.conftest import (
make_core_client,
make_pod,
run_check,
)
MODULE = (
"prowler.providers.kubernetes.services.core."
"core_minimize_hostpath_volume_mounts.core_minimize_hostpath_volume_mounts"
)
CLASS = "core_minimize_hostpath_volume_mounts"
class TestCoreMinimizeHostpathVolumeMounts:
def test_build_volumes_maps_kubernetes_hostpath_volume(self):
volumes = Core._build_volumes(
[
client.V1Volume(
name="host-logs",
host_path=client.V1HostPathVolumeSource(
path="/var/log",
type="Directory",
),
)
]
)
assert volumes == [
{
"name": "host-logs",
"host_path": {"path": "/var/log", "type": "Directory"},
}
]
def test_no_resources(self):
result = run_check(MODULE, CLASS, make_core_client({}))
assert len(result) == 0
def test_no_hostpath_volumes_pass(self):
pod = make_pod(
volumes=[
{"name": "config", "host_path": None},
{"name": "scratch", "host_path": None},
]
)
result = run_check(MODULE, CLASS, make_core_client({pod.uid: pod}))
assert result[0].status == "PASS"
assert (
result[0].status_extended == "Pod test-pod does not use hostPath volumes."
)
def test_hostpath_volume_fails(self):
pod = make_pod(
volumes=[
{
"name": "host-logs",
"host_path": {"path": "/var/log", "type": "Directory"},
}
]
)
result = run_check(MODULE, CLASS, make_core_client({pod.uid: pod}))
assert result[0].status == "FAIL"
assert (
result[0].status_extended == "Pod test-pod uses hostPath volume host-logs."
)
def test_mixed_volumes_fail_on_hostpath(self):
pod = make_pod(
volumes=[
{"name": "config", "host_path": None},
{
"name": "host-socket",
"host_path": {"path": "/var/run/docker.sock", "type": "Socket"},
},
]
)
result = run_check(MODULE, CLASS, make_core_client({pod.uid: pod}))
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Pod test-pod uses hostPath volume host-socket."
)