Skip to main content

ECS Task Definitions Contain No Secrets in Environment Variables

Overview

This check scans AWS ECS task definitions for plaintext secrets stored in container environment variables. Prowler uses pattern-based detection to identify credentials such as API keys, passwords, tokens, and database connection strings embedded directly in task definition configurations.

Risk

Storing secrets in ECS task definition environment variables creates serious security vulnerabilities:

  • Credential exposure: Environment variables are visible in plain text through the ECS console, CLI, AWS API, and container metadata service
  • Log leakage: Secrets may appear in CloudWatch logs, deployment logs, CloudTrail events, or debugging output
  • Metadata endpoint access: Containers can access environment variables of other containers in the same task via the task metadata endpoint
  • Lateral movement: Attackers who obtain credentials can access connected systems (databases, APIs, other AWS services)
  • Compliance violations: Storing secrets in plain-text environment variables violates PCI-DSS, HIPAA, SOC 2, and other security frameworks

Severity: Critical

Remediation Steps

Prerequisites

  • AWS account access with permissions to modify ECS task definitions and create secrets
  • Knowledge of which environment variables contain sensitive values
  • Ability to redeploy ECS services or tasks with the updated task definition
Required IAM permissions

You will need the following permissions:

  • ecs:DescribeTaskDefinition - Read task definition configuration
  • ecs:RegisterTaskDefinition - Create new task definition revision
  • ecs:UpdateService - Update service to use new task definition
  • secretsmanager:CreateSecret - Create new secrets
  • secretsmanager:GetSecretValue - Retrieve secret values
  • iam:PassRole - Pass the task execution role to ECS

AWS Console Method

Step 1: Identify the secrets in environment variables

  1. Sign in to the AWS Management Console
  2. Navigate to ECS > Task Definitions
  3. Select the flagged task definition family
  4. Click on the latest revision number to view details
  5. Scroll to Container definitions and expand each container
  6. Review the Environment variables section to identify sensitive values
  7. Note down the variable names and their current values

Step 2: Store secrets in AWS Secrets Manager

  1. Open Secrets Manager in the AWS Console
  2. Click Store a new secret
  3. For Secret type, choose Other type of secret
  4. Enter your secret key-value pairs (e.g., DB_PASSWORD = your-actual-password)
  5. Click Next
  6. For Secret name, enter a descriptive name (e.g., myapp/prod/ecs-credentials)
  7. Add a description and tags if desired
  8. Click Next, configure rotation if desired, then click Store
  9. Copy the Secret ARN for the next step

Step 3: Ensure the task execution role can access secrets

  1. Go to IAM > Roles
  2. Find your ECS task execution role (typically named ecsTaskExecutionRole or similar)
  3. Click Add permissions > Create inline policy
  4. Choose the JSON tab and paste:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "<your-secret-arn>"
}
]
}
  1. Replace <your-secret-arn> with the ARN from Step 2
  2. Click Next, name the policy (e.g., ECSSecretsAccess), and click Create policy

Step 4: Create a new task definition revision with secrets

  1. Return to ECS > Task Definitions
  2. Select your task definition family
  3. Click Create new revision
  4. Scroll to Container definitions and click on the container to edit
  5. In the Environment variables section:
    • Remove the variables containing sensitive values
  6. In the Secrets section (below Environment variables):
    • Click Add secret
    • Enter the Key (environment variable name your app expects, e.g., DB_PASSWORD)
    • For Value, select valueFrom and enter the Secret ARN (optionally with a JSON key: arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp/prod/ecs-credentials:DB_PASSWORD::)
  7. Ensure your Task execution role is set (required for secrets injection)
  8. Click Create to register the new revision

Step 5: Update your ECS service

  1. Navigate to ECS > Clusters > your cluster
  2. Select the Services tab
  3. Click on your service
  4. Click Update service
  5. The new task definition revision should be selected automatically
  6. Check Force new deployment to ensure running tasks are replaced
  7. Click Update
AWS CLI (optional)

View current task definition:

aws ecs describe-task-definition \
--task-definition <your-task-definition> \
--region us-east-1 \
--query 'taskDefinition.containerDefinitions[*].{name:name,environment:environment}'

Create a secret in Secrets Manager:

aws secretsmanager create-secret \
--name myapp/prod/ecs-credentials \
--description "ECS task credentials" \
--secret-string '{"DB_PASSWORD":"your-db-password","API_KEY":"your-api-key"}' \
--region us-east-1

Get the secret ARN:

aws secretsmanager describe-secret \
--secret-id myapp/prod/ecs-credentials \
--region us-east-1 \
--query 'ARN' \
--output text

