Cross-Account Bedrock (Hub Account)
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: MIT-0
Cross-Account Bedrock (Hub Account)
Section titled “Cross-Account Bedrock (Hub Account)”Some enterprises require all Amazon Bedrock invocations to be routed through a single, centralized “hub” AWS account so they can centralize budget, audit, and policy controls (model allow-listing, Guardrails, application inference profiles). This page explains how to deploy the GenAI IDP Accelerator so that the workload account assumes a role in a hub account before calling Bedrock.
TL;DR: Set the
BedrockHubRoleArnparameter at deploy time. All Bedrock data-plane and control-plane traffic from IDP processing Lambdas will then route throughsts:AssumeRoleinto that role. When the parameter is left empty, behavior is unchanged.
When to use this
Section titled “When to use this”Enable the hub-role mode when any of the following apply:
- Your organization mandates that all Bedrock invocations originate from a designated AI/ML account.
- You want to centralize Bedrock model access governance, application inference profiles, or Guardrails in one account.
- You need a single CloudTrail in the hub account that records every IDP Bedrock invocation for audit.
If your IDP stack and Bedrock invocations live in the same account, leave BedrockHubRoleArn empty and ignore this page.
How it works
Section titled “How it works”When BedrockHubRoleArn is set:
- Each processing Lambda’s execution role is granted
sts:AssumeRolepermission on the configured ARN. - The Lambda receives
BEDROCK_ASSUME_ROLE_ARN,BEDROCK_ASSUME_ROLE_EXTERNAL_ID, andBEDROCK_ASSUME_ROLE_SESSION_NAMEenvironment variables. - The
idp_common.bedrock.session.get_bedrock_session()factory builds a single boto3 session withbotocore.credentials.DeferredRefreshableCredentials. This means STS credentials are automatically refreshed before they expire — warm Lambda containers don’t break after the default 1-hour STS session. - Every Bedrock client (
bedrock-runtime,bedrockcontrol-plane, embedding model invocations, StrandsBedrockModelfor agentic extraction) is created from this shared session.
When BedrockHubRoleArn is empty:
- No new IAM permissions are attached.
- No new env vars are set on Lambdas.
get_bedrock_session()returns a vanillaboto3.Session— identical to legacy behavior.
Coverage
Section titled “Coverage”The following Bedrock call paths route through the hub role when configured:
- ✅ Pipeline mode (Pattern 2): classification, extraction (traditional + agentic), assessment, summarization, evaluation, OCR (Bedrock OCR backend), discovery (classes, rules, multi-doc), embeddings, CachePoint inference-profile resolution.
- ✅ Strands agentic extraction in
idp_common/extraction/agentic_idp.py(uses the same factory viaBedrockModel(boto_session=...)). - ✅ Multi-doc discovery nested stack (Embed, Cluster, Analyze, Save Lambdas).
- ✅ Chat with Document — the streaming chat-with-document processor (
src/lambda/chat_with_document_processor). - ✅ Agent Companion — the agent chat processor (
src/lambda/agent_chat_processor) and agent processor (src/lambda/agent_processor). These build sub-agent boto3 sessions for connection isolation; the sharedcreate_strands_bedrock_model()helper detectsBEDROCK_ASSUME_ROLE_ARNand overrides those sessions for Bedrock calls only — workload-account AWS calls (DynamoDB, S3, Athena, Glue, AppSync) continue to use local credentials. - ⏸️ Bedrock Knowledge Bases (
query_knowledgebase_resolver): not yet covered. The KB resolver uses thebedrock-agent-runtimeservice which has a different identity surface; cross-account KB warrants a separate design. - ⏸️ BDA (formerly Pattern 1): out of scope for v1. Cross-account BDA has its own model (project ARNs, BDA service roles) and warrants a separate design. If your deployment uses BDA mode (
use_bda: true), the BDA runtime calls remain in the calling account. - ⏸️ Model fine-tuning utilities (
idp_common/model_finetuning/): out of scope for v1.
Required deployment parameters
Section titled “Required deployment parameters”| Parameter | Required | Description |
|---|---|---|
BedrockHubRoleArn | Yes | ARN of the role to assume in the hub account, e.g. arn:aws:iam::111122223333:role/IDPBedrockHubRole. |
BedrockHubRoleExternalId | Optional | Pass through to sts:AssumeRole as ExternalId. Required when the hub-account role’s trust policy enforces an ExternalId. Set as NoEcho. |
BedrockHubRoleSessionName | Optional | Custom STS RoleSessionName (max 64 chars; allowed characters: A-Z a-z 0-9 + = , . @ - _). Defaults to the calling Lambda’s function name for CloudTrail attribution. |
Hub-account role: example trust policy
Section titled “Hub-account role: example trust policy”In the hub account, create a role (for example IDPBedrockHubRole) whose trust policy allows the workload account’s IDP processing Lambda execution roles to assume it:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::WORKLOAD_ACCOUNT_ID:root" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "sts:ExternalId": "your-shared-external-id" } } } ]}Tighten the Principal to a specific role ARN (e.g. the IDP function role) once you know it, instead of using the account root.
Hub-account role: example permission policy
Section titled “Hub-account role: example permission policy”Attach the Bedrock permissions to the hub-account role:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream", "bedrock:GetInferenceProfile", "bedrock:ApplyGuardrail" ], "Resource": [ "arn:aws:bedrock:*::foundation-model/*", "arn:aws:bedrock:*:HUB_ACCOUNT_ID:inference-profile/*", "arn:aws:bedrock:*:HUB_ACCOUNT_ID:application-inference-profile/*", "arn:aws:bedrock:*:HUB_ACCOUNT_ID:guardrail/*" ] }, { "Comment": "Required only if using OpenAI GPT-5.x (bedrock-mantle Responses API)", "Effect": "Allow", "Action": [ "bedrock-mantle:CreateInference", "bedrock-mantle:GetProject", "bedrock-mantle:ListProjects", "bedrock-mantle:ListTagsForResources" ], "Resource": "*" } ]}Resource-scoping caveats
Section titled “Resource-scoping caveats”When the hub role is enabled, some Bedrock resource identifiers must refer to the hub account, not the workload account:
| Resource type | Where it must live |
|---|---|
System-defined cross-region inference profiles (us.*, eu.*, global.*) | Either account — these resolve from any account with model access. |
Application inference profile ARNs (configured via the IDP model field) | Hub account — application inference profiles are account-scoped. |
Bedrock Guardrail IDs (BedrockGuardrailId) | Hub account — Guardrails are account-scoped. |
| Bedrock Knowledge Base IDs | Hub account if you want them invoked via the assumed role. |
If you set BedrockGuardrailId in the workload account but enable the hub role, the Guardrail call will fail because the assumed role does not have access to the workload-account Guardrail. Move the Guardrail to the hub account, or leave the hub role disabled.
Private (VPC-secured) deployments
Section titled “Private (VPC-secured) deployments”If you also use UsePrivateAppSync=true or otherwise deploy IDP into a VPC, ensure the VPC has an STS interface VPC endpoint in addition to the existing Bedrock and AppSync endpoints. Without it, sts:AssumeRole calls from the Lambda cannot reach the AWS STS regional service.
See Deployment in a Private Network for the full list of required VPC endpoints.
How to verify
Section titled “How to verify”After deploying with BedrockHubRoleArn set, run through the following checklist to confirm cross-account routing is in effect end-to-end.
1. Backwards-compatibility test (most important)
Section titled “1. Backwards-compatibility test (most important)”Deploy with BedrockHubRoleArn="" (or omit it). All existing flows should behave exactly as before. If extraction, classification, summarization, evaluation, discovery, chat-with-document, and agent companion all work, the additive change is confirmed safe.
2. CloudTrail in the hub account — AssumeRole calls
Section titled “2. CloudTrail in the hub account — AssumeRole calls”Each Lambda invocation should produce an AssumeRole event in the hub account. Note that lookup-events returns the top-level Username as null for cross-account AssumeRole events — the source account and session name live inside the embedded CloudTrailEvent JSON. Decode it with a small Python snippet:
aws cloudtrail lookup-events \ --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \ --max-results 20 \ --region <region-where-stack-is-deployed> \ --output json --profile hub | python3 -c "import json, sysfor e in json.load(sys.stdin)['Events']: ct = json.loads(e['CloudTrailEvent']) req = ct.get('requestParameters') or {} role_arn = req.get('roleArn', '') if 'IDPBedrockHubRole' not in role_arn: # adjust to your role name continue src_acct = ct.get('userIdentity', {}).get('accountId', '?') print(f\"{e['EventTime']} src={src_acct} session={req.get('roleSessionName','?')}\")"You should see one event per Lambda invocation, with RoleSessionName matching the Lambda function name (e.g. mystack-ExtractionFunction-abc123). This proves per-Lambda CloudTrail attribution is working.
3. CloudTrail in the hub account — Bedrock data-plane calls
Section titled “3. CloudTrail in the hub account — Bedrock data-plane calls”The actual Bedrock invocations should show up under the assumed role’s session principal:
aws cloudtrail lookup-events \ --lookup-attributes AttributeKey=EventName,AttributeValue=Converse \ --max-results 20 --profile hub | \ jq '.Events[].CloudTrailEvent | fromjson | .userIdentity.sessionContext.sessionIssuer.userName'4. Negative test: workload account has no Bedrock permissions
Section titled “4. Negative test: workload account has no Bedrock permissions”Strip the local bedrock:InvokeModel from the workload account (or apply a permissions boundary that blocks Bedrock). With BedrockHubRoleArn set, processing should still succeed via the assumed role — proving the traffic actually transits the hub.
5. Warm-Lambda credential refresh
Section titled “5. Warm-Lambda credential refresh”The implementation uses DeferredRefreshableCredentials so STS credentials auto-refresh before the 1-hour expiry. Two ways to verify:
- Trigger one of the processing Lambdas, wait > 60 minutes, trigger again. A successful call after the wait proves the refresher worked.
- Look at the hub-account CloudTrail for multiple
AssumeRoleevents from a single Lambda invocation lifetime (each refresh triggers a newAssumeRole).
6. Coverage matrix — exercise each surface
Section titled “6. Coverage matrix — exercise each surface”| Surface | How to exercise it | Expected RoleSessionName (CloudTrail) |
|---|---|---|
| Pipeline (extraction, classification, summarization, evaluation, OCR, assessment) | Upload a sample document via the IDP UI. | *-ClassificationFunction-*, *-ExtractionFunction-*, *-AssessmentFunction-*, *-SummarizationFunction-*, etc. |
| Discovery | Run a discovery job from the UI. | *-MultiDocDiscoveryEmbedFunction-*, *-AnalyzeFunction-*, *-SaveFunction-* |
| Chat with Document | Open a processed document and use the “Chat with Document” feature. | *-ChatWithDocumentProcessor-* |
| Agent Companion / Agent Chat | Open the agent chat panel and ask an analytics question. | *-AgentChatProcessor-* (sub-agents reuse the same session, so they appear under the same RoleSessionName). |
| Agent processor | Trigger an agent invocation from the UI. | *-AgentProcessor-* |
| Strands agentic extraction | Set extraction.agentic.enabled: true in the configuration and run extraction on a document with large tables. | *-ExtractionFunction-* |
7. Failure-mode probes
Section titled “7. Failure-mode probes”Force-fail each likely misconfiguration to verify error messages are useful:
- Bad ARN: deploy with a non-existent role ARN — Lambdas should fail with a clear
AccessDeniedonsts:AssumeRole. - Missing ExternalId: enable an
ExternalIdrequirement in the trust policy, but unsetBedrockHubRoleExternalIdin the stack. Confirm the failure points atExternalId. - Application inference profile in wrong account: set the IDP
modelfield to an application inference profile ARN that lives in the workload account while the hub role is enabled. Confirm the failure clearly indicates the resource is not accessible from the assumed role.
Operational considerations
Section titled “Operational considerations”- Credential expiry: STS session credentials default to 1 hour. The session factory uses
DeferredRefreshableCredentialsso credentials are refreshed transparently before expiry. No code changes are required to your service classes. - Per-Lambda CloudTrail attribution: STS
RoleSessionNamedefaults to the Lambda’sAWS_LAMBDA_FUNCTION_NAME. Searching CloudTrail in the hub account foruserIdentity.sessionContext.sessionIssuer.userNameoruserIdentity.principalIdwill surface which IDP function made the call. - Backward compatibility: setting
BedrockHubRoleArn=""(the default) is fully backward-compatible. The localbedrock:InvokeModelIAM statements remain attached to processing Lambdas and continue to work.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause |
|---|---|
AccessDenied on sts:AssumeRole from the Lambda execution role itself (error message references User: arn:aws:sts::<workload-acct>:assumed-role/... is not authorized) | The Lambda execution role is missing the sts:AssumeRole IAM stmt. Most common root cause: the BedrockHubRoleArn parameter was deployed with the role ARN and the ExternalId concatenated into a single string (e.g. arn:...:role/Foo ExternalId: xyz). The IAM stmt is then synthesized with a corrupt Resource. Fix: redeploy the stack passing BedrockHubRoleArn and BedrockHubRoleExternalId as separate parameters. Verify with aws iam get-role-policy ... --query 'PolicyDocument.Statement[?Action==\sts:AssumeRole`]’`. |
AccessDenied on sts:AssumeRole blamed on the trust policy (error message references the resource role ARN, not the caller) | Hub-account trust policy missing the workload account/role; missing ExternalId; permissions boundary in the workload account blocking AssumeRole. |
AccessDenied on bedrock:InvokeModel after AssumeRole succeeds | Hub-account role missing model access (e.g., not allow-listed for the requested foundation model). |
AccessDenied on bedrock:GetInferenceProfile | The inference profile is in the workload account, not the hub account. Move it, or use a system-defined cross-region profile. |
AccessDenied on bedrock:ApplyGuardrail | The Guardrail is in the workload account. Move it to the hub account or unset BedrockGuardrailId. |
| Errors after ~1 hour of warm Lambda activity | Should not happen — credentials auto-refresh. If it does, file a bug; the factory may have been bypassed. |
EndpointConnectionError on sts.<region>.amazonaws.com from a VPC Lambda | Missing STS interface VPC endpoint. |
Implementation notes
Section titled “Implementation notes”- Single source of truth:
lib/idp_common_pkg/idp_common/bedrock/session.py. - The factory is process-cached per region; same-process callers share the session and credential refresher.
BedrockClient’slambda_clientands3_clientproperties intentionally use the calling-account session — Lambda invocations and S3 operations stay local.