mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 04:21:52 +00:00
feat(kubernetes): add hostPath volume check (#11837)
Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
`core_minimize_hostpath_volume_mounts` check for Kubernetes provider, detecting Pods that use `hostPath` volumes
|
||||
+38
@@ -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."
|
||||
}
|
||||
+23
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
+91
@@ -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."
|
||||
)
|
||||
Reference in New Issue
Block a user