Add Secrets Manager access to task execution role:

aws iam put-role-policy \
--role-name ecsTaskExecutionRole \
--policy-name ECSSecretsAccess \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:us-east-1:<account-id>:secret:myapp/prod/ecs-credentials-*"
}]
}'

Register new task definition with secrets (example JSON file):

Create a file task-definition.json:

{
"family": "my-task-family",
"executionRoleArn": "arn:aws:iam::<account-id>:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "my-container",
"image": "my-image:latest",
"essential": true,
"environment": [
{
"name": "LOG_LEVEL",
"value": "INFO"
}
],
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:us-east-1:<account-id>:secret:myapp/prod/ecs-credentials:DB_PASSWORD::"
},
{
"name": "API_KEY",
"valueFrom": "arn:aws:secretsmanager:us-east-1:<account-id>:secret:myapp/prod/ecs-credentials:API_KEY::"
}
],
"portMappings": [
{
"containerPort": 80,
"protocol": "tcp"
}
]
}
],
"requiresCompatibilities": ["FARGATE"],
"networkMode": "awsvpc",
"cpu": "256",
"memory": "512"
}

Register the task definition:

aws ecs register-task-definition \
--cli-input-json file://task-definition.json \
--region us-east-1

Update the service to use the new revision:

aws ecs update-service \
--cluster <your-cluster-name> \
--service <your-service-name> \
--task-definition <your-task-definition>:<new-revision-number> \
--force-new-deployment \
--region us-east-1
CloudFormation (optional)
AWSTemplateFormatVersion: '2010-09-09'
Description: ECS Task Definition with Secrets Manager integration (no secrets in env vars)

Parameters:
DbPassword:
Type: String
NoEcho: true
Description: Database password to store in Secrets Manager
ApiKey:
Type: String
NoEcho: true
Description: API key to store in Secrets Manager

Resources:
# Store secrets in Secrets Manager
AppSecrets:
Type: AWS::SecretsManager::Secret
Properties:
Name: myapp/prod/ecs-credentials
Description: Credentials for ECS task
SecretString: !Sub '{"DB_PASSWORD":"${DbPassword}","API_KEY":"${ApiKey}"}'

# Task execution role with secrets access
TaskExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: MyECSTaskExecutionRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
Policies:
- PolicyName: SecretsAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: secretsmanager:GetSecretValue
Resource: !Ref AppSecrets

# ECS Task Definition - secrets injected at runtime, NOT in environment
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: my-secure-task
ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
Cpu: '256'
Memory: '512'
ContainerDefinitions:
- Name: my-container
Image: my-image:latest
Essential: true
# Only non-sensitive environment variables here
Environment:
- Name: LOG_LEVEL
Value: INFO
- Name: APP_ENV
Value: production
# Sensitive values injected from Secrets Manager
Secrets:
- Name: DB_PASSWORD
ValueFrom: !Sub '${AppSecrets}:DB_PASSWORD::'
- Name: API_KEY
ValueFrom: !Sub '${AppSecrets}:API_KEY::'
PortMappings:
- ContainerPort: 80
Protocol: tcp
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: ecs

LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /ecs/my-secure-task
RetentionInDays: 30

Outputs:
TaskDefinitionArn:
Description: ARN of the task definition
Value: !Ref TaskDefinition
SecretArn:
Description: ARN of the created secret
Value: !Ref AppSecrets

Deploy the template:

aws cloudformation deploy \
--template-file ecs-task-secrets.yaml \
--stack-name ecs-secrets-stack \
--parameter-overrides DbPassword=your-db-password ApiKey=your-api-key \
--capabilities CAPABILITY_NAMED_IAM \
--region us-east-1
Terraform (optional)
provider "aws" {
region = "us-east-1"
}

# Store secrets in Secrets Manager
resource "aws_secretsmanager_secret" "app_credentials" {
name = "myapp/prod/ecs-credentials"
description = "Credentials for ECS task"
}

resource "aws_secretsmanager_secret_version" "app_credentials" {
secret_id = aws_secretsmanager_secret.app_credentials.id
secret_string = jsonencode({
DB_PASSWORD = var.db_password
API_KEY = var.api_key
})
}

# Task execution role
resource "aws_iam_role" "task_execution_role" {
name = "my-ecs-task-execution-role"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}]
})
}

