Deploying Prowler Security tool to AWS ECS Fargate
The AWS Console is really handy to get something up and running in the prototyping stage of a project. But often times the temptation is there to just take the 'ok, thats working now, phew!' approach and move on. Inevitably, 6 months or more down the track you've got to either recreate a similar resource or redeploy the same resource again, and despite being sure you'd remember all the little aspects, you've got to go on the voyage of rediscovery all over again to re-identify all the little settings you need.
Because I have the memory of a goldfish, I prefer to take a 'Infrastructure as Code' approach, even for personal side projects. So, here's a walk through how I wrote up a Cloudformation template to deploy the Prowler open source security scanner as a Docker container in AWS Elastic Container Service (ECS) using Fargate.
The end goal is for a scheduled ECS task that will periodically scan my AWS account using the Prowler security tool that will write its report output to an S3 bucket and send new findings to AWS Security Hub to be actioned.
To begin with, we'll set up a couple of Parameters that will be used to provide inputs for the resources that will be created so names are created consistently, as well as some parameters for configuration settings that we might want to be able to easily change on redeployment of the stack if necessary.
AWSTemplateFormatVersion: '2010-09-09'
Description: A stack for deploying the containerized prowler security scanner in AWS ECS Fargate.
Parameters:
ServiceName:
Type: String
Default: prowler-scanner
Description: A name for the service
ECRRepository:
Type: String
Default: prowler-repository
Description: The name of the ECR repository where the docker image is stored
Note that this repository needs to already exist, with an image tagged appropriately
already pushed to this repository.
ECRTag:
Type: String
Default: latest-prowler
Description: The tag of the container image to use
ContainerCpu:
Type: Number
Default: 1024
Description: How much CPU to give the container. 1024 is 1 CPU
ContainerMemory:
Type: Number
Default: 4096
Description: How much memory in megabytes to give the container
DesiredCount:
Type: Number
Default: 1
Description: How many copies of the service task to run
S3BucketName:
Type: String
Default: 'log-archive'
Description: The S3 bucket name for the scan output files.
This bucket needs to already exist - it is not created in this template.
The first resource to create is a log group that the ECS tasks can log to. Rather simple & straight forward. Be sure to add a retention period that works for you - too long and you'll be keeping logs (and paying for them) longer than necessary.
Resources:
# ECS log group
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '/ecs/${ServiceName}'
RetentionInDays: 14
Next is to create the ECS cluster. Again, not particularly complex. One thing is to enable Container Insights. This is one of the checks done by AWS Security Hub, so enable it to get a pass.
# ECS Resources
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub '${ServiceName}-cluster'
ClusterSettings:
- Name: containerInsights
Value: enabled
Next, a security group is defined that will be used by the ECS task. Only HTTPS outgoing is required.
ECSContainerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub '${ServiceName}-sg'
GroupDescription: HTTPS outbound to call AWS API
VpcId: !ImportValue 'vpc-id'
SecurityGroupEgress:
- IpProtocol: TCP
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Note some of these properties import values (!ImportValue) from Cloudformation Exports. These have been created previously by the VPC stack deployed as part of Building out a simple AWS VPC. Where you see any !ImportValue calls, either change these to either new parameters added to the template, or substitute as appropriate for your own VPC if necessary.
Next is an IAM Role that allows the ECS task agent to access necessary AWS resources to be able to fetch the ECR image and write Cloudwatch logs. These are seperate permissions to the permissions required by the task itself.
ECSExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${ServiceName}-execution-role'
Description: Role for the ECS agent task manager execution
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ecs-tasks.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: ECSTaskManagerEcrTokenAccessPolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- 'ecr:GetAuthorizationToken'
Resource: "*"
- PolicyName: ECSTaskManagerEcrAccessPolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- 'ecr:GetAuthorizationToken'
- 'ecr:BatchCheckLayerAvailability'
- 'ecr:GetDownloadUrlForLayer'
- 'ecr:BatchGetImage'
Resource: !Sub 'arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${ECRRepository}'
- PolicyName: ECSTaskManagerLogPolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/ecs/${ServiceName}:log-stream:*'
Then the IAM role used by the running task itself is defined. This role has a large number of permissions to be able to read many AWS resources that are not covered by the SecurityAudit AWS Managed Policy. Additionally, the role has S3 write access as well as permission to write findings to Security Hub.
ECSTaskRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${ServiceName}-task-role'
Description: Role for the ECS task
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: [ecs-tasks.amazonaws.com]
Action: ['sts:AssumeRole']
Path: "/"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/SecurityAudit
- arn:aws:iam::aws:policy/job-function/ViewOnlyAccess
Policies:
- PolicyName: s3Access
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- s3:PutObject
Resource: [
!Join [ "", [ "arn:aws:s3:::", !Ref S3BucketName, "/*" ] ]
]
- PolicyName: securityHubAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- securityhub:BatchImportFindings
- securityhub:GetFindings
Resource: "*"
- PolicyName: apiGatewayReadAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- apigateway:GET
Resource: [
!Sub 'arn:aws:apigateway:${AWS::Region}::/restapis/*',
!Sub 'arn:aws:apigateway:${AWS::Region}::/apis/*'
]
- PolicyName: extendedProwlerReadAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- account:Get*
- appstream:Describe*
- appstream:List*
- backup:List*
- cloudtrail:GetInsightSelectors
- codeartifact:List*
- codebuild:BatchGet*
- dlm:Get*
- drs:Describe*
- ds:Get*
- ds:Describe*
- ds:List*
- ec2:GetEbsEncryptionByDefault
- ecr:Describe*
- ecr:GetRegistryScanningConfiguration
- elasticfilesystem:DescribeBackupPolicy
- glue:GetConnections
- glue:GetSecurityConfiguration*
- glue:SearchTables
- lambda:GetFunction*
- logs:FilterLogEvents
- macie2:GetMacieSession
- s3:GetAccountPublicAccessBlock
- shield:DescribeProtection
- shield:GetSubscriptionState
- securityhub:BatchImportFindings
- securityhub:GetFindings
- ssm:GetDocument
- ssm-incidents:List*
- support:Describe*
- tag:GetTagKeys
- wellarchitected:List*
Resource: "*"
Next, add the AWSECSTaskDefinition, referencing the resources created above. Of note is the requirement that the root filesystem be set to false so that output can be written locally prior to upload to S3 and Security Hub. A potential modification here would be to add a seperate ephemeral volume that could be attached to /output which would allow the remainder of the root filesystem to be readonly.
ECSTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Ref 'ServiceName'
Cpu: !Ref 'ContainerCpu'
Memory: !Ref 'ContainerMemory'
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ExecutionRoleArn: !Ref ECSExecutionRole
TaskRoleArn: !Ref ECSTaskRole
ContainerDefinitions:
- Name: !Ref 'ServiceName'
Cpu: !Ref 'ContainerCpu'
Memory: !Ref 'ContainerMemory'
ReadonlyRootFilesystem: false # needs to be false to allow writing to /prowler/output
Image:
!Join [
'.',
[
!Ref AWS::AccountId,
'dkr.ecr',
!Ref AWS::Region,
!Sub 'amazonaws.com/${ECRRepository}:${ECRTag}'
]
]
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref 'AWS::Region'
awslogs-stream-prefix: ecs
Command:
- aws
- -f
- ap-southeast-2
- -M
- html
- -B
- !Ref S3BucketName
- --security-hub
- --quiet
Note the Command field which passes command line parameters to the prowler application. These are documented in the Prowler documentation, but this combination is running against AWS for the ap-southeast-2 region outputting a html format report to an S3 bucket as well as publishing the findings to Security Hub. Only failed findings are being reported and published.
The last thing thats needed is a scheduled run of the task. Add the following to the template to create a scheduled run of the task to run weekly. Also note that the task is being run in one of three private subnets. As the task runs only for a relatively short period, running in a public subnet would probably be suitable. For the task to be able to run in a private subnet, a NAT Gateway needs to already exist in a public subnet (which is optionally included in Building out a simple AWS VPC.
ECSTaskScheduler:
Type: AWS::Events::Rule
Properties:
Description: "A rule to schedule the prowler scanner"
Name: !Sub '${ServiceName}-scheduler'
ScheduleExpression: "rate(7 days)"
State: ENABLED
Targets:
- Arn: !GetAtt ECSCluster.Arn
RoleArn: !GetAtt ECSTaskRole.Arn
Id: TaskScheduler
EcsParameters:
TaskDefinitionArn: !Ref ECSTaskDefinition
TaskCount: 1
LaunchType: FARGATE
PlatformVersion: 'LATEST'
NetworkConfiguration:
AwsVpcConfiguration:
AssignPublicIp: DISABLED
SecurityGroups:
- !Ref ECSContainerSecurityGroup
Subnets:
- !ImportValue subnet-private-a
- !ImportValue subnet-private-b
- !ImportValue subnet-private-c
The entire template looks like the below
AWSTemplateFormatVersion: '2010-09-09'
Description: A stack for deploying the containerized prowler security scanner in AWS Fargate.
Parameters:
ServiceName:
Type: String
Default: prowler-scanner
Description: A name for the service
ECRRepository:
Type: String
Default: prowler-repository
Description: The name of the ECR repository where the docker image is stored
Note that this repository needs to already exist, with an image tagged appropriately
already pushed to this repository.
ECRTag:
Type: String
Default: latest-prowler
Description: The tag of the container image to use
ContainerCpu:
Type: Number
Default: 1024
Description: How much CPU to give the container. 1024 is 1 CPU
ContainerMemory:
Type: Number
Default: 4096
Description: How much memory in megabytes to give the container
DesiredCount:
Type: Number
Default: 1
Description: How many copies of the service task to run
S3BucketName:
Type: String
Default: 'log-archive'
Description: The S3 bucket name for the scan output files.
This bucket needs to already exist - it is not created in this template.
Resources:
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '/ecs/${ServiceName}'
RetentionInDays: 14
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub '${ServiceName}-cluster'
ClusterSettings:
- Name: containerInsights
Value: enabled
ECSContainerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub '${ServiceName}-sg'
GroupDescription: HTTPS outbound to call AWS API
VpcId: !ImportValue 'vpc-id'
SecurityGroupEgress:
- IpProtocol: TCP
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
ECSExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${ServiceName}-execution-role'
Description: Role for the ECS agent task manager execution
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ecs-tasks.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: ECSTaskManagerEcrTokenAccessPolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- 'ecr:GetAuthorizationToken'
Resource: "*"
- PolicyName: ECSTaskManagerEcrAccessPolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- 'ecr:GetAuthorizationToken'
- 'ecr:BatchCheckLayerAvailability'
- 'ecr:GetDownloadUrlForLayer'
- 'ecr:BatchGetImage'
Resource: !Sub 'arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${ECRRepository}'
- PolicyName: ECSTaskManagerLogPolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/ecs/${ServiceName}:log-stream:*'
ECSTaskRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${ServiceName}-task-role'
Description: Role for the ECS task
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: [ecs-tasks.amazonaws.com]
Action: ['sts:AssumeRole']
Path: "/"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/SecurityAudit
- arn:aws:iam::aws:policy/job-function/ViewOnlyAccess
Policies:
- PolicyName: s3Access
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- s3:PutObject
Resource: [
!Join [ "", [ "arn:aws:s3:::", !Ref S3BucketName, "/*" ] ]
]
- PolicyName: securityHubAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- securityhub:BatchImportFindings
- securityhub:GetFindings
Resource: "*"
- PolicyName: apiGatewayReadAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- apigateway:GET
Resource: [
!Sub 'arn:aws:apigateway:${AWS::Region}::/restapis/*',
!Sub 'arn:aws:apigateway:${AWS::Region}::/apis/*'
]
- PolicyName: extendedProwlerReadAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- account:Get*
- appstream:Describe*
- appstream:List*
- backup:List*
- cloudtrail:GetInsightSelectors
- codeartifact:List*
- codebuild:BatchGet*
- dlm:Get*
- drs:Describe*
- ds:Get*
- ds:Describe*
- ds:List*
- ec2:GetEbsEncryptionByDefault
- ecr:Describe*
- ecr:GetRegistryScanningConfiguration
- elasticfilesystem:DescribeBackupPolicy
- glue:GetConnections
- glue:GetSecurityConfiguration*
- glue:SearchTables
- lambda:GetFunction*
- logs:FilterLogEvents
- macie2:GetMacieSession
- s3:GetAccountPublicAccessBlock
- shield:DescribeProtection
- shield:GetSubscriptionState
- securityhub:BatchImportFindings
- securityhub:GetFindings
- ssm:GetDocument
- ssm-incidents:List*
- support:Describe*
- tag:GetTagKeys
- wellarchitected:List*
Resource: "*"
ECSTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Ref 'ServiceName'
Cpu: !Ref 'ContainerCpu'
Memory: !Ref 'ContainerMemory'
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ExecutionRoleArn: !Ref ECSExecutionRole
TaskRoleArn: !Ref ECSTaskRole
ContainerDefinitions:
- Name: !Ref 'ServiceName'
Cpu: !Ref 'ContainerCpu'
Memory: !Ref 'ContainerMemory'
ReadonlyRootFilesystem: false # needs to be false to allow writing to /prowler/output
Image:
!Join [
'.',
[
!Ref AWS::AccountId,
'dkr.ecr',
!Ref AWS::Region,
!Sub 'amazonaws.com/${ECRRepository}:${ECRTag}'
]
]
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref 'AWS::Region'
awslogs-stream-prefix: ecs
Command:
- aws
- -f
- ap-southeast-2
- -M
- html
- -B
- !Ref S3BucketName
- --security-hub
- --quiet
ECSTaskScheduler:
Type: AWS::Events::Rule
Properties:
Description: "A rule to schedule the prowler scanner"
Name: !Sub '${ServiceName}-scheduler'
ScheduleExpression: "rate(7 days)"
State: ENABLED
Targets:
- Arn: !GetAtt ECSCluster.Arn
RoleArn: !GetAtt ECSTaskRole.Arn
Id: TaskScheduler
EcsParameters:
TaskDefinitionArn: !Ref ECSTaskDefinition
TaskCount: 1
LaunchType: FARGATE
PlatformVersion: 'LATEST'
NetworkConfiguration:
AwsVpcConfiguration:
AssignPublicIp: DISABLED
SecurityGroups:
- !Ref ECSContainerSecurityGroup
Subnets:
- !ImportValue subnet-private-a
- !ImportValue subnet-private-b
- !ImportValue subnet-private-c