From 22401a54a0bd0a8b477e005c91db391797263ad8 Mon Sep 17 00:00:00 2001 From: weedle02 <139173311+Weedle02@users.noreply.github.com> Date: Tue, 14 Jul 2026 07:23:17 -0400 Subject: [PATCH] feat(kubernetes): add core check for readonly root filesystem enabled (#11835) Co-authored-by: wbro <80239840234@proton.me> Co-authored-by: Hugo P.Brito --- ...-readonly-root-filesystem-enabled.added.md | 1 + .../__init__.py | 0 ...only_root_filesystem_enabled.metadata.json | 37 +++++ .../core_readonly_root_filesystem_enabled.py | 38 ++++++ .../kubernetes/services/core/conftest.py | 3 +- ...e_readonly_root_filesystem_enabled_test.py | 126 ++++++++++++++++++ .../services/core/core_service_test.py | 64 +++++++++ 7 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 prowler/changelog.d/core-readonly-root-filesystem-enabled.added.md create mode 100644 prowler/providers/kubernetes/services/core/core_readonly_root_filesystem_enabled/__init__.py create mode 100644 prowler/providers/kubernetes/services/core/core_readonly_root_filesystem_enabled/core_readonly_root_filesystem_enabled.metadata.json create mode 100644 prowler/providers/kubernetes/services/core/core_readonly_root_filesystem_enabled/core_readonly_root_filesystem_enabled.py create mode 100644 tests/providers/kubernetes/services/core/core_readonly_root_filesystem_enabled/core_readonly_root_filesystem_enabled_test.py create mode 100644 tests/providers/kubernetes/services/core/core_service_test.py diff --git a/prowler/changelog.d/core-readonly-root-filesystem-enabled.added.md b/prowler/changelog.d/core-readonly-root-filesystem-enabled.added.md new file mode 100644 index 0000000000..cfc398b111 --- /dev/null +++ b/prowler/changelog.d/core-readonly-root-filesystem-enabled.added.md @@ -0,0 +1 @@ +`core_readonly_root_filesystem_enabled` check for Kubernetes provider, verifying that every container in each Pod explicitly sets `readOnlyRootFilesystem: true` in its security context diff --git a/prowler/providers/kubernetes/services/core/core_readonly_root_filesystem_enabled/__init__.py b/prowler/providers/kubernetes/services/core/core_readonly_root_filesystem_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/kubernetes/services/core/core_readonly_root_filesystem_enabled/core_readonly_root_filesystem_enabled.metadata.json b/prowler/providers/kubernetes/services/core/core_readonly_root_filesystem_enabled/core_readonly_root_filesystem_enabled.metadata.json new file mode 100644 index 0000000000..d28b7ab94f --- /dev/null +++ b/prowler/providers/kubernetes/services/core/core_readonly_root_filesystem_enabled/core_readonly_root_filesystem_enabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "kubernetes", + "CheckID": "core_readonly_root_filesystem_enabled", + "CheckTitle": "Containers should run with a read-only root filesystem", + "CheckType": [], + "ServiceName": "core", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Pod", + "ResourceGroup": "container", + "Description": "**Kubernetes Pods** are evaluated to ensure every container sets `readOnlyRootFilesystem: true` in its `securityContext`. A writable root filesystem lets an attacker who gains code execution modify binaries, drop tools, or persist malicious files inside the container.", + "Risk": "Without a read-only root filesystem an attacker with code execution inside a container can tamper with binaries or libraries (**integrity**), write credential files or exfiltration tools (**confidentiality**), and persist across process restarts (**availability**).", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/", + "https://kubernetes.io/docs/concepts/security/pod-security-standards/" + ], + "Remediation": { + "Code": { + "CLI": "kubectl patch deployment/ -n -p '{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"\",\"securityContext\":{\"readOnlyRootFilesystem\":true}}]}}}}'\n# Deployment template example for regular containers. For init containers, use initContainers instead of containers. Ephemeral containers are added through the ephemeralcontainers subresource; remove or recreate debug containers with a compliant securityContext.", + "NativeIaC": "", + "Other": "1. Open your Kubernetes Dashboard (or your cloud provider's Kubernetes console) and locate the workload managing the failing Pod (Deployment/StatefulSet/DaemonSet)\n2. Click Edit to modify the manifest (YAML)\n3. For each container missing `securityContext.readOnlyRootFilesystem: true`, add or set it to `true`\n4. If the container needs write access, mount a writable `emptyDir` or `persistentVolumeClaim` at the specific paths that require writes instead of making the entire root filesystem writable\n5. Save/Apply the changes to trigger a rollout\n6. Verify new Pods have `securityContext.readOnlyRootFilesystem` set to `true`", + "Terraform": "```hcl\nresource \"kubernetes_pod\" \"main\" {\n metadata {\n name = \"\"\n }\n spec {\n container {\n name = \"app\"\n image = \"nginx\"\n security_context {\n read_only_root_filesystem = true # Critical: enforce a read-only root filesystem\n }\n }\n }\n}\n```" + }, + "Recommendation": { + "Text": "Set `readOnlyRootFilesystem: true` for every regular, init, and ephemeral container that is part of a Pod spec. Mount writable `emptyDir` volumes only at the specific paths that genuinely need write access (e.g., `/tmp`, `/var/cache`). Enforce this at admission time with custom admission policies such as ValidatingAdmissionPolicy or OPA/Gatekeeper.", + "Url": "https://hub.prowler.com/check/core_readonly_root_filesystem_enabled" + } + }, + "Categories": [ + "container-security" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Regular, init, and ephemeral containers are evaluated. Workloads that genuinely require root filesystem writes should mount writable volumes at specific paths and may be exempted via Prowler's mutelist." +} diff --git a/prowler/providers/kubernetes/services/core/core_readonly_root_filesystem_enabled/core_readonly_root_filesystem_enabled.py b/prowler/providers/kubernetes/services/core/core_readonly_root_filesystem_enabled/core_readonly_root_filesystem_enabled.py new file mode 100644 index 0000000000..cb7eebcdf7 --- /dev/null +++ b/prowler/providers/kubernetes/services/core/core_readonly_root_filesystem_enabled/core_readonly_root_filesystem_enabled.py @@ -0,0 +1,38 @@ +from prowler.lib.check.models import Check, Check_Report_Kubernetes +from prowler.providers.kubernetes.services.core.core_client import core_client + + +class core_readonly_root_filesystem_enabled(Check): + """Check whether every container in each Pod has readOnlyRootFilesystem set to true.""" + + def execute(self) -> list[Check_Report_Kubernetes]: + """Execute the Kubernetes read-only root filesystem check. + + Returns: + List of check reports for Kubernetes pods. + """ + 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} has read-only root filesystem enabled for all containers." + + for containers in ( + pod.containers, + pod.init_containers, + pod.ephemeral_containers, + ): + for container in (containers or {}).values(): + if ( + container.security_context.get("read_only_root_filesystem") + is not True + ): + report.status = "FAIL" + report.status_extended = f"Pod {pod.name} container {container.name} does not have readOnlyRootFilesystem set to true." + break + if report.status == "FAIL": + break + + findings.append(report) + + return findings diff --git a/tests/providers/kubernetes/services/core/conftest.py b/tests/providers/kubernetes/services/core/conftest.py index 9b0becd33c..31c7e77d13 100644 --- a/tests/providers/kubernetes/services/core/conftest.py +++ b/tests/providers/kubernetes/services/core/conftest.py @@ -13,6 +13,7 @@ def make_container( resources=None, liveness_probe=None, readiness_probe=None, + security_context=None, ): return Container( name=name, @@ -20,7 +21,7 @@ def make_container( command=None, ports=None, env=None, - security_context={}, + security_context=security_context if security_context is not None else {}, resources=resources, liveness_probe=liveness_probe, readiness_probe=readiness_probe, diff --git a/tests/providers/kubernetes/services/core/core_readonly_root_filesystem_enabled/core_readonly_root_filesystem_enabled_test.py b/tests/providers/kubernetes/services/core/core_readonly_root_filesystem_enabled/core_readonly_root_filesystem_enabled_test.py new file mode 100644 index 0000000000..352c08dc85 --- /dev/null +++ b/tests/providers/kubernetes/services/core/core_readonly_root_filesystem_enabled/core_readonly_root_filesystem_enabled_test.py @@ -0,0 +1,126 @@ +from tests.providers.kubernetes.services.core.conftest import ( + make_container, + make_core_client, + make_pod, + run_check, +) + +MODULE = "prowler.providers.kubernetes.services.core.core_readonly_root_filesystem_enabled.core_readonly_root_filesystem_enabled" +CLASS = "core_readonly_root_filesystem_enabled" + + +class TestCoreReadonlyRootFilesystemEnabled: + def test_no_resources(self): + result = run_check(MODULE, CLASS, make_core_client({})) + + assert len(result) == 0 + + def test_readonly_root_filesystem_enabled_pass(self): + pod = make_pod( + containers={ + "app": make_container( + security_context={"read_only_root_filesystem": True} + ) + } + ) + + result = run_check(MODULE, CLASS, make_core_client({pod.uid: pod})) + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Pod test-pod has read-only root filesystem enabled for all containers." + ) + + def test_readonly_root_filesystem_false_fail(self): + pod = make_pod( + containers={ + "app": make_container( + security_context={"read_only_root_filesystem": False} + ) + } + ) + + result = run_check(MODULE, CLASS, make_core_client({pod.uid: pod})) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Pod test-pod container app does not have readOnlyRootFilesystem set to true." + ) + + def test_readonly_root_filesystem_unset_fail(self): + pod = make_pod(containers={"app": make_container(security_context={})}) + + result = run_check(MODULE, CLASS, make_core_client({pod.uid: pod})) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Pod test-pod container app does not have readOnlyRootFilesystem set to true." + ) + + def test_mixed_containers_fail(self): + pod = make_pod( + containers={ + "app": make_container( + name="app", + security_context={"read_only_root_filesystem": True}, + ), + "sidecar": make_container( + name="sidecar", + security_context={"read_only_root_filesystem": False}, + ), + } + ) + + result = run_check(MODULE, CLASS, make_core_client({pod.uid: pod})) + + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Pod test-pod container sidecar does not have readOnlyRootFilesystem set to true." + ) + + def test_init_container_missing_readonly_root_filesystem_fails(self): + pod = make_pod( + containers={ + "app": make_container( + security_context={"read_only_root_filesystem": True} + ) + }, + init_containers={ + "init": make_container(name="init", security_context=None) + }, + ) + + result = run_check(MODULE, CLASS, make_core_client({pod.uid: pod})) + + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Pod test-pod container init does not have readOnlyRootFilesystem set to true." + ) + + def test_ephemeral_container_missing_readonly_root_filesystem_fails(self): + pod = make_pod( + containers={ + "app": make_container( + security_context={"read_only_root_filesystem": True} + ) + }, + ephemeral_containers={ + "debug": make_container(name="debug", security_context={}) + }, + ) + + result = run_check(MODULE, CLASS, make_core_client({pod.uid: pod})) + + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Pod test-pod container debug does not have readOnlyRootFilesystem set to true." + ) diff --git a/tests/providers/kubernetes/services/core/core_service_test.py b/tests/providers/kubernetes/services/core/core_service_test.py new file mode 100644 index 0000000000..5fef91159c --- /dev/null +++ b/tests/providers/kubernetes/services/core/core_service_test.py @@ -0,0 +1,64 @@ +from kubernetes import client +from prowler.providers.kubernetes.services.core.core_service import Core, Pod + + +def test_pod_model_preserves_core_service_container_groups(): + containers = Core._build_containers( + [ + client.V1Container( + name="app", + image="nginx:1.25", + security_context=client.V1SecurityContext( + read_only_root_filesystem=True + ), + ) + ] + ) + init_containers = Core._build_containers( + [ + client.V1Container( + name="init", + image="busybox:1.36", + security_context=client.V1SecurityContext( + read_only_root_filesystem=True + ), + ) + ] + ) + ephemeral_containers = Core._build_containers( + [ + client.V1EphemeralContainer( + name="debug", + image="busybox:1.36", + security_context=client.V1SecurityContext( + read_only_root_filesystem=True + ), + ) + ] + ) + + pod = Pod( + name="test-pod", + uid="test-pod-uid", + namespace="default", + labels=None, + annotations=None, + node_name=None, + service_account=None, + status_phase="Running", + pod_ip="10.0.0.1", + host_ip="192.168.1.1", + host_pid=False, + host_ipc=False, + host_network=False, + security_context={}, + containers=containers, + init_containers=init_containers, + ephemeral_containers=ephemeral_containers, + ) + + assert list(pod.containers) == ["app"] + assert list(pod.init_containers) == ["init"] + assert list(pod.ephemeral_containers) == ["debug"] + assert "init" not in pod.containers + assert "debug" not in pod.containers