An AWS principal can sometimes submit work that later executes under a different role. If the submitted code can call AWS APIs, the service execution role becomes an action proxy. The original principal remains visible on the job-submission event, while downstream calls identify the execution-role session.

SageMaker’s Python SDK @remote feature is the validated implementation. It packages local Python as a training job, SageMaker assumes the selected execution role, and the remote code uses that role’s temporary credentials for Boto3 calls.

The technique shifts downstream activity into a service execution-role session; it does not make the submitting principal invisible.

Technique

The attacker crosses an identity boundary by passing a role to AWS-managed execution. One identity authorizes the workload submission. The service then assumes a second identity to run the workload. API calls made inside that workload inherit the second identity’s permissions and attribution.

flowchart LR
    A([Abused principal])
    W[Attacker-controlled workload]
    S[AWS-managed execution]
    R([Service execution-role session])
    D[Downstream AWS API]
    A -->|submits| W
    W --> S
    S -->|assumes passed role| R
    R --> D
    class A,R principal
    class W,S,D awsResource
The submission remains attributable to the original principal. Downstream actions execute under the role assumed by the service.

Required properties

  1. The initiating principal can submit attacker-controlled work.
  2. The service accepts a role that it can assume for execution.
  3. The execution environment can reach and call downstream AWS APIs.
  4. The execution role grants useful permissions.
  5. Defenders do not already correlate the submission with that role’s subsequent activity.

Implementations

Implementation Validation Submitted work Execution identity Downstream channel Key tradeoff
SageMaker Python SDK @remote Validated Serialized Python function and dependencies in a training job Passed SageMaker execution role Boto3 from the training container Visible job creation, S3 staging, startup delay, logs, and compute cost.

No other service is presented as validated. Services that accept jobs and execution roles are research leads until their code control, credential context, network behavior, and CloudTrail attribution are tested.

How the implementation works

eventSource identifies the AWS service that received an API call. A CreateTrainingJob event therefore has eventSource: sagemaker.amazonaws.com even when a compromised IAM user or role made the request. The event’s userIdentity still records that caller. AWS documents that SageMaker control-plane calls are CloudTrail management events and that their identity fields show who generated each request.[1]

The remote function does not make arbitrary downstream events become userIdentity.type: AWSService. Python code in the training container signs Boto3 requests with temporary credentials for the execution role. A call to S3 therefore has eventSource: s3.amazonaws.com and identifies the assumed execution role in userIdentity.

userIdentity.invokedBy: sagemaker.amazonaws.com has a narrower meaning. CloudTrail includes invokedBy when an AWS service made the request, including requests made through service principals, service roles, service-linked roles, or forward access sessions. When this field appears on SageMaker-related activity, it describes the service-mediated request path. It does not erase the role ARN or prove that an arbitrary SDK call originated from SageMaker service code.[2]

The technique therefore does not fully hide the abused principal. It creates an attribution break:

  • The initiating identity remains visible on CreateTrainingJob.
  • Downstream API calls are attributed to the selected execution role.
  • Some service-managed calls can additionally identify sagemaker.amazonaws.com through invokedBy.
  • Correlation is required to reconnect the training job to its submitter.

Offensive value and limits

The technique is useful when the SageMaker execution role is more privileged than the abused caller, when its activity is treated as expected ML automation, or when calls from SageMaker network paths receive less scrutiny. It also gives the attacker AWS-managed compute and an S3-backed request and response channel without maintaining a separate host.

