AWS role based access (role chaining) allows your embedded service to access AWS resources in customer accounts through a sequence of temporary credential assumptions:
Embedded Service → Assumer Role → Customer Role → AWS Resources
(ROOT) (Your AWS) (Customer AWS)Role chaining eliminates the need for customers to share long-lived access keys. Instead, each role in the chain uses temporary credentials with scoped permissions, following the principle of least privilege. All role assumptions are logged in CloudTrail for auditability, and customers retain full control; they can revoke access at any time by modifying their role's trust policy.
Before configuring role chaining, you need an AWS account where you can host your Assumer Role, along with IAM permissions to create roles and policies. The embedded service should already be deployed and running, and you should have access to modify its feature flags and startup configuration.
These steps are performed by the team deploying the embedded service.
The ROOT identity is what your embedded service uses to authenticate with AWS. For development and testing, an IAM User is sufficient. For production deployments, it is best practice to assume an IAM Role through workload identity. Some examples of workload identity authentication include EC2 instance roles, ECS task roles, EKS IRSA, and EKS Pod Identity.
Create a policy that allows assuming your Assumer Role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::YOUR_ACCOUNT_ID:role/YOUR_ASSUMER_ROLE_NAME"
}
]
}Attach this policy to your ROOT user or role.
The Assumer Role is a public-facing role that customers reference in their trust policies. Create this role in your AWS account.
Trust Policy (allows ROOT to assume this role):
If using an IAM Role as ROOT:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::YOUR_ACCOUNT_ID:role/YOUR_ROOT_ROLE"
},
"Action": "sts:AssumeRole"
}
]
}If using an IAM User as ROOT:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::YOUR_ACCOUNT_ID:user/YOUR_ROOT_USER"
},
"Action": "sts:AssumeRole"
}
]
}Permissions Policy (allows assuming customer roles):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::*:role/*"
}
]
}You can restrict the
Resourceto a specific naming pattern (e.g.,arn:aws:iam::*:role/YourCompanyAccess*) for additional security.
Note the ARN of this role - you'll need it for service configuration and to provide to customers.
Start the embedded service with the Assumer Role ARN and enable the feature flag:
./embedded \
--aws-assumer-role arn:aws:iam::YOUR_ACCOUNT_ID:role/YOUR_ASSUMER_ROLE_NAME \
--features '{"aws_role_based_access_enabled": {"value": true}}'Alternatively, use a features file:
./embedded \
--aws-assumer-role arn:aws:iam::YOUR_ACCOUNT_ID:role/YOUR_ASSUMER_ROLE_NAME \
--features-file /path/to/features.jsonWhere features.json contains:
{
"aws_role_based_access_enabled": {
"value": true
}
}Provide these instructions to customers who want to connect their AWS accounts.
Customers create a role in their AWS account with a trust policy that allows your Assumer Role to assume it.
Trust Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "ASSUMER_ROLE_ARN"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "CUSTOMER_EXTERNAL_ID"
}
}
}
]
}In this policy, replace ASSUMER_ROLE_ARN with the Assumer Role ARN provided by your integrator, and CUSTOMER_EXTERNAL_ID with a unique identifier you generate (typically a UUID). You will provide both your role ARN and ExternalId to your integrator during onboarding or when completing the AWS provider configuration. The ExternalId is critical for security—see Security Considerations below.
Customers attach a permissions policy to their role granting access to the required AWS services. Example for S3 read access:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::their-bucket-name", "arn:aws:s3:::their-bucket-name/*"]
}
]
}When advising customers on permissions, recommend they follow the principle of least privilege by granting only the permissions necessary for your integration. This limits the blast radius if credentials are ever compromised and makes security audits straightforward.
The ExternalId is a critical security mechanism that prevents the confused deputy problem. Without it, a malicious actor could create a role in their own account that trusts your Assumer Role, then trick your service into accessing another customer's resources by providing the wrong role ARN. Your service would unknowingly act as a proxy for the attacker.
The ExternalId solves this by requiring a unique secret that must match in both your sts:AssumeRole API call and the customer's trust policy condition. When customers onboard, they generate their own unique ExternalId (typically a UUID), embed it in their role's trust policy, and provide it to you along with their role ARN. Since only the legitimate customer knows their ExternalId, attackers cannot trick your service into assuming roles on their behalf.
Use the AWS CLI to verify the role chain works end-to-end:
# Step 1: Assume the Assumer Role
ASSUMER_CREDS=$(aws sts assume-role \
--role-arn "arn:aws:iam::YOUR_ACCOUNT:role/ASSUMER_ROLE" \
--role-session-name "test-assumer" \
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' \
--output text)
export AWS_ACCESS_KEY_ID=$(echo $ASSUMER_CREDS | awk '{print $1}')
export AWS_SECRET_ACCESS_KEY=$(echo $ASSUMER_CREDS | awk '{print $2}')
export AWS_SESSION_TOKEN=$(echo $ASSUMER_CREDS | awk '{print $3}')
# Step 2: Assume the Customer Role
aws sts assume-role \
--role-arn "arn:aws:iam::CUSTOMER_ACCOUNT:role/CUSTOMER_ROLE" \
--role-session-name "test-customer" \
--external-id "CUSTOMER_EXTERNAL_ID"This error typically indicates a break in the trust chain. The ROOT identity may lack permission to assume the Assumer Role, the Assumer Role's trust policy may not allow ROOT, or the customer's role trust policy may not allow your Assumer Role.
To resolve this, verify that Principal ARNs in all trust policies match exactly and that permission policies include sts:AssumeRole for the target role ARN.
This error occurs when the ExternalId in your sts:AssumeRole API call doesn't match the value in the customer's trust policy, or when the ExternalId condition is missing from the trust policy entirely.
Verify that the ExternalId matches exactly—it is case-sensitive—and ensure the customer's trust policy includes the sts:ExternalId condition.
This error means the requested session duration exceeds the role's configured maximum. Either reduce the duration in your configuration, or have the customer increase their role's MaxSessionDuration setting (up to 12 hours).
If you see the message "AWS role-based access is disabled: configure feature flag 'aws_role_based_access_enabled' to enable", the embedded service was not started with role chaining enabled. Restart the service with the --features or --features-file flag to enable aws_role_based_access_enabled.