# Attach the standard ECS task execution policy
resource "aws_iam_role_policy_attachment" "task_execution_policy" {
role = aws_iam_role.task_execution_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

# Grant access to secrets
resource "aws_iam_role_policy" "secrets_access" {
name = "SecretsAccess"
role = aws_iam_role.task_execution_role.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "secretsmanager:GetSecretValue"
Resource = aws_secretsmanager_secret.app_credentials.arn
}]
})
}

# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "ecs_logs" {
name = "/ecs/my-secure-task"
retention_in_days = 30
}

# ECS Task Definition - NO secrets in environment variables
resource "aws_ecs_task_definition" "my_task" {
family = "my-secure-task"
execution_role_arn = aws_iam_role.task_execution_role.arn
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "256"
memory = "512"

container_definitions = jsonencode([
{
name = "my-container"
image = "my-image:latest"
essential = true

# Only non-sensitive environment variables
environment = [
{
name = "LOG_LEVEL"
value = "INFO"
},
{
name = "APP_ENV"
value = "production"
}
]

# Sensitive values injected from Secrets Manager at runtime
secrets = [
{
name = "DB_PASSWORD"
valueFrom = "${aws_secretsmanager_secret.app_credentials.arn}:DB_PASSWORD::"
},
{
name = "API_KEY"
valueFrom = "${aws_secretsmanager_secret.app_credentials.arn}:API_KEY::"
}
]

portMappings = [
{
containerPort = 80
protocol = "tcp"
}
]

logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.ecs_logs.name
awslogs-region = "us-east-1"
awslogs-stream-prefix = "ecs"
}
}
}
])
}

# Variables - marked as sensitive
variable "db_password" {
type = string
sensitive = true
description = "Database password to store in Secrets Manager"
}

variable "api_key" {
type = string
sensitive = true
description = "API key to store in Secrets Manager"
}

output "task_definition_arn" {
description = "ARN of the task definition"
value = aws_ecs_task_definition.my_task.arn
}

output "secret_arn" {
description = "ARN of the secret"
value = aws_secretsmanager_secret.app_credentials.arn
}

Verification

After completing the remediation:

  1. Navigate to ECS > Task Definitions > your task definition in the AWS Console
  2. Click on the latest revision
  3. Expand each container definition and verify:
    • The Environment variables section contains only non-sensitive values
    • The Secrets section shows your sensitive values referencing Secrets Manager
  4. Navigate to ECS > Clusters > your cluster > Tasks
  5. Verify new tasks are running with the updated task definition revision
  6. Check application logs to ensure secrets are being retrieved correctly
  7. Re-run the Prowler check to confirm the issue is resolved
CLI verification commands

Verify the new task definition has secrets (not environment variables):

aws ecs describe-task-definition \
--task-definition <your-task-definition> \
--region us-east-1 \
--query 'taskDefinition.containerDefinitions[*].{name:name,environment:environment,secrets:secrets}'

Verify the secret exists in Secrets Manager:

aws secretsmanager describe-secret \
--secret-id myapp/prod/ecs-credentials \
--region us-east-1

Verify running tasks use the new revision:

aws ecs list-tasks \
--cluster <your-cluster-name> \
--service-name <your-service-name> \
--region us-east-1

aws ecs describe-tasks \
--cluster <your-cluster-name> \
--tasks <task-arn> \
--region us-east-1 \
--query 'tasks[*].taskDefinitionArn'

Additional Resources

Notes

  • Task execution role is required: To inject secrets at runtime, your task definition must specify an executionRoleArn with permissions to access Secrets Manager.

  • Secrets are injected at task startup: ECS retrieves secrets from Secrets Manager when the task starts. If you update a secret, you must stop and restart tasks (or force a new deployment) to pick up the new value.

  • Secret reference format: The valueFrom field supports different formats:

    • Full secret: arn:aws:secretsmanager:us-east-1:123456789012:secret:mysecret
    • Specific JSON key: arn:aws:secretsmanager:us-east-1:123456789012:secret:mysecret:DB_PASSWORD::
    • Specific version: arn:aws:secretsmanager:us-east-1:123456789012:secret:mysecret:DB_PASSWORD:AWSCURRENT:
  • Alternative: AWS Systems Manager Parameter Store: For simpler secrets or cost savings, you can use Parameter Store with SecureString parameters. The valueFrom syntax is similar but uses the parameter ARN or name.

  • Fargate considerations: Fargate tasks must use the awsvpc network mode and require the task execution role for secrets injection.

  • Old task definition revisions remain: Creating a new revision does not delete or modify existing revisions. Ensure running services are updated to use the new revision.

  • Cost considerations: Secrets Manager charges per secret per month plus per 10,000 API calls. For high-volume workloads, consider caching strategies or Parameter Store for lower costs.