Pulling private container images into ECS securely is actually a solved problem, but the common approach is often overly complex, leading to security misconfigurations.
Let’s see it in action. Imagine we have a private Docker registry (like Docker Hub, ECR, or a self-hosted registry) and an ECS task that needs to pull an image from it.
Here’s a simplified task-definition.json snippet for an ECS task that needs to pull a private image:
{
"family": "my-private-app-task",
"containerDefinitions": [
{
"name": "app-container",
"image": "my-private-registry.com/my-app:latest",
"cpu": 256,
"memory": 512,
"portMappings": [
{
"containerPort": 80,
"hostPort": 80
}
],
"essential": true
}
],
"requiresCompatibilities": [
"FARGATE"
],
"networkMode": "awsvpc",
"cpu": "256",
"memory": "512"
}
When ECS tries to pull my-private-registry.com/my-app:latest, it needs credentials. The most secure and idiomatic way to provide these is via an IAM role associated with the ECS task or ECS service.
The problem ECS solves is how to authenticate the container runtime (Docker, containerd) with your private registry without embedding credentials directly in your task definition or container image. This is crucial because credentials should never be hardcoded.
Here’s the mental model:
- ECS Task Role: You grant your ECS task an IAM role. This role has permissions to perform actions on behalf of the task.
- Registry Authentication: For private registries, ECS needs a way to obtain authentication tokens.
- AWS ECR: If your private registry is Amazon Elastic Container Registry (ECR), ECS can automatically authenticate. The ECS task role doesn’t need explicit
ecr:GetAuthorizationTokenorecr:BatchCheckLayerAvailabilitypermissions for ECR if the task is running on the same AWS account as the ECR repository. ECS itself handles the authentication using the instance profile or task role implicitly. - Other Registries (Docker Hub, Artifactory, etc.): For non-ECR registries, ECS needs a mechanism to retrieve credentials. The most common and secure way is to use AWS Secrets Manager. You store your registry username and password (or an API token) in a Secrets Manager secret.
- AWS ECR: If your private registry is Amazon Elastic Container Registry (ECR), ECS can automatically authenticate. The ECS task role doesn’t need explicit
- Task Definition Configuration: In your task definition, you reference the Secrets Manager secret. ECS then retrieves the secret’s content at runtime and makes it available to the container runtime for authentication.
Let’s flesh this out with a non-ECR example. Suppose you’re using Docker Hub with a username my-dockerhub-user and an access token abcdef1234567890.
Step 1: Store Credentials in Secrets Manager
Create a secret in AWS Secrets Manager. The content should be a JSON string:
{
"username": "my-dockerhub-user",
"password": "abcdef1234567890"
}
Let’s say you name this secret dockerhub-credentials.
Step 2: Grant ECS Task Role Permissions
Your ECS task execution role needs permission to secretsmanager:GetSecretValue for the dockerhub-credentials secret.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:dockerhub-credentials-XXXXXX" // Replace with your actual ARN
}
]
}
Step 3: Update Task Definition
Modify your task definition to include the repositoryCredentials block, referencing the Secrets Manager secret.
{
"family": "my-private-app-task",
"containerDefinitions": [
{
"name": "app-container",
"image": "docker.io/my-dockerhub-user/my-app:latest", // Explicitly use docker.io for clarity
"cpu": 256,
"memory": 512,
"portMappings": [
{
"containerPort": 80,
"hostPort": 80
}
],
"essential": true,
"repositoryCredentials": {
"credentialsParameter": "arn:aws:secretsmanager:us-east-1:123456789012:secret:dockerhub-credentials-XXXXXX" // Replace with your actual ARN
}
}
],
"requiresCompatibilities": [
"FARGATE"
],
"networkMode": "awsvpc",
"cpu": "256",
"memory": "512"
}
When ECS launches the task, it will:
- Look up the
repositoryCredentialsin the task definition. - Use the task execution role to call
secretsmanager:GetSecretValuefor the specified secret ARN. - Parse the returned JSON to get the
usernameandpassword. - Pass these credentials to the container runtime (Docker/containerd) when it requests the image
docker.io/my-dockerhub-user/my-app:latest. The container runtime then uses these to authenticate with Docker Hub.
The most surprising thing about this mechanism is how ECS abstracts away the credential handling for the container runtime. You don’t directly configure Docker daemons or ~/.docker/config.json files within your Fargate tasks. ECS orchestrates the retrieval of these credentials from Secrets Manager and ensures they are available to the underlying container runtime without exposing them as environment variables or command-line arguments to your application containers. This is a significant security win, as it prevents your application code from ever seeing or needing to handle these sensitive credentials.
The next hurdle is often how to manage image pull secrets for multiple private registries within the same task definition or how to handle different credential types (e.g., IAM roles for ECR vs. tokens for Docker Hub).