Bedrock inference profiles route cross-region, and your SCP will block it
I enabled Bedrock for developers on a Control Tower account locked to us-east-1. Direct model invocations worked fine — claude-3-haiku responded immediately. Then someone tried us.anthropic.claude-sonnet-4-6 and got AccessDenied. Same account, same IAM role, same region. The only difference was the model identifier.
The us. prefix on the model name isn’t a region alias. It’s a US inference profile, and inference profiles do dynamic capacity routing. When you call us.anthropic.claude-sonnet-4-6 in us-east-1, Bedrock tries to serve it locally first. If capacity is tight, it routes the request to us-east-2 or us-west-2 over the AWS backbone — per request, dynamically, based on regional load and latency. No additional cost, no data stored in the destination region, no public internet traversal.
The newer Claude models (3.5 onward) require inference profiles because AWS needs the cross-region capacity pool to maintain availability guarantees. Older models like Haiku 3 still work on-demand in a single region because they have enough dedicated capacity there. So direct invocations passed, and inference profiles failed, because they’re fundamentally different routing strategies.
GRREGIONDENY sees the hop
Control Tower’s region deny SCP — the one everybody has — fires on aws:RequestedRegion. When Bedrock routes a request to us-east-2 for capacity, that cross-region hop looks like an API call in us-east-2 to the SCP evaluator. The deny fires, and the caller sees AccessDenied with no indication that the issue is regional routing rather than a permissions gap.
I’ve written before about how a separate Allow SCP can extend Control Tower’s guardrails — it works for ELB, SES, and other services whose API endpoints live in your home region even when the resources don’t. The Allow SCP adds the service to the “not denied” set because the SCP evaluator still sees aws:RequestedRegion = us-east-1.
Bedrock inference profiles break that pattern. The cross-region hop is a real API call in us-east-2, not a global endpoint served from us-east-1. The SCP evaluator sees aws:RequestedRegion = us-east-2 and the deny fires on the region condition, not on the action. An Allow SCP for bedrock:* adds Bedrock to the allowed actions, but the deny isn’t targeting the action — it’s targeting the region. Different evaluation path entirely.
I added bedrock:* to the NotAction list in GRREGIONDENY itself, which exempts Bedrock from region restrictions entirely. But SCPs have a hard 5,120 character limit, and adding another service pushed us over.
The policy looked like this (37 characters):
{"NotAction": ["iam:*", "sts:*"]}
Every comma and colon has a trailing space. Multiply that across 40+ actions and half a dozen condition blocks and it adds up. json.dumps(policy, separators=(',', ':')) strips those spaces:
{"NotAction":["iam:*","sts:*"]}
31 characters. Same policy, six fewer characters in two lines. Across the full GRREGIONDENY document, that reclaimed enough room for bedrock:*.
CloudTrail tells a different story too
Direct model invocations log a modelId like claude-3-haiku-20240307. Inference profiles log a full ARN: arn:aws:bedrock:us-east-1::inference-profile/us.anthropic.claude-sonnet-4-6-20250514. If your cost enrichment Lambda is parsing modelId to attribute spend per model, inference profiles show up as unrecognized entries until you normalize the ARN back to a model name.
CloudTrail also records additionalEventData.inferenceRegion, which tells you which region actually handled each request. I’m adding that to the cost enrichment pipeline so I can see the routing distribution — whether requests fan out evenly or pile into one region.
Inference profiles are a capacity abstraction, not a deployment target. If your organization restricts regions via SCPs, Bedrock’s dynamic routing will collide with that restriction silently. The AccessDenied gives you no hint that region policy is the cause — it looks like a straightforward permissions failure on a service you already enabled.