feat(ecs): Ensure ECS containers have read-only access to root filesystems (#5168)

Co-authored-by: Sergio <sergio@prowler.com>
This commit is contained in:
Mario Rodriguez Lopez
2024-09-26 20:37:24 +02:00
committed by GitHub
parent 0474c7995c
commit ff05ce4da1
7 changed files with 248 additions and 29 deletions
@@ -47,7 +47,7 @@ class ECS(AWSService):
)
def _describe_task_definition(self, task_definition):
logger.info("ECS - Describing Task Definitions...")
logger.info("ECS - Describing Task Definition...")
try:
client = self.regional_clients[task_definition.region]
response = client.describe_task_definition(
@@ -70,6 +70,9 @@ class ECS(AWSService):
ContainerDefinition(
name=container["name"],
privileged=container.get("privileged", False),
readonly_rootfilesystem=container.get(
"readonlyRootFilesystem", False
),
user=container.get("user", ""),
environment=environment,
)
@@ -164,6 +167,7 @@ class ContainerEnvVariable(BaseModel):
class ContainerDefinition(BaseModel):
name: str
privileged: bool
readonly_rootfilesystem: bool = False
user: str
environment: list[ContainerEnvVariable]
@@ -0,0 +1,34 @@
{
"Provider": "aws",
"CheckID": "ecs_task_definitions_containers_readonly_access",
"CheckTitle": "ECS containers should be limited to read-only access to root filesystems",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "ecs",
"SubServiceName": "taskDefinition",
"ResourceIdTemplate": "arn:aws:ecs:{region}:{account-id}:task-definition/{task-definition-name}",
"Severity": "high",
"ResourceType": "AwsEcsTaskDefinition",
"Description": "This control checks if Amazon ECS containers are limited to read-only access to mounted root filesystems. The control fails if the readonlyRootFilesystem parameter is set to false or if the parameter doesn't exist in the container definition.",
"Risk": "If ECS containers have write access to root filesystems, it increases the risk of filesystem tampering and exploitation of vulnerabilities, violating the principle of least privilege.",
"RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/ecs-containers-readonly-access.html",
"Remediation": {
"Code": {
"CLI": "aws ecs register-task-definition --family <task-family> --container-definitions '[{\"name\":\"<container-name>\",\"image\":\"<image>\",\"readonlyRootFilesystem\":true}]'",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ecs-controls.html#ecs-5",
"Terraform": ""
},
"Recommendation": {
"Text": "Ensure that containers have read-only access to their root filesystems to limit write access and reduce the attack surface.",
"Url": "https://docs.aws.amazon.com/AmazonECS/latest/userguide/task_definition_parameters.html#container_definition_readonly"
}
},
"Categories": [
"vulnerabilities"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,27 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.ecs.ecs_client import ecs_client
class ecs_task_definitions_containers_readonly_access(Check):
def execute(self):
findings = []
for task_definition in ecs_client.task_definitions.values():
report = Check_Report_AWS(self.metadata())
report.region = task_definition.region
report.resource_id = f"{task_definition.name}:{task_definition.revision}"
report.resource_arn = task_definition.arn
report.resource_tags = task_definition.tags
report.status = "PASS"
report.status_extended = f"ECS task definition {task_definition.name} does not have containers with write access to the root filesystems."
failed_containers = []
for container in task_definition.container_definitions:
if not container.readonly_rootfilesystem:
report.status = "FAIL"
failed_containers.append(container.name)
if failed_containers:
report.status_extended = f"ECS task definition {task_definition.name} has containers with write access to the root filesystem: {', '.join(failed_containers)}"
findings.append(report)
return findings
@@ -157,6 +157,11 @@ class Test_ECS_Service:
assert ecs.task_definitions[task_arn].network_mode == "host"
assert not ecs.task_definitions[task_arn].container_definitions[0].privileged
assert ecs.task_definitions[task_arn].container_definitions[0].user == ""
assert (
not ecs.task_definitions[task_arn]
.container_definitions[0]
.readonly_rootfilesystem
)
# Test list ECS clusters
@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
@@ -0,0 +1,149 @@
from unittest import mock
from prowler.providers.aws.services.ecs.ecs_service import (
ContainerDefinition,
TaskDefinition,
)
from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1
TASK_NAME = "test-task-readonly"
TASK_REVISION = "1"
CONTAINER_NAME = "test-container"
TASK_ARN = f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{TASK_NAME}:{TASK_REVISION}"
class Test_ecs_task_definitions_containers_readonly_access:
def test_no_task_definitions(self):
ecs_client = mock.MagicMock
ecs_client.task_definitions = {}
with mock.patch(
"prowler.providers.aws.services.ecs.ecs_service.ECS",
ecs_client,
):
from prowler.providers.aws.services.ecs.ecs_task_definitions_containers_readonly_access.ecs_task_definitions_containers_readonly_access import (
ecs_task_definitions_containers_readonly_access,
)
check = ecs_task_definitions_containers_readonly_access()
result = check.execute()
assert len(result) == 0
def test_task_definition_all_containers_readonly(self):
ecs_client = mock.MagicMock
ecs_client.task_definitions = {}
ecs_client.task_definitions[TASK_ARN] = TaskDefinition(
name=TASK_NAME,
arn=TASK_ARN,
revision=TASK_REVISION,
region=AWS_REGION_US_EAST_1,
network_mode="bridge",
container_definitions=[
ContainerDefinition(
name=CONTAINER_NAME,
readonly_rootfilesystem=True,
privileged=False,
user="appuser",
environment=[],
)
],
)
with mock.patch(
"prowler.providers.aws.services.ecs.ecs_service.ECS",
ecs_client,
):
from prowler.providers.aws.services.ecs.ecs_task_definitions_containers_readonly_access.ecs_task_definitions_containers_readonly_access import (
ecs_task_definitions_containers_readonly_access,
)
check = ecs_task_definitions_containers_readonly_access()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"ECS task definition {TASK_NAME} does not have containers with write access to the root filesystems."
)
def test_task_definition_some_containers_not_readonly(self):
ecs_client = mock.MagicMock
ecs_client.task_definitions = {}
ecs_client.task_definitions[TASK_ARN] = TaskDefinition(
name=TASK_NAME,
arn=TASK_ARN,
revision=TASK_REVISION,
region=AWS_REGION_US_EAST_1,
network_mode="bridge",
container_definitions=[
ContainerDefinition(
name=CONTAINER_NAME,
readonly_rootfilesystem=False,
privileged=False,
user="appuser",
environment=[],
)
],
)
with mock.patch(
"prowler.providers.aws.services.ecs.ecs_service.ECS",
ecs_client,
):
from prowler.providers.aws.services.ecs.ecs_task_definitions_containers_readonly_access.ecs_task_definitions_containers_readonly_access import (
ecs_task_definitions_containers_readonly_access,
)
check = ecs_task_definitions_containers_readonly_access()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"ECS task definition {TASK_NAME} has containers with write access to the root filesystem: {CONTAINER_NAME}"
)
def test_task_definition_mixed_containers(self):
ecs_client = mock.MagicMock
ecs_client.task_definitions = {
TASK_ARN: TaskDefinition(
name=TASK_NAME,
arn=TASK_ARN,
revision=TASK_REVISION,
region=AWS_REGION_US_EAST_1,
network_mode="bridge",
container_definitions=[
ContainerDefinition(
name=CONTAINER_NAME,
readonly_rootfilesystem=False,
privileged=False,
user="appuser",
environment=[],
),
ContainerDefinition(
name="readonly-container",
readonly_rootfilesystem=True,
privileged=False,
user="appuser",
environment=[],
),
],
)
}
with mock.patch(
"prowler.providers.aws.services.ecs.ecs_service.ECS",
ecs_client,
):
from prowler.providers.aws.services.ecs.ecs_task_definitions_containers_readonly_access.ecs_task_definitions_containers_readonly_access import (
ecs_task_definitions_containers_readonly_access,
)
check = ecs_task_definitions_containers_readonly_access()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"ECS task definition {TASK_NAME} has containers with write access to the root filesystem: {CONTAINER_NAME}"
)
@@ -7,10 +7,10 @@ from prowler.providers.aws.services.ecs.ecs_service import (
)
from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1
task_name = "test-task"
task_revision = "1"
container_name = "test-container"
task_arn = f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{task_name}:{task_revision}"
TASK_NAME = "test-task-hostmode"
TASK_REVISION = "1"
CONTAINER_NAME = "test-container"
TASK_ARN = f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{TASK_NAME}:{TASK_REVISION}"
class Test_ecs_task_definitions_host_networking_mode_users:
@@ -33,15 +33,15 @@ class Test_ecs_task_definitions_host_networking_mode_users:
def test_task_definition_no_host_network_mode(self):
ecs_client = mock.MagicMock
ecs_client.task_definitions = {}
ecs_client.task_definitions[task_arn] = TaskDefinition(
name=task_name,
arn=task_arn,
revision=task_revision,
ecs_client.task_definitions[TASK_ARN] = TaskDefinition(
name=TASK_NAME,
arn=TASK_ARN,
revision=TASK_REVISION,
region=AWS_REGION_US_EAST_1,
network_mode="bridge",
container_definitions=[
ContainerDefinition(
name=container_name,
name=CONTAINER_NAME,
privileged=False,
user="",
environment=[
@@ -68,21 +68,21 @@ class Test_ecs_task_definitions_host_networking_mode_users:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"ECS task definition {task_name} does not have host network mode."
== f"ECS task definition {TASK_NAME} does not have host network mode."
)
def test_task_definition_host_mode_container_root_non_privileged(self):
ecs_client = mock.MagicMock
ecs_client.task_definitions = {}
ecs_client.task_definitions[task_arn] = TaskDefinition(
name=task_name,
arn=task_arn,
revision=task_revision,
ecs_client.task_definitions[TASK_ARN] = TaskDefinition(
name=TASK_NAME,
arn=TASK_ARN,
revision=TASK_REVISION,
region=AWS_REGION_US_EAST_1,
network_mode="host",
container_definitions=[
ContainerDefinition(
name=container_name,
name=CONTAINER_NAME,
privileged=False,
user="root",
environment=[],
@@ -104,21 +104,21 @@ class Test_ecs_task_definitions_host_networking_mode_users:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"ECS task definition {task_name} has containers with host network mode and non-privileged containers running as root or with no user specified: {container_name}"
== f"ECS task definition {TASK_NAME} has containers with host network mode and non-privileged containers running as root or with no user specified: {CONTAINER_NAME}"
)
def test_task_definition_host_mode_container_privileged(self):
ecs_client = mock.MagicMock
ecs_client.task_definitions = {}
ecs_client.task_definitions[task_arn] = TaskDefinition(
name=task_name,
arn=f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{task_name}:{task_revision}",
revision=task_revision,
ecs_client.task_definitions[TASK_ARN] = TaskDefinition(
name=TASK_NAME,
arn=f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{TASK_NAME}:{TASK_REVISION}",
revision=TASK_REVISION,
region=AWS_REGION_US_EAST_1,
network_mode="host",
container_definitions=[
ContainerDefinition(
name=container_name,
name=CONTAINER_NAME,
privileged=True,
user="root",
environment=[],
@@ -140,21 +140,21 @@ class Test_ecs_task_definitions_host_networking_mode_users:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"ECS task definition {task_name} has host network mode but no containers running as root or with no user specified."
== f"ECS task definition {TASK_NAME} has host network mode but no containers running as root or with no user specified."
)
def test_task_definition_host_mode_container_not_root(self):
ecs_client = mock.MagicMock
ecs_client.task_definitions = {}
ecs_client.task_definitions[task_arn] = TaskDefinition(
name=task_name,
arn=task_arn,
revision=task_revision,
ecs_client.task_definitions[TASK_ARN] = TaskDefinition(
name=TASK_NAME,
arn=TASK_ARN,
revision=TASK_REVISION,
region=AWS_REGION_US_EAST_1,
network_mode="host",
container_definitions=[
ContainerDefinition(
name=container_name,
name=CONTAINER_NAME,
privileged=False,
user="appuser",
environment=[],
@@ -176,5 +176,5 @@ class Test_ecs_task_definitions_host_networking_mode_users:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"ECS task definition {task_name} has host network mode but no containers running as root or with no user specified."
== f"ECS task definition {TASK_NAME} has host network mode but no containers running as root or with no user specified."
)