The current AmazonSageMakerFullAccess managed policy makes role selection particularly broad. It grants iam:PassRole on arn:aws:iam::*:role/* when the destination service is sagemaker.amazonaws.com. A principal with that policy can pass any role in the account that trusts SageMaker, rather than only a designated execution role.[3]

Operational costs are substantial. Each invocation creates a visible training job, requires startup time, moves artifacts through S3, writes training logs, and incurs compute charges. The technique is weak when the execution role has no additional privileges, network isolation blocks direct service access, or defenders correlate CreateTrainingJob with subsequent execution-role activity. It displaces attribution rather than removing it.

Execution requirements

The abused principal requires:

  • sagemaker:CreateTrainingJob.
  • sagemaker:DescribeTrainingJob to receive job state and results, plus sagemaker:ListTrainingJobs when existing jobs are used to discover plausible roles and configurations.
  • iam:PassRole for a same-account execution role trusted by sagemaker.amazonaws.com.[4]
  • Read and write access to the S3 remote-function staging location used for serialized code, arguments, and results.[5]
  • Access to any caller-side KMS key or other resource required by the selected configuration.

The execution role requires:

  • A trust policy allowing sagemaker.amazonaws.com to call sts:AssumeRole.
  • Read and write access to the remote-function staging objects.
  • Permissions required to pull the configured container image and write training logs.[4:1]
  • Access to any KMS keys used for staging data or storage volumes.
  • Permissions for each downstream API operation.
  • Network reachability to the target AWS service endpoints when the function makes the calls directly.

The technique loses its arbitrary API-proxy behavior when training network isolation is enabled. AWS documents that EnableNetworkIsolation prevents the training container from making inbound or outbound network calls. SageMaker can still move configured input and output data, but the Python process cannot directly reach arbitrary AWS endpoints.[6]

The selected Region must also have quota and capacity for the requested training instance. A remote-function-compatible Python environment and container image are required; reusing an arbitrary image found through DescribeTrainingJob is not sufficient.

Execution

Existing jobs expose role ARNs, images, output locations, VPC settings, and other configuration that can guide selection of a plausible execution role and staging pattern.[7] An existing image is reusable only when it is compatible with the remote-function runtime.

import boto3

REGION = "us-east-1"
sm = boto3.client("sagemaker", region_name=REGION)

jobs = sm.list_training_jobs(
    SortBy="CreationTime",
    SortOrder="Descending",
    MaxResults=20,
)

for summary in jobs["TrainingJobSummaries"]:
    job = sm.describe_training_job(
        TrainingJobName=summary["TrainingJobName"]
    )
    print(
        job["TrainingJobName"],
        job["RoleArn"],
        job["AlgorithmSpecification"].get("TrainingImage"),
        job["OutputDataConfig"]["S3OutputPath"],
        job.get("VpcConfig"),
    )

This enumeration is not required when the role and staging location are already known. iam:PassRole still controls whether the selected role can be attached to the new job.

Create requirements.txt in the local Python environment:

sagemaker
boto3

The proxy below accepts a batch of Boto3 operations. Batching avoids the repeated startup delay and repeated CreateTrainingJob events caused by launching one training job per API call. S3_ROOT_URI must be writable by the submitting principal and readable and writable by the execution role.

import boto3
from sagemaker.core.helper.session_helper import Session
from sagemaker.remote_function import remote

REGION = "us-east-1"
ROLE_ARN = (
    "arn:aws:iam::111122223333:"
    "role/SageMakerRemoteExecutionRole"
)
S3_ROOT_URI = (
    "s3://sagemaker-us-east-1-111122223333/"
    "remote-function"
)

sm_session = Session(
    boto_session=boto3.Session(region_name=REGION)
)


@remote(
    sagemaker_session=sm_session,
    role=ROLE_ARN,
    instance_type="ml.m5.xlarge",
    dependencies="./requirements.txt",
    s3_root_uri=S3_ROOT_URI,
)
def aws_api_batch(calls, region):
    import boto3

    results = []
    for call in calls:
        try:
            client = boto3.client(
                call["service"],
                region_name=region,
            )
            method = getattr(client, call["operation"])
            response = method(**call.get("parameters", {}))

            body = response.get("Body")
            if body is not None and hasattr(body, "read"):
                response["Body"] = body.read()

            results.append(
                {
                    "service": call["service"],
                    "operation": call["operation"],
                    "response": response,
                }
            )
        except Exception as error:
            results.append(
                {
                    "service": call["service"],
                    "operation": call["operation"],
                    "error": (
                        f"{type(error).__name__}: {error}"
                    ),
                }
            )

    return results


calls = [
    {
        "service": "sts",
        "operation": "get_caller_identity",
    },
    {
        "service": "s3",
        "operation": "list_buckets",
    },
]

for result in aws_api_batch(calls, REGION):
    print(result)

GetCallerIdentity returns the execution-role session, not the identity running the local script. Each following call uses the same training job and execution-role credential set. Method names use Boto3’s snake-case client methods, and parameters maps directly to the selected method’s keyword arguments.

Responses must be serializable because the remote decorator returns them through S3. The example consumes a response Body stream before returning it. Operations that return other streaming handles or very large data should process or store those results inside the remote function instead of returning the raw response.

The serialized function, arguments, results, exceptions, and workspace archive occupy predictable subpaths beneath the generated job-name prefix.[5:1] S3 data events expose access to those keys but do not record the object contents. The operation list is therefore not directly visible in CloudTrail unless the downstream service events reveal it.

The method receives only the permissions of SageMakerRemoteExecutionRole. Passing a role does not grant permissions that the role lacks. The caller’s iam:PassRole scope determines which execution roles can be selected.

The decorator translates the local workspace and function into a SageMaker training job. From a local IDE, AWS requires the role setting to name the execution role SageMaker will use.[8] The underlying CreateTrainingJob request exposes that ARN in requestParameters.roleArn.

Boundaries and failure conditions

The technique does not erase the initiating identity. CreateTrainingJob still records the submitter and selected roleArn. It fails when iam:PassRole cannot select a useful role, the role lacks downstream permissions, training network isolation blocks outbound API calls, staging access is unavailable, or defenders correlate job lifetime with execution-role activity.

SageMaker startup time, S3 artifacts, training logs, and compute cost make this a visible and comparatively heavy proxy. Its value is the identity transition and managed execution context, not invisibility.

Detection

The behavioral invariant is a principal submitting work, a service assuming the supplied execution role, and downstream AWS actions occurring under that role during the workload lifetime. Detect the handoff as a relationship, not by allowlisting sagemaker.amazonaws.com.

A focused job-submission event has this shape:

{
  "eventSource": "sagemaker.amazonaws.com",
  "eventName": "CreateTrainingJob",
  "userIdentity": {
    "type": "AssumedRole",
    "arn": "arn:aws:sts::111122223333:assumed-role/DeveloperRole/alice",
    "sessionContext": {
      "sessionIssuer": {
        "arn": "arn:aws:iam::111122223333:role/DeveloperRole"
      }
    }
  },
  "sourceIPAddress": "198.51.100.24",
  "requestParameters": {
    "trainingJobName": "remote-function-2026-09-01-120000",
    "roleArn": "arn:aws:iam::111122223333:role/SageMakerRemoteExecutionRole",
    "enableNetworkIsolation": false,
    "resourceConfig": {
      "instanceType": "ml.m5.xlarge",
      "instanceCount": 1
    }
  },
  "eventType": "AwsApiCall",
  "managementEvent": true
}

The example keeps only fields needed to identify the submitter, execution role, network-isolation state, and compute request. Actual remote-function job names and request details depend on the SDK version and configuration.

Detection inputs

Event source and name Fields Why it matters
sagemaker.amazonaws.com and ListTrainingJobs Caller identity, source address, request filters and pagination token Shows training-job discovery used to identify established roles and configurations.
sagemaker.amazonaws.com and CreateTrainingJob Caller identity, source address, trainingJobName, roleArn, image, input and output S3 URIs, enableNetworkIsolation, VPC configuration Establishes who created the remote execution context and which role it received.
sagemaker.amazonaws.com and DescribeTrainingJob Caller identity and training-job name Shows polling associated with synchronous @remote execution.
sagemaker.amazonaws.com and StopTrainingJob Caller identity and training-job name Shows manual termination of the proxy job.
s3.amazonaws.com and staging-object operations Bucket, key, caller identity, source address, user agent; paths under workdir/workspace.zip, function/, arguments/, results/, and exception/ Identifies the remote-function staging structure when S3 data events are enabled. CloudTrail does not contain the serialized object payload.
Any downstream service event Execution-role ARN, access key, source address, user agent, invokedBy, operation, resource Captures the actions performed from the training job.

Correlation logic

  1. Select CreateTrainingJob events and retain the caller identity, requestParameters.roleArn, job name, event time, source address, image, S3 locations, network-isolation state, and VPC configuration.
  2. Flag callers that do not normally create training jobs, execution roles not previously passed by that caller, and jobs with network isolation disabled outside the approved baseline.
  3. Find downstream events whose userIdentity.sessionContext.sessionIssuer.arn equals the submitted roleArn.
  4. Restrict the time window to the training job’s lifetime and group activity by execution-role access key, Region, source address, and user agent.
  5. Raise severity for APIs unrelated to the role’s established ML workload, especially identity, secrets, organization, logging, security-control, or cross-service discovery operations.
  6. Treat invokedBy: sagemaker.amazonaws.com as context, not a reason to suppress the event.
  7. Correlate CloudWatch training-job logs and S3 staging-object data events with the job name when those telemetry sources are available.

The highest-confidence signal is a novel caller-to-execution-role pairing followed by downstream actions that the execution role has not historically performed.

Hardening

Hardening must remove workload submission, useful role selection, downstream network access, execution-role privilege, or the attribution gap between submission and downstream activity.

Control Implementation Why it helps
Training-job creation Grant sagemaker:CreateTrainingJob only to identities that submit training workloads. Separate interactive development from automated job submission. Removes the API used to create remote execution contexts.
PassRole scope Restrict iam:PassRole to exact SageMaker execution-role ARNs and retain the iam:PassedToService condition for sagemaker.amazonaws.com. Prevents a caller from selecting a more privileged execution role.
Execution-role scope Give each workload an execution role limited to its required data, APIs, KMS keys, logs, and image repositories. Limits what remote Python can do after the job starts.
Network isolation Set EnableNetworkIsolation for jobs that do not require container-initiated network access. Prevents arbitrary Boto3 calls from the training container.
VPC egress Place jobs that require networking in controlled subnets and restrict endpoint policies, routes, DNS, security groups, and proxy destinations. Narrows the services and external systems reachable by remote code.
Staging storage Use a dedicated S3 prefix for remote-function artifacts and restrict both writers and readers. Enable data events for that prefix. Exposes and limits serialized code, arguments, dependencies, and results.
Job configuration Constrain approved images, instance types, VPC settings, KMS keys, tags, and S3 locations through IAM conditions and deployment policy. Reduces attacker control over the execution environment.
Correlated monitoring Join SageMaker management events to downstream execution-role activity instead of evaluating either stream alone. Restores the attribution link the technique attempts to separate.

References


  1. Amazon SageMaker AI Developer Guide, Logging Amazon SageMaker AI API calls using AWS CloudTrail. ↩︎

  2. AWS CloudTrail User Guide, CloudTrail userIdentity element. ↩︎

  3. AWS Managed Policy Reference, AmazonSageMakerFullAccess. ↩︎

  4. Amazon SageMaker AI Developer Guide, SageMaker Python SDK Troubleshooting Guide. ↩︎ ↩︎

  5. Amazon SageMaker AI Developer Guide, Invoke a remote function. ↩︎ ↩︎

  6. Amazon SageMaker AI API Reference, CreateTrainingJob. ↩︎

  7. Amazon SageMaker AI API Reference, DescribeTrainingJob. ↩︎

  8. Amazon SageMaker AI Developer Guide, Run your local code as a SageMaker training job. ↩︎