feat(kubernetes): add core check for readonly root filesystem enabled (#11835)

Co-authored-by: wbro <80239840234@proton.me>
Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
This commit is contained in:
weedle02
2026-07-14 07:23:17 -04:00
committed by GitHub
parent 9c15796c84
commit 22401a54a0
7 changed files with 268 additions and 1 deletions
@@ -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
@@ -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/<workload-name> -n <namespace> -p '{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"<container-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 = \"<example_resource_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."
}
@@ -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
@@ -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,
@@ -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."
)
@@ -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