Tarek Cheikh
Founder & AWS Security Expert
Amazon Elastic Container Service (ECS) is the backbone of containerized workloads on AWS, running millions of containers daily across EC2 and Fargate launch types. But containers introduce a unique attack surface -- shared kernel namespaces, image supply chain risks, over-privileged task roles, and runtime threats that traditional VM security does not address.
In August 2025, researchers at Sweet Security disclosed ECScape at Black Hat USA -- a vulnerability in the undocumented ECS Agent Communication Service (ACS) protocol that allows a compromised container on an EC2 host to steal IAM credentials from neighboring tasks without ever escaping the container. The attack works by manipulating network traffic to impersonate the ECS agent, exploiting default behaviors of ECS on EC2. Then in November 2025, AWS GuardDuty detected an active cryptomining campaign where attackers used compromised IAM credentials to spin up over 50 ECS clusters per account, deploying the malicious Docker Hub image yenik65958/secret:user (over 100,000 pulls) to Fargate tasks and mining cryptocurrency within 10 minutes of initial access. A key technique observed was the use of ModifyInstanceAttribute with disable API termination set to true, forcing victims to re-enable API termination before they could delete the compromised resources.
These incidents underscore that securing ECS requires a layered approach spanning identity, network, image supply chain, runtime monitoring, and compliance automation. This guide covers 12 battle-tested best practices with real CLI commands and the latest 2025-2026 updates from AWS.
ECS has two distinct IAM roles that are frequently confused, leading to over-privileged containers:
taskRoleArn): The IAM role your application code assumes at runtime. This is what your container uses to call AWS APIs (S3, DynamoDB, SQS, etc.).executionRoleArn): The IAM role the ECS agent uses to pull images from ECR, fetch secrets, and write logs. Your application never sees these credentials.The danger: many teams assign a single over-permissive role for both purposes. If any container is compromised -- as in the November 2025 cryptomining campaign -- the attacker inherits every permission from both roles simultaneously.
# Create a minimal task role (application permissions only)
aws iam create-role --role-name my-app-task-role --assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ecs-tasks.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
# Attach only the permissions your app needs -- specific ARNs, never wildcards
aws iam put-role-policy --role-name my-app-task-role --policy-name app-permissions --policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-app-bucket/*"
}]
}'
# Create a separate execution role (ECR pull + logs only)
aws iam create-role --role-name my-app-execution-role --assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ecs-tasks.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
aws iam attach-role-policy --role-name my-app-execution-role --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
Security Hub Controls: [ECS.4] (containers should run as non-privileged), [ECS.17] (task definitions should not share the host process namespace). Never assign AdministratorAccess or broad *:* policies to task roles. Use IAM Access Analyzer to generate least-privilege policies from CloudTrail usage data.
Key principle: Each microservice should have its own task role with the minimum permissions it needs. Never share a single task role across multiple services -- this violates least privilege and increases blast radius during a compromise.
By default, containers run as root (UID 0). If an attacker exploits an application vulnerability, they gain root-level access inside the container -- and potentially to the host via kernel exploits or misconfigurations like shared namespaces.
In your Dockerfile:
# Create a non-root user
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
# Set ownership of application files
COPY . /app
# Switch to non-root user
USER appuser
# Application runs as UID != 0
CMD ["node", "server.js"]
In your ECS task definition, enforce read-only root filesystem and drop all Linux capabilities:
{
"containerDefinitions": [{
"name": "my-app",
"image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest",
"user": "appuser",
"readonlyRootFilesystem": true,
"linuxParameters": {
"capabilities": {
"drop": ["ALL"]
}
},
"mountPoints": [{
"sourceVolume": "tmp-volume",
"containerPath": "/tmp",
"readOnly": false
}]
}],
"volumes": [{
"name": "tmp-volume"
}]
}
Security Hub Control [ECS.5]: ECS containers should be limited to read-only access to root filesystems. Combined with dropping all Linux capabilities and running as non-root, this drastically limits what an attacker can do post-compromise. If your application needs to write temporary files, mount a writable /tmp volume as shown above.
Container images are built from layers of dependencies, each potentially harboring known vulnerabilities (CVEs). Without scanning, you are deploying blind.
# Enable enhanced scanning with Amazon Inspector (replaces basic scanning)
aws ecr put-registry-scanning-configuration --scan-type ENHANCED --rules '[{
"repositoryFilters": [{"filter": "*", "filterType": "WILDCARD"}],
"scanFrequency": "CONTINUOUS_SCAN"
}]'
# Verify scanning configuration
aws ecr get-registry-scanning-configuration
# Check scan findings for a specific image
aws ecr describe-image-scan-findings --repository-name my-app --image-id imageTag=latest
# Clean up old untagged images to reduce attack surface
aws ecr put-lifecycle-policy --repository-name my-app --lifecycle-policy-text '{
"rules": [{
"rulePriority": 1,
"description": "Expire untagged images older than 14 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 14
},
"action": {"type": "expire"}
}]
}'
Enhanced scanning with Amazon Inspector provides continuous monitoring (not just scan-on-push), covers both OS packages and application dependencies (Java, Python, Node.js, Go, .NET), and integrates findings directly into Security Hub.
CI/CD integration: Add a gate in your pipeline that fails the build if critical or high-severity CVEs are found. Use aws ecr describe-image-scan-findings with --query to filter by severity and block deployment of vulnerable images.
Storing secrets in plaintext environment variables exposes them in task definitions, CloudWatch logs, container inspect output, and the ECS console. The November 2025 cryptomining campaign demonstrated how compromised credentials can be weaponized in minutes.
# Store a secret in Secrets Manager
aws secretsmanager create-secret --name /myapp/database-password --secret-string '{"password":"MyS3cur3P@ss!"}'
# Or use SSM Parameter Store (cheaper for non-rotating secrets)
aws ssm put-parameter --name /myapp/api-key --type SecureString --value "sk-abc123..."
Reference secrets in your task definition using secrets (not environment):
{
"containerDefinitions": [{
"name": "my-app",
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:/myapp/database-password:password::"
},
{
"name": "API_KEY",
"valueFrom": "arn:aws:ssm:us-east-1:123456789012:parameter/myapp/api-key"
}
]
}]
}
The execution role needs permission to fetch these secrets:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"ssm:GetParameters"
],
"Resource": [
"arn:aws:secretsmanager:us-east-1:123456789012:secret:/myapp/*",
"arn:aws:ssm:us-east-1:123456789012:parameter/myapp/*"
]
}]
}
Security Hub Control [ECS.8]: Secrets should not be passed as container environment variables. This control specifically flags task definitions that use the environment field for sensitive values instead of the secrets field. Use Secrets Manager for credentials that need automatic rotation and SSM Parameter Store for static configuration values.
Static scanning catches known CVEs, but runtime threats -- cryptominers, reverse shells, lateral movement -- require behavioral detection. In December 2025, AWS extended GuardDuty Extended Threat Detection to ECS, introducing the critical-severity finding AttackSequence:ECS/CompromisedCluster that correlates multiple signals across runtime activity, malware detections, VPC Flow Logs, DNS queries, and CloudTrail events.
# Enable GuardDuty ECS Runtime Monitoring
aws guardduty update-detector --detector-id $(aws guardduty list-detectors --query "DetectorIds[0]" --output text) --features '[{
"Name": "RUNTIME_MONITORING",
"Status": "ENABLED",
"AdditionalConfiguration": [{
"Name": "ECS_FARGATE_AGENT_MANAGEMENT",
"Status": "ENABLED"
}]
}]'
# Verify runtime monitoring status
aws guardduty get-detector --detector-id $(aws guardduty list-detectors --query "DetectorIds[0]" --output text) --query "Features[?Name=='RUNTIME_MONITORING']"
For Fargate tasks, the GuardDuty security agent is fully managed -- no installation, configuration, or updates required. For EC2 launch type, the agent deploys automatically as a sidecar container when you enable the feature. Findings map to MITRE ATT&CK tactics and integrate with Security Hub, EventBridge, and Amazon Detective for automated response and investigation.
The awsvpc network mode gives each ECS task its own elastic network interface (ENI) and private IP address, enabling task-level security groups. This is the only network mode supported by Fargate and the most secure option for EC2 launch type.
# Create a security group for a specific microservice
aws ec2 create-security-group --group-name ecs-api-service-sg --description "Security group for API service tasks" --vpc-id vpc-0123456789abcdef0
# Allow only ALB traffic on port 8080
aws ec2 authorize-security-group-ingress --group-id sg-0123456789abcdef0 --protocol tcp --port 8080 --source-group sg-ALB-SECURITY-GROUP
# Register task definition with awsvpc mode
aws ecs register-task-definition --family my-api-service --network-mode awsvpc --requires-compatibilities FARGATE --cpu "256" --memory "512" --container-definitions '[{
"name": "api",
"image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/api:latest",
"portMappings": [{"containerPort": 8080, "protocol": "tcp"}]
}]'
# Create service with security group and private subnets
aws ecs create-service --cluster my-cluster --service-name api-service --task-definition my-api-service --network-configuration '{
"awsvpcConfiguration": {
"subnets": ["subnet-private-1", "subnet-private-2"],
"securityGroups": ["sg-0123456789abcdef0"],
"assignPublicIp": "DISABLED"
}
}'
Security Hub Control [ECS.2]: ECS services should not have public IP addresses assigned automatically. Always deploy tasks in private subnets with assignPublicIp: DISABLED and route outbound traffic through NAT gateways or VPC endpoints.
Best practice: Each microservice should have its own security group. Never share a single security group across all tasks -- this defeats the purpose of network micro-segmentation. Use security group references (not CIDR ranges) to restrict inter-service communication to only the paths your architecture requires.
The ECScape vulnerability, presented at Black Hat USA 2025, demonstrated that a compromised container running on an EC2 host can steal IAM credentials from neighboring ECS tasks. The attack exploits the undocumented Agent Communication Service (ACS) WebSocket protocol by manipulating network traffic from within the container to impersonate the ECS agent -- all without requiring a traditional container escape. Unlike typical container escapes that require host-level access, ECScape operates entirely within the container's namespace.
AWS stated that this behavior does not present a security concern and updated documentation rather than issuing a patch. No CVE was assigned. This makes proactive mitigation essential.
{
"containerDefinitions": [{
"name": "my-app",
"privileged": false,
"linuxParameters": {
"capabilities": {
"drop": ["ALL"],
"add": []
},
"initProcessEnabled": true
}
}],
"pidMode": "task",
"ipcMode": "task"
}
pidMode and ipcMode to task instead of host. This prevents containers from seeing processes or shared memory in other tasks on the same instance."privileged": false explicitly and drop all Linux capabilities.# Enforce IMDSv2 with hop limit 1 on ECS EC2 instances
# (prevents containers from reaching IMDS through the container network layer)
aws ec2 modify-instance-metadata-options --instance-id i-0123456789abcdef0 --http-tokens required --http-put-response-hop-limit 1
# Set ECS agent to disable privileged containers on the host
# In /etc/ecs/ecs.config:
# ECS_DISABLE_PRIVILEGED=true
# Set up CloudTrail alerts to detect unusual usage of IAM roles
aws cloudwatch put-metric-alarm --alarm-name ecs-unusual-role-assumption --metric-name UnauthorizedAttemptCount --namespace CloudTrailMetrics --statistic Sum --period 300 --threshold 1 --comparison-operator GreaterThanOrEqualToThreshold --evaluation-periods 1 --alarm-actions arn:aws:sns:us-east-1:123456789012:security-alerts
Security Hub Control [ECS.3]: ECS task definitions should not share the host's process namespace. This control flags task definitions with pidMode set to host.
Without centralized logging, investigating a container compromise is nearly impossible. Containers are ephemeral -- when a task stops, its local logs disappear. The November 2025 cryptomining campaign was detected in part because GuardDuty correlated runtime events with CloudTrail logs -- organizations without centralized logging would have been blind to the attack.
{
"containerDefinitions": [
{
"name": "log-router",
"image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable",
"essential": true,
"firelensConfiguration": {
"type": "fluentbit",
"options": {
"config-file-type": "file",
"config-file-value": "/fluent-bit/configs/parse-json.conf"
}
}
},
{
"name": "my-app",
"image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest",
"logConfiguration": {
"logDriver": "awsfirelens",
"options": {
"Name": "cloudwatch_logs",
"region": "us-east-1",
"log_group_name": "/ecs/my-app",
"log_stream_prefix": "app-",
"auto_create_group": "true"
}
}
}
]
}
# Enable enhanced Container Insights on an ECS cluster
aws ecs update-cluster-settings --cluster my-cluster --settings '[{"name": "containerInsights", "value": "enhanced"}]'
# Verify Container Insights is enabled
aws ecs describe-clusters --clusters my-cluster --query "clusters[0].settings"
Security Hub Control [ECS.12]: ECS clusters should use Container Insights. Container Insights provides CPU, memory, network, and storage metrics at the task and service level, plus integration with CloudWatch for alarms and dashboards.
For security-critical environments, route logs to both CloudWatch (for real-time alerting) and S3 (for long-term retention and forensics). Use FireLens to fan out logs to multiple destinations simultaneously.
ECS Fargate tasks can mount Amazon EFS file systems for persistent storage. Without encryption, data at rest on the file system and data in transit between the task and EFS are exposed.
# Create an encrypted EFS file system
aws efs create-file-system --encrypted --kms-key-id arn:aws:kms:us-east-1:123456789012:key/my-efs-key --performance-mode generalPurpose --tags Key=Name,Value=ecs-app-storage
# Create a mount target in each private subnet
aws efs create-mount-target --file-system-id fs-0123456789abcdef0 --subnet-id subnet-private-1 --security-groups sg-efs-access
# Create an EFS access point for the application
aws efs create-access-point --file-system-id fs-0123456789abcdef0 --posix-user '{"Uid": 1000, "Gid": 1000}' --root-directory '{"Path": "/app-data", "CreationInfo": {"OwnerUid": 1000, "OwnerGid": 1000, "Permissions": "750"}}'
In your task definition, enforce in-transit encryption:
{
"volumes": [{
"name": "app-data",
"efsVolumeConfiguration": {
"fileSystemId": "fs-0123456789abcdef0",
"transitEncryption": "ENABLED",
"transitEncryptionPort": 2999,
"authorizationConfig": {
"accessPointId": "fsap-0123456789abcdef0",
"iam": "ENABLED"
}
}
}]
}
Setting iam: ENABLED ensures that the task role must have elasticfilesystem:ClientMount and elasticfilesystem:ClientWrite permissions, adding an IAM-based access control layer on top of POSIX permissions. Combined with EFS access points, this provides fine-grained isolation between tasks sharing the same file system. Use KMS customer-managed keys for encryption to maintain full control over key rotation and access policies.
By default, inter-service traffic within a VPC is unencrypted. If an attacker gains network access (via a compromised container or VPC misconfiguration), they can sniff traffic between microservices.
ECS Service Connect provides built-in service discovery and traffic management with optional TLS encryption using AWS Private Certificate Authority (PCA) short-lived certificates:
# Create a Cloud Map namespace for Service Connect
aws servicediscovery create-http-namespace --name my-app.local --description "ECS Service Connect namespace"
# Create an ECS service with Service Connect and TLS
aws ecs create-service --cluster my-cluster --service-name api-service --task-definition my-api-service --service-connect-configuration '{
"enabled": true,
"namespace": "arn:aws:servicediscovery:us-east-1:123456789012:namespace/ns-EXAMPLE",
"services": [{
"portName": "api",
"discoveryName": "api-service",
"clientAliases": [{"port": 8080, "dnsName": "api.my-app.local"}],
"tls": {
"issuerCertificateAuthority": {
"awsPcaAuthorityArn": "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/ca-EXAMPLE"
},
"roleArn": "arn:aws:iam::123456789012:role/ServiceConnectTLSRole"
}
}]
}'
Service Connect manages the Envoy proxy sidecar automatically, handles certificate rotation via ACM PCA, and requires no application code changes. For environments requiring mutual TLS (mTLS), use Application Load Balancer mTLS termination with ACM PCA-issued client certificates.
Important: AWS App Mesh will be discontinued on September 30, 2026. If you are using App Mesh for mTLS, migrate to ECS Service Connect. For new deployments, Service Connect should be your default choice.
Without image signing, there is no guarantee that the container image running in production is the same image that passed your CI/CD security checks. An attacker who compromises your ECR repository or CI pipeline can inject malicious images -- exactly the supply chain attack pattern seen in the November 2025 campaign where the malicious yenik65958/secret image was pulled from Docker Hub.
# Create a signing profile
aws signer put-signing-profile --profile-name ecs-production --platform-id Notation-OCI-SHA384-ECDSA
# Sign an image with Notation CLI and AWS Signer plugin
notation sign --plugin com.amazonaws.signer.notation.plugin --id arn:aws:signer:us-east-1:123456789012:/signing-profiles/ecs-production 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest
# Verify an image signature
notation verify 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest --trust-policy ./trust-policy.json
# Alternative: use cosign for Sigstore-based verification
cosign verify --key awskms:///arn:aws:kms:us-east-1:123456789012:key/my-signing-key 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest
Amazon ECR now supports managed signing, where images are automatically signed during push without requiring client-side tools like Notation or cosign. AWS Signer handles key material, certificate lifecycle management, key generation, secure storage, and rotation. For existing pipelines that need manual control, use the Notation CLI with the AWS Signer plugin as shown above.
Use ECS service deployment lifecycle hooks with Lambda to verify image signatures before tasks start. If the signature is invalid, the Lambda function kills the tasks and sends an alert via SNS. Combine image signing with ECR immutable tags to prevent tag overwriting, ensuring that a signed v1.2.3 tag always points to the same verified image digest.
AWS Security Hub provides automated, continuous evaluation of your ECS configuration against the AWS Foundational Security Best Practices (FSBP) standard.
| Control ID | Description | Severity |
|---|---|---|
| [ECS.4] | Containers should run as non-privileged | High |
| [ECS.2] | ECS services should not have public IP addresses assigned automatically | High |
| [ECS.3] | Task definitions should not share the host's process namespace | High |
| [ECS.5] | Containers should be limited to read-only access to root filesystems | High |
| [ECS.8] | Secrets should not be passed as container environment variables | High |
| [ECS.10] | Fargate services should run on the latest Fargate platform version | Medium |
| [ECS.12] | ECS clusters should use Container Insights | Medium |
# Enable AWS Foundational Security Best Practices standard
aws securityhub batch-enable-standards --standards-subscription-requests '[{
"StandardsArn": "arn:aws:securityhub:us-east-1::standards/aws-foundational-security-best-practices/v/1.0.0"
}]'
# List all failing ECS controls
aws securityhub get-findings --filters '{
"ComplianceStatus": [{"Value": "FAILED", "Comparison": "EQUALS"}],
"ResourceType": [{"Value": "AwsEcsTaskDefinition", "Comparison": "EQUALS"}]
}' --query "Findings[].{Control:Compliance.SecurityControlId,Resource:Resources[0].Id,Status:Compliance.Status}"
# Get Fargate platform version compliance
aws securityhub get-findings --filters '{
"ComplianceSecurityControlId": [{"Value": "ECS.10", "Comparison": "EQUALS"}],
"ComplianceStatus": [{"Value": "FAILED", "Comparison": "EQUALS"}]
}'
Security Hub Control [ECS.10]: Fargate services should run on the latest platform version. Older platform versions may lack security patches and features. Always specify LATEST or pin to the current version (1.4.0 for Linux) and update regularly.
Integrate Security Hub findings with EventBridge to trigger automated remediation -- for example, automatically stopping a service that deploys a task definition with pidMode: host or secrets in environment variables.
| Misconfiguration | Risk | Detection |
|---|---|---|
| Shared task role across services | Blast radius expansion on compromise | IAM Access Analyzer unused permissions |
| Containers running as root | Privilege escalation to host | Security Hub [ECS.4] |
| Secrets in environment variables | Credential exposure in logs and console | Security Hub [ECS.8] |
pidMode: host on task definitions |
Cross-container process visibility (ECScape) | Security Hub [ECS.3] |
| Public IP on Fargate tasks | Direct internet exposure | Security Hub [ECS.2] |
| No image scanning enabled | Deploying images with known CVEs | Inspector dashboard |
| Outdated Fargate platform version | Missing security patches | Security Hub [ECS.10] |
| # | Practice | Priority |
|---|---|---|
| 1 | Separate task roles from execution roles | Critical |
| 2 | Run containers as non-root | Critical |
| 3 | Enable ECR image scanning with Inspector | Critical |
| 4 | Inject secrets via Secrets Manager/SSM | Critical |
| 5 | Enable GuardDuty runtime monitoring | High |
| 6 | Use awsvpc mode with task-level security groups | High |
| 7 | Mitigate ECScape (restrict namespaces, use Fargate) | High |
| 8 | Centralize logging with FireLens and Container Insights | High |
| 9 | Encrypt EFS volumes (at-rest and in-transit) | Medium |
| 10 | Enable TLS for service-to-service communication | Medium |
| 11 | Sign and verify container images | Medium |
| 12 | Automate compliance with Security Hub ECS controls | High |
This article is just the start. Get the full picture with our free whitepaper - 8 chapters covering IAM, S3, VPC, monitoring, agentic AI security, compliance, and a prioritized action plan with 50+ CLI commands.
Comprehensive guide to securing AWS EC2 instances. Covers IMDSv2 enforcement, security groups, EBS encryption, SSM Session Manager, private subnets, VPC Flow Logs, Amazon Inspector, and AMI hardening.
Comprehensive guide to securing AWS Identity and Access Management. Covers MFA enforcement, least privilege, IAM Identity Center, SCPs, Access Analyzer, and credential management.
Comprehensive guide to securing AWS Virtual Private Cloud. Covers Security Groups, NACLs, VPC Flow Logs, VPC Endpoints, Block Public Access, Encryption Controls, Network Firewall, Transit Gateway, and GuardDuty threat detection.