Skip to main content
emnode / learn
Compliance High severity

AWS Security Hub · ECS

ECS.8: Secrets are passed as plaintext container env vars

Written and reviewed by Emnode · Last reviewed

What does AWS Security Hub ECS.8 check?

ECS.8 fails the latest active task-definition revision when any container's environment block contains a variable named AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, or ECS_ENGINE_AUTH_DATA. Those keys are stored as readable plaintext in the task definition document.

Why does ECS.8 matter?

Anything in the environment block is readable by every principal that can call ecs:DescribeTaskDefinition — usually a far broader set than you would trust with a production credential — and it leaks sideways into IaC state, CI logs, and monitoring tools. A leaked long-lived AWS access key is one of the fastest paths to full account compromise, which is why the control names those keys specifically.

How do I fix ECS.8?

  1. Move the value into AWS Secrets Manager or SSM Parameter Store.
  2. Reference it from the container's secrets field, which injects it at launch without writing plaintext into the definition.
  3. Rotate any credential that was previously exposed in environment, then register the cleaned revision.

Remediation script · bash

# Inventory: flag containers running as root or with a writable root filesystem.
for fam in $(aws ecs list-task-definition-families --status ACTIVE \
    --query 'families[]' --output text); do
  aws ecs describe-task-definition --task-definition "$fam" \
    --query "taskDefinition.containerDefinitions[?user==null || user=='root' || user=='0' || readonlyRootFilesystem!=\`true\`].{Family:'$fam',Name:name,User:user,ReadOnly:readonlyRootFilesystem}" \
    --output text
done

# Harden at the source. Dockerfile:
#   RUN addgroup -S app && adduser -S -G app appuser
#   USER appuser
# Task definition: non-root user, read-only root with one narrow tmpfs, secrets via ARN.
#   "user": "1000:1000",
#   "readonlyRootFilesystem": true,
#   "mountPoints": [{ "sourceVolume": "scratch", "containerPath": "/tmp", "readOnly": false }],
#   "secrets": [{ "name": "DB_PASSWORD",
#     "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/checkout/db-AbCdEf" }]

# Register the hardened revision and roll it out (tasks only update on redeploy).
aws ecs register-task-definition --cli-input-json file://checkout-api-hardened.json
aws ecs update-service --cluster prod --service checkout-api \
  --task-definition checkout-api --force-new-deployment

Full walkthrough (console steps, edge cases and verification) in the lesson Harden ECS container workloads.

Is ECS.8 a false positive?

Passing ECS.8 is a floor, not proof of safety: it only matches those three exact key names. A DB_PASSWORD or STRIPE_SECRET_KEY in the same block is just as exposed but will not trip this control — move those to secrets too.

Part of the learning path Lock down access
  • ECS.2 An ECS service auto-assigns public IPs to tasks
  • ECS.3 A task definition shares the host PID namespace
  • ECS.4 A container runs in privileged mode
  • ECS.5 A container has a writable root filesystem
  • ECS.9 A task definition has no logging configuration
  • ECS.10 Fargate services should run latest platform version
  • ECS.12 ECS clusters should use Container Insights
  • ECS.16 An ECS task set auto-assigns public IPs
  • ECS.18 ECS task defs should encrypt EFS volumes in transit
  • ECS.19 Capacity providers managed termination protection
  • ECS.20 Linux containers should run as non-root users
  • ECS.21 Windows containers should run as non-admin users