mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
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:
@@ -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
|
||||
+37
@@ -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."
|
||||
}
|
||||
+38
@@ -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,
|
||||
|
||||
+126
@@ -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
|
||||
Reference in New Issue
Block a user