Deny root user for Amazon ECS and AWS Fargate tasks

Prevent container tasks from running as root on Amazon ECS and AWS Fargate

Nathan Peck
Nathan Peck
Senior Developer Advocate at AWS

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

CloudFormation Guard Rule

File: no-root-for-tasks.guard Language: guard
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 authoritative way to guarantee this is to explicitly set the user that the container 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:

File: safe-task-defs.yml Language: yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
File: bad-task-defs.yml Language: yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
AWSTemplateFormatVersion: '2010-09-09'
Description: Example task definitions that run as root

Resources:
  # If not specified then the user is likely root, or it depends on the
  # contents of the Dockerfile and what USER it defines
  AmbiguousUser:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: alpine
      Cpu: 256
      Memory: 128
      ContainerDefinitions:
        - Name: alpine
          Image: public.ecr.aws/docker/library/alpine:latest
          Essential: true

  # Runs as root via running as the user 0
  RootViaUid:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: alpine
      Cpu: 256
      Memory: 128
      ContainerDefinitions:
        - Name: alpine
          Image: public.ecr.aws/docker/library/alpine:latest
          Essential: true
          User: '0'

  # Runs as root explicitly
  RootViaUserName:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: alpine
      Cpu: 256
      Memory: 128
      ContainerDefinitions:
        - Name: alpine
          Image: public.ecr.aws/docker/library/alpine:latest
          Essential: true
          User: 'root'

  # Runs as root via the group name
  RootViaGroup:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: alpine
      Cpu: 256
      Memory: 128
      ContainerDefinitions:
        - Name: alpine
          Image: public.ecr.aws/docker/library/alpine:latest
          Essential: true
          User: 'bin:root'

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: