While we were setting up bastion on ECS fargate at work, I decided to look into AWS ECS Execute Command. This feature seems relatively new to the scene, so I decided to write down what steps I took to get this working.
We are setting up the ECS services to support ECS Execute command and Cloudwatch logging.
Requirements
- AWS CLI
- Session Manager plugin
VPC Endpoint
In order for us to access the fargate containers, we will need to setup a VPC Endpoint
for ssmmessages
.
Notice! The VPC endpoint inbound rule needs https port 443 open in order for the fargate tasks to communicate.
resource "aws_vpc_endpoint" "this" {
private_dns_enabled = true
security_group_ids = [
"sg-*****",
]
service_name = "com.amazonaws.us-west-2.ssmmessages"
subnet_ids = [
"subnet-*****",
"subnet-*****",
"subnet-*****",
]
vpc_endpoint_type = "Interface"
...
}
ECS Service
Notice! In order to enable logging to cloudwatch, your container images will need to have script(1)
and cat(1)
. On Alpine/CentOS, the package is util-linux
, bsdutils
for Debian based distros.
Service Resources
Enable enable_execute_command
in aws_ecs_service
.
resource "aws_ecs_service" "service" {
launch_type = "FARGATE"
enable_execute_command = true
...
Task IAM Policy Resources
The policy will need the following Permissions.
ssmmessages:
- These are required for ecs-exec-command to work.
kms:
- They are used for both cloudwatch and ecs-exec-command. See Logging and Auditing
logs:
- They are required to write logs to cloudwatch. See Logging and Auditing
resource "aws_iam_role_policy" "task_execution" {
policy = jsonencode(
{
Statement = [
{
Action = [
"kms:Decrypt",
"kms:DescribeKey",
"kms:Encrypt",
"kms:GenerateDataKey*",
"kms:ReEncrypt*",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:Describe*",
"logs:Get*",
"logs:PutLogEvents",
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel",
...
]
Effect = "Allow"
Resource = "*"
},
]
}
)
}
Task Definition
In the task definition of the services, you will need to enable initProcessEnabled
.
$ aws ecs describe-task-definition --task-definition qa-bastion-01 | jq -r '.taskDefinition.containerDefinitions[] | {linuxParameters: .linuxParameters}'
{
"linuxParameters": {
"initProcessEnabled": true
}
]
}
User IAM Policy Resources
In order for users to run ecs-execute-command, you will need to create a policy and attach it to the user/group IAM.
From what I can tell, we do need to grant the following Actions.
kms:GenerateDataKey
ecs:ExecuteCommand
What I am also doing is ONLY allow if the container-name
does NOT match bastion
.
Notice! I THINK theres a bug in the AWS documentation regarding Resource
. I have to have all 4 in order for it to work: :task/*
and :cluster/*
.
resource "aws_iam_policy" "iam_policy" {
path = "/"
policy = jsonencode(
{
Statement = [
{
Action = "kms:GenerateDataKey"
Resource = "arn:aws:kms:us-west-2:*****:key/*****"
},
{
Action = "ecs:ExecuteCommand"
Condition = {
StringNotLike = {
ecs:container-name = [
"qa-bastion-*",
]
}
}
Effect = "Allow"
Resource = [
"arn:aws:ecs:us-west-2:*****:task/qa-default-01/*",
"arn:aws:ecs:us-west-2:*****:task/qa-default-01",
"arn:aws:ecs:us-west-2:*****:cluster/qa-default-01/*",
"arn:aws:ecs:us-west-2:*****:cluster/qa-default-01",
]
},
]
}
)
}
Logging and Auditing
We want to enable auditing in cloudtrail and logging in cloudwatch log. For aws_ecs_cluster
we set this up under configuration
section.
Cluster Resources
There are some settings needed in the ECS cluster, Here are the essentials. We are enabling cloudwatch log with KMS.
resource "aws_ecs_cluster" "ecs_cluster" {
...
configuration {
execute_command_configuration {
kms_key_id = "arn:aws:kms:us-west-2:*****:key/*****"
logging = "OVERRIDE"
log_configuration {
cloud_watch_encryption_enabled = true
cloud_watch_log_group_name = "/ecs/cluster/qa-default-01"
s3_bucket_encryption_enabled = false
}
}
}
}
Cloudwatch log with KMS key.
resource "aws_cloudwatch_log_group" "cloudwatch_log_group" {
id = "/ecs/cluster/qa-default-01"
kms_key_id = "arn:aws:kms:us-west-2:*****:key/*****"
name = "/ecs/cluster/qa-default-01"
retention_in_days = 30
...
}
KMS key requires the following policy: kms:
and access to logs resource.
resource "aws_kms_key" "kms_key" {
deletion_window_in_days = 7
description = "ECS Cluster Key - qa-default-01"
is_enabled = true
key_usage = "ENCRYPT_DECRYPT"
policy = jsonencode(
{
Statement = [
{
Action = "kms:*"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::*****:root"
}
Resource = "*"
},
{
Action = [
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:Encrypt*",
"kms:Describe*",
"kms:Decrypt*",
]
Effect = "Allow"
Principal = {
Service = "logs.us-west-2.amazonaws.com"
}
Resource = "*"
},
]
}
)
}
resource "aws_kms_alias" "kms_alias" {
id = "alias/qa-default-01-key"
name = "alias/qa-default-01-key"
target_key_id = "*****"
}
Tools
I wrote a tool that interacts with ecs-exec-command
called ecsrun. This along with other tools in the aws-utils repo.
For validating requirements are met, Amazon ECS Exec Checker is very useful.
$ bash <(curl -Ls https://raw.githubusercontent.com/aws-containers/amazon-ecs-exec-checker/main/check-ecs-exec.sh) qa-default-01 *****
-------------------------------------------------------------
Prerequisites for check-ecs-exec.sh v0.7
-------------------------------------------------------------
jq | OK (/usr/local/bin/jq)
AWS CLI | OK (/usr/local/bin/aws)
-------------------------------------------------------------
Prerequisites for the AWS CLI to use ECS Exec
-------------------------------------------------------------
AWS CLI Version | OK (aws-cli/2.4.6 Python/3.9.9 Darwin/21.1.0 source/x86_64 prompt/off)
Session Manager Plugin | OK (1.2.279.0)
-------------------------------------------------------------
Checks on ECS task and other resources
-------------------------------------------------------------
Region : us-west-2
Cluster: qa-default-01
Task : *****
-------------------------------------------------------------
Cluster Configuration |
KMS Key : arn:aws:kms:us-west-2:*****:key/*****
Audit Logging : OVERRIDE
S3 Bucket Name: Not Configured
CW Log Group : /ecs/cluster/qa-default-01, Encryption Enabled: true
Can I ExecuteCommand? | arn:aws:iam::*****:role/Admin
ecs:ExecuteCommand: allowed
kms:GenerateDataKey: allowed
ssm:StartSession denied?: allowed
Task Status | RUNNING
Launch Type | Fargate
Platform Version | 1.4.0
Exec Enabled for Task | OK
Container-Level Checks |
----------
Managed Agent Status
----------
1. RUNNING for "qa-bastion-01"
...
----------
Init Process Enabled (qa-bastion-01:20)
----------
1. Enabled - "qa-bastion-01"
...
----------
Read-Only Root Filesystem (qa-bastion-01:20)
----------
1. Disabled - "qa-bastion-01"
...
Task Role Permissions | arn:aws:iam::*****:role/qa-bastion-01-task-execution
ssmmessages:CreateControlChannel: allowed
ssmmessages:CreateDataChannel: allowed
ssmmessages:OpenControlChannel: allowed
ssmmessages:OpenDataChannel: allowed
-----
kms:Decrypt: allowed
-----
logs:DescribeLogGroups: allowed
logs:CreateLogStream: allowed
logs:DescribeLogStreams: allowed
logs:PutLogEvents: allowed
VPC Endpoints |
Found existing endpoints for vpc-*****:
- com.amazonaws.us-west-2.ssmmessages
...
AWS CLI
Check for ExecuteCommand
status.
$ ecslookup qa-default-01 | \
while read line; do \
aws ecs describe-services --cluster qa-default-01 --service ${line} | \
jq -r '.services[] | [.serviceName, .status, .launchType, .platformVersion, .createdAt, .enableExecuteCommand] | join(",")'; done | \
column -ts, | \
sort
qa-bastion-01 ACTIVE FARGATE 1.4.0 2021-12-27T13:03:28.958000-08:00 true
...
qa-notification-01 ACTIVE FARGATE 1.4.0 2021-03-12T14:47:07.209000-08:00 false
qa-notification-02 ACTIVE FARGATE 1.4.0 2021-11-09T12:18:50.024000-08:00 false
...
qa-rest-green ACTIVE FARGATE 1.4.0 2021-03-12T13:56:35.764000-08:00 true
Auditing with Cloudwatch and Cloudtrail.
$ aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=ExecuteCommand | \
jq -r '.Events[] | .CloudTrailEvent' | \
jq -r '[.userIdentity.arn, .eventTime, .sourceIPAddress, (.requestParameters | to_entries[] | .value)] + [if .responseElements != null then .responseElements.session.streamUrl | gsub(".*data-channel/(?<match>[a-z0-9-]+)?.*"; .match) else "not-available" end] | join(",")' | \
column -ts, | \
head -n 2
arn:aws:sts::*****:assumed-role/Admin/cwong@your-company.com 2021-12-20T16:32:07Z 199.***.237.28 qa-default-01 qa-bastion-01 /bin/sh true *****-service-task-***** not-available
arn:aws:sts::*****:assumed-role/Admin/cwong@your-company.com 2021-12-20T06:17:04Z 199.***.237.28 qa-default-01 qa-recommendation-api-01 /bin/sh true *****-service-task-***** ecs-execute-command-0ee3598c40e47bf6e
$ aws logs describe-log-streams --log-group-name /ecs/cluster/qa-default-01 --log-stream-name-prefix ecs-execute-command
{
"logStreams": [
{
"logStreamName": "ecs-execute-command-0ee3598c40e47bf6e",
"creationTime": 1639966898013,
...
},
...
$ aws logs get-log-events --log-group-name /ecs/cluster/qa-default-01 --log-stream-name ecs-execute-command-0ee3598c40e47bf6e
{
"events": [
{
"timestamp": 1639966897981,
"message": "Script started on 2021-12-20 02:21:36+00:00\nsh-4.4# \r\nsh-4.4# exit\r\nexit\r\n\nScript done on 2021-12-20 02:21:36+00:00",
"ingestionTime": 1639966898034
}
],
...
}
Invoke ecs-exec-command
.
$ aws ecs execute-command --cluster qa-default-01 --container qa-bastion-01 --task ***** --interactive --command /bin/sh
The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.
Starting session with SessionId: ecs-execute-command-0d8772eca1533f471
This session is encrypted using AWS KMS.
/ #