From fa9b2c707b0deb67281de17a8d9b66df0a96a32d Mon Sep 17 00:00:00 2001 From: Haitao Zheng Date: Tue, 14 Jul 2026 13:23:59 +0200 Subject: [PATCH] feat(kubernetes): add hostPath volume check (#11837) Co-authored-by: Hugo P.Brito --- ...e-minimize-hostpath-volume-mounts.added.md | 1 + .../__init__.py | 0 ...imize_hostpath_volume_mounts.metadata.json | 38 ++++++++ .../core_minimize_hostpath_volume_mounts.py | 23 +++++ .../kubernetes/services/core/core_service.py | 17 ++++ .../kubernetes/services/core/conftest.py | 2 + ...re_minimize_hostpath_volume_mounts_test.py | 91 +++++++++++++++++++ 7 files changed, 172 insertions(+) create mode 100644 prowler/changelog.d/core-minimize-hostpath-volume-mounts.added.md create mode 100644 prowler/providers/kubernetes/services/core/core_minimize_hostpath_volume_mounts/__init__.py create mode 100644 prowler/providers/kubernetes/services/core/core_minimize_hostpath_volume_mounts/core_minimize_hostpath_volume_mounts.metadata.json create mode 100644 prowler/providers/kubernetes/services/core/core_minimize_hostpath_volume_mounts/core_minimize_hostpath_volume_mounts.py create mode 100644 tests/providers/kubernetes/services/core/core_minimize_hostpath_volume_mounts/core_minimize_hostpath_volume_mounts_test.py diff --git a/prowler/changelog.d/core-minimize-hostpath-volume-mounts.added.md b/prowler/changelog.d/core-minimize-hostpath-volume-mounts.added.md new file mode 100644 index 0000000000..af1595a354 --- /dev/null +++ b/prowler/changelog.d/core-minimize-hostpath-volume-mounts.added.md @@ -0,0 +1 @@ +`core_minimize_hostpath_volume_mounts` check for Kubernetes provider, detecting Pods that use `hostPath` volumes diff --git a/prowler/providers/kubernetes/services/core/core_minimize_hostpath_volume_mounts/__init__.py b/prowler/providers/kubernetes/services/core/core_minimize_hostpath_volume_mounts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/kubernetes/services/core/core_minimize_hostpath_volume_mounts/core_minimize_hostpath_volume_mounts.metadata.json b/prowler/providers/kubernetes/services/core/core_minimize_hostpath_volume_mounts/core_minimize_hostpath_volume_mounts.metadata.json new file mode 100644 index 0000000000..f25b22231f --- /dev/null +++ b/prowler/providers/kubernetes/services/core/core_minimize_hostpath_volume_mounts/core_minimize_hostpath_volume_mounts.metadata.json @@ -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 -n -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 / -n \n# For a standalone Pod, export the manifest, remove hostPath volumes, then recreate it:\nkubectl get pod -n -o yaml > pod.yaml\nkubectl delete pod -n \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\" \"\" {\n metadata {\n 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." +} diff --git a/prowler/providers/kubernetes/services/core/core_minimize_hostpath_volume_mounts/core_minimize_hostpath_volume_mounts.py b/prowler/providers/kubernetes/services/core/core_minimize_hostpath_volume_mounts/core_minimize_hostpath_volume_mounts.py new file mode 100644 index 0000000000..a693f52f0e --- /dev/null +++ b/prowler/providers/kubernetes/services/core/core_minimize_hostpath_volume_mounts/core_minimize_hostpath_volume_mounts.py @@ -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 diff --git a/prowler/providers/kubernetes/services/core/core_service.py b/prowler/providers/kubernetes/services/core/core_service.py index b53a81779c..dc44d8f51f 100644 --- a/prowler/providers/kubernetes/services/core/core_service.py +++ b/prowler/providers/kubernetes/services/core/core_service.py @@ -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): diff --git a/tests/providers/kubernetes/services/core/conftest.py b/tests/providers/kubernetes/services/core/conftest.py index 31c7e77d13..89add32aa9 100644 --- a/tests/providers/kubernetes/services/core/conftest.py +++ b/tests/providers/kubernetes/services/core/conftest.py @@ -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, ) diff --git a/tests/providers/kubernetes/services/core/core_minimize_hostpath_volume_mounts/core_minimize_hostpath_volume_mounts_test.py b/tests/providers/kubernetes/services/core/core_minimize_hostpath_volume_mounts/core_minimize_hostpath_volume_mounts_test.py new file mode 100644 index 0000000000..586abac5ec --- /dev/null +++ b/tests/providers/kubernetes/services/core/core_minimize_hostpath_volume_mounts/core_minimize_hostpath_volume_mounts_test.py @@ -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." + )