ECS task IAM roles are a common point of over-privilege, and it’s easy for them to grow into massive, all-access buckets.
Let’s say you have a task running an application that needs to read from a specific S3 bucket, my-app-data-bucket.
Here’s a task definition snippet:
{
"family": "my-app-task",
"taskRoleArn": "arn:aws:iam::123456789012:role/MyECSTaskRole",
"executionRoleArn": "arn:aws:iam::123456789012:role/MyECSExecutionRole",
"containerDefinitions": [
{
"name": "my-app-container",
"image": "my-docker-repo/my-app:latest",
"portMappings": [
{
"containerPort": 8080,
"hostPort": 8080
}
]
}
]
}
The taskRoleArn is what your application inside the container assumes. The executionRoleArn is what the ECS agent uses to pull the image, push logs, etc. We’re focusing on the taskRoleArn here.
A common, but dangerous, MyECSTaskRole might look like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": "*"
}
]
}
This grants all S3 actions on all buckets. Bad.
Instead, we want to restrict it.
First, identify exactly what your application needs. For our example, it’s reading objects from my-app-data-bucket.
The IAM policy should look like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-app-data-bucket",
"arn:aws:s3:::my-app-data-bucket/*"
]
}
]
}
Let’s break down why this works:
"Action": ["s3:GetObject", "s3:ListBucket"]: This explicitly lists the only S3 actions allowed.GetObjectlets you read individual files, andListBucketlets you see what files are in the bucket. If your app only needs to read specific files it knows the name of, you might even omitListBucket."Resource": ["arn:aws:s3:::my-app-data-bucket", "arn:aws:s3:::my-app-data-bucket/*"]: This is crucial.arn:aws:s3:::my-app-data-bucket: This ARN refers to the bucket itself, and is required for actions likeListBucket.arn:aws:s3:::my-app-data-bucket/*: This ARN refers to all objects within the bucket, and is required for actions likeGetObject.
The s3:GetObject action requires permissions on the objects, while s3:ListBucket requires permissions on the bucket itself. Specifying both ARNs covers the necessary permissions for these actions.
To implement this:
-
Create or Update IAM Role:
- Go to the IAM console.
- Navigate to "Roles".
- Find your
MyECSTaskRole(or create a new one). - Attach a new policy with the JSON above.
-
Update ECS Task Definition:
- Go to the ECS console.
- Navigate to "Task Definitions".
- Select your task definition and click "Create new revision".
- In the "Task role" dropdown, select your updated
MyECSTaskRole. - Click "Create".
-
Update ECS Service:
- Go to your ECS cluster and service.
- Edit the service.
- Under "Task Definition", select the new revision you just created.
- Update the service.
Now, any task launched with this new task definition revision will only have the explicitly granted permissions. If your application tries to access another-bucket or perform s3:PutObject, it will fail with an AccessDenied error.
The most common pitfall when scoping down is forgetting to include both the bucket ARN and the object ARN for actions that operate on both. For instance, if you only include arn:aws:s3:::my-app-data-bucket/*, s3:ListBucket will fail because the permission isn’t granted on the bucket itself. Conversely, if you only include arn:aws:s3:::my-app-data-bucket, s3:GetObject will fail because the permission isn’t granted on the objects.
The next error you’ll likely encounter after fixing this is an AccessDenied error when your application tries to perform an action that was implicitly allowed by a broader permission but is now explicitly denied by your more restrictive policy, and you’ll need to iterate on the Action and Resource fields of your IAM policy.