AWS Bedrock - Assume Role Setup
Configure an AWS Bedrock-backed API key using IAM role assumption instead of long-lived access keys. Applies to bedrock, anthropic_messages_bedrock, bedrock_embeddings, and bedrock_rerank providers.
Why assume role
With static keys, you paste an AWS access key and secret into the QuilrAI dashboard; those credentials live in your key's provider settings until you rotate them.
With assume role, you hand QuilrAI an IAM role ARN plus an ExternalId. At request time QuilrAI calls sts:AssumeRole on that role using its own gateway IAM user, gets short-lived credentials (default 1 hour), and uses those to call Bedrock. No long-lived AWS secrets leave your account, you can revoke access at any time by deleting or detaching the role, and every call is attributable in CloudTrail via the session name.
ExternalId is required. AWS recommends it for any cross-account role assumption scenario to prevent the confused-deputy problem, and a multi-tenant gateway assuming a customer-owned role is exactly that scenario.
What QuilrAI gives you
QuilrAI's gateway runs under a dedicated IAM user whose only permission is sts:AssumeRole. Its ARN is:
arn:aws:iam::975050335771:user/quilr-gateway
You'll reference this ARN in the trust policy of the IAM role you create below.
1. Pick an ExternalId
Generate a random, unguessable string - a UUID works. It doesn't need to be secret but it must be unique per role and not predictable.
uuidgen
# or
python -c "import uuid; print(uuid.uuid4())"
Save it - you'll paste it in two places: the role's trust policy (step 2) and the QuilrAI API key form (step 4).
2. Create the IAM role in your AWS account
AWS Console → IAM → Roles → Create role → Custom trust policy. Paste the following, substituting your ExternalId from step 1:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::975050335771:user/quilr-gateway"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "<YOUR-EXTERNAL-ID>"
}
}
}
]
}
3. Attach a Bedrock permissions policy
On the same role, attach a permissions policy granting the Bedrock actions you need. Minimum for chat:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": "arn:aws:bedrock:*::foundation-model/*"
}
]
}
The same bedrock:InvokeModel action also covers OpenAI-compatible chat through Bedrock Converse, boto3 Runtime converse / invoke_model, Titan embeddings, and Cohere / Amazon rerank models, so one role can serve all Bedrock provider types.
The arn:aws:bedrock:*::foundation-model/* resource only covers on-demand foundation models. If you use any of the following, add their ARNs to the Resource array:
- Inference profiles (required for cross-region inference on newer Claude models):
arn:aws:bedrock:*:<YOUR-ACCOUNT-ID>:inference-profile/* - Provisioned Throughput:
arn:aws:bedrock:*:<YOUR-ACCOUNT-ID>:provisioned-model/* - Custom models:
arn:aws:bedrock:*:<YOUR-ACCOUNT-ID>:custom-model/*
Name the role (e.g. quilr-gateway-bedrock), save, and copy the role ARN - it looks like:
arn:aws:iam::<YOUR-ACCOUNT-ID>:role/quilr-gateway-bedrock
4. Enter these values in the QuilrAI dashboard
When creating or updating the Bedrock API key, select Assume Role and provide:
Required
Optional
Fields to not send in assume-role mode
These belong to the static-key path and will be rejected:
aws_access_keyaws_secret_keyaws_session_token
5. Verify before going live
The fastest way to verify is to save the key in the QuilrAI dashboard and send a test request - if the role, ExternalId, and permissions are correct, it will succeed; otherwise the error surfaces directly in the dashboard and maps to the troubleshooting table below.
Optional: local CLI check
You cannot run aws sts assume-role against this role from your own terminal as written - the trust policy in step 2 pins the Principal to arn:aws:iam::975050335771:user/quilr-gateway, so AWS denies any caller that isn't the QuilrAI gateway user before it even looks at the ExternalId.
If you still want to exercise the trust policy locally, temporarily add your own IAM user or role to the Principal.AWS array:
"Principal": {
"AWS": [
"arn:aws:iam::975050335771:user/quilr-gateway",
"arn:aws:iam::<YOUR-ACCOUNT>:user/<your-iam-user>"
]
}
Then run:
aws sts assume-role \
--role-arn arn:aws:iam::<YOUR-ACCOUNT>:role/quilr-gateway-bedrock \
--role-session-name test
# Should fail with AccessDenied (ExternalId condition not satisfied).
aws sts assume-role \
--role-arn arn:aws:iam::<YOUR-ACCOUNT>:role/quilr-gateway-bedrock \
--role-session-name test \
--external-id <YOUR-EXTERNAL-ID>
# Should succeed.
If the first call succeeds, the trust policy is missing the ExternalId condition - fix it before using the role. Remove your own ARN from the Principal block before going live so only the QuilrAI gateway user can assume the role.
How the gateway uses these values at runtime
- For each Bedrock request, the gateway calls
sts:AssumeRoleon your role using its own base IAM user, passing youraws_role_arn,aws_external_id,aws_role_session_name, andaws_session_duration_seconds. - The resulting temporary credentials are cached in-process and auto-refreshed before expiry, so the STS call cost is amortized across many requests.
- The Bedrock client is built with those temporary credentials and makes the actual boto3 Runtime / Anthropic Messages / embeddings / rerank call.