Deny root user for Amazon ECS and AWS Fargate tasks
What and why?
Amazon Elastic Container Service (ECS) is a container orchestrator that launches and manages container deployments on your behalf. It launches applications as containerized processes. One aspect of a containerized process that you can control is the user that the process runs as.
By default, unless otherwise specified, Docker containers typically run as root
. However, the root
user is a special user with elevated access to the underlying host. Therefore, running a container as root
greatly increases the risk that a remote code execution vulnerability can be exploited to access the underlying host or other containers on the host.
In this pattern you will learn how to force Amazon ECS tasks to run containers as non root, by applying CloudFormation Guard, an open-source, general-purpose, policy-as-code evaluation tool.
TIP
On a standard Linux system only the root
user is allowed to bind to ports below 100. This includes port 80, port 22, and other well known ports. When running as non root
you must configure your application to bind to a higher number port, such as port 8080 instead of port 80.
Dependencies
This pattern uses CloudFormation Guard, which can be installed with the following command:
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/aws-cloudformation/cloudformation-guard/main/install-guard.sh | sh
export PATH=~/.guard/bin:$PATH
cfn-guard --version
You can also see the install instructions for other systems.
CloudFormation Guard Rule
let task_defs = Resources.*[Type == 'AWS::ECS::TaskDefinition']
#
# Verify that ECS tasks are not running as root
#
rule tasks_denied_root {
when %task_defs !empty {
%task_defs.Properties.ContainerDefinitions[*] {
User exists <<Container in the ECS task definition must specify the `User` property>>
User != /root/ <<Container in the ECS task definition denied `User` that includes 'root'>>
}
}
}
WARNING
Note that this rule treats the lack of a User
field as if the user is root
. This is because there is no way to tell if the Dockerfile
for the container specified a downscoped user or not. It is entirely possible that the container is assuming another user at runtime. However, the only authoratative way to guarantee this is to explictly set the user that the conatiner will run as, using the Amazon ECS task definition setting.
Sample Templates
The following sample CloudFormation templates can be used to verify that this rule works:
- Good ECS tasks
- Bad ECS tasks
AWSTemplateFormatVersion: '2010-09-09'
Description: Example task definitions that use a safe user
Resources:
SafeUid:
Type: AWS::ECS::TaskDefinition
Properties:
Family: alpine
Cpu: 256
Memory: 128
ContainerDefinitions:
- Name: alpine
Image: public.ecr.aws/docker/library/alpine:latest
User: '1010'
Essential: true
SafeUidAndGuid:
Type: AWS::ECS::TaskDefinition
Properties:
Family: alpine
Cpu: 256
Memory: 128
ContainerDefinitions:
- Name: alpine
Image: public.ecr.aws/docker/library/alpine:latest
User: 1010:1010
Essential: true
SafeUser:
Type: AWS::ECS::TaskDefinition
Properties:
Family: alpine
Cpu: 256
Memory: 128
ContainerDefinitions:
- Name: alpine
Image: public.ecr.aws/docker/library/alpine:latest
User: 'node'
Essential: true
SafeUserAndGroup:
Type: AWS::ECS::TaskDefinition
Properties:
Family: alpine
Cpu: 256
Memory: 128
ContainerDefinitions:
- Name: alpine
Image: public.ecr.aws/docker/library/alpine:latest
User: 'node:node'
Essential: true
Usage
You can validate the sample CloudFormation templates against the CloudFormation guard rule using the following command:
cfn-guard validate --data *.yml --rules .
You should see output similar to this:
bad-task-defs.yml Status = FAIL
FAILED rules
no-root-for-tasks.guard/tasks_denied_root FAIL
---
Evaluating data bad-task-defs.yml against rules no-root-for-tasks.guard
Number of non-compliant resources 3
Resource = AmbiguousUser {
Type = AWS::ECS::TaskDefinition
Rule = tasks_denied_root {
ALL {
Check = User EXISTS {
Message = Container in the ECS task definition must specify the `User` property
RequiredPropertyError {
PropertyPath = /Resources/AmbiguousUser/Properties/ContainerDefinitions/0[L:13,C:10]
MissingProperty = User
Reason = Could not find key User inside struct at path /Resources/AmbiguousUser/Properties/ContainerDefinitions/0[L:13,C:10]
Code:
11. Cpu: 256
12. Memory: 128
13. ContainerDefinitions:
14. - Name: alpine
15. Image: public.ecr.aws/docker/library/alpine:latest
16. Essential: true
}
}
Check = User not EQUALS "/root/" {
Message = Container in the ECS task definition denied `User` that includes 'root'
RequiredPropertyError {
PropertyPath = /Resources/AmbiguousUser/Properties/ContainerDefinitions/0[L:13,C:10]
MissingProperty = User
Reason = Could not find key User inside struct at path /Resources/AmbiguousUser/Properties/ContainerDefinitions/0[L:13,C:10]
Code:
11. Cpu: 256
12. Memory: 128
13. ContainerDefinitions:
14. - Name: alpine
15. Image: public.ecr.aws/docker/library/alpine:latest
16. Essential: true
}
}
}
}
}
Resource = RootViaUserName {
Type = AWS::ECS::TaskDefinition
Rule = tasks_denied_root {
ALL {
Check = User not EQUALS "/root/" {
ComparisonError {
Message = Container in the ECS task definition denied `User` that includes 'root'
Error = Check was not compliant as property value [Path=/Resources/RootViaUserName/Properties/ContainerDefinitions/0/User[L:41,C:16] Value="root"] equal to value [Path=[L:0,C:0] Value="/root/"].
PropertyPath = /Resources/RootViaUserName/Properties/ContainerDefinitions/0/User[L:41,C:16]
Operator = NOT EQUAL
Value = "root"
ComparedWith = "/root/"
Code:
39. - Name: alpine
40. Image: public.ecr.aws/docker/library/alpine:latest
41. Essential: true
42. User: 'root'
43.
44. # Runs as root via the group name
}
}
}
}
}
Resource = RootViaGroup {
Type = AWS::ECS::TaskDefinition
Rule = tasks_denied_root {
ALL {
Check = User not EQUALS "/root/" {
ComparisonError {
Message = Container in the ECS task definition denied `User` that includes 'root'
Error = Check was not compliant as property value [Path=/Resources/RootViaGroup/Properties/ContainerDefinitions/0/User[L:54,C:16] Value="bin:root"] equal to value [Path=[L:0,C:0] Value="/root/"].
PropertyPath = /Resources/RootViaGroup/Properties/ContainerDefinitions/0/User[L:54,C:16]
Operator = NOT EQUAL
Value = "bin:root"
ComparedWith = "/root/"
Code:
52. - Name: alpine
53. Image: public.ecr.aws/docker/library/alpine:latest
54. Essential: true
55. User: 'bin:root'
}
}
}
}
}
See Also
More policy as code patterns:
- Enforce non-blocking mode for awslogs logging driver, with CloudFormation Guard policy as code
- Enforce readonly root filesystem for containers in ECS, with CloudFormation Guard policy as code
- Deny privileged container mode in Amazon ECS with CloudFormation Guard policy as code
- Deny Linux kernel capabilities for Amazon ECS and AWS Fargate tasks