Deny root user for Amazon ECS and AWS Fargate tasks

Nathan Peck profile picture
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

This pattern uses CloudFormation Guard, which can be installed with the following command:

Language: sh
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

File: no-root-for-tasks.guardLanguage: guard

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
File: safe-task-defs.ymlLanguage: yml
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:

Language: sh
cfn-guard validate --data *.yml --rules .

You should see output similar to this:

Language: txt
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: