bedrock:* on Resource: "*" is the policy almost every Bedrock tutorial hands you, and it grants a role the ability to invoke every model in the catalogue, create and delete agents, and read every knowledge base in the account. Scoping it down is not hard, but it has one trap that makes the obvious version fail at runtime.
The actions you actually need
An application that only sends prompts and reads answers needs a surprisingly short list. On the bedrock-runtime endpoint there are four invocation actions: bedrock:InvokeModel, bedrock:InvokeModelWithResponseStream, bedrock:Converse and bedrock:ConverseStream. AWS’s own inference-profile prerequisites page writes these as the single wildcard bedrock:InvokeModel*, which is the form worth copying — it matches all four and does not silently break when you move from InvokeModel to the Converse API.
Note what is not in that list. Listing models (bedrock:ListFoundationModels) is a control-plane action on the bedrock endpoint and a runtime role does not need it. Neither does it need the AWS Marketplace actions. Those — aws-marketplace:Subscribe, Unsubscribe and ViewSubscriptions — are required only the first time a third-party model is enabled in an account, and Amazon documents that once a model is enabled, users can invoke it without holding any Marketplace permission. Granting them to a production role is granting it the ability to subscribe your account to paid products.
Two actions do belong in some runtime policies and are easily forgotten. bedrock:ApplyGuardrail is separate from invocation: a role that may invoke a model is not thereby allowed to run a guardrail over text, so a policy listing only bedrock:InvokeModel* starts failing the day somebody adds a guardrailIdentifier to the request. And bedrock:GetInferenceProfile is needed by code that resolves a profile by name before calling it, which some SDK helpers do on your behalf. Neither surfaces until the code path that needs it ships, which is why the resulting denial always looks like a regression in something unrelated.
Two ARN shapes, one of which has no account
Bedrock exposes three resource types you will meet in an invocation policy, and the difference between them is the part that catches people:
- Foundation model —
arn:aws:bedrock:us-east-1::foundation-model/model-id. Note the empty account field, the double colon. A foundation model is not your resource; it belongs to the service. This is also why you cannot attach a policy to one. - System-defined inference profile —
arn:aws:bedrock:us-west-2:111122223333:inference-profile/us.model-id. This one does carry your account, and its id is prefixed with a geography (us.,eu.,apac.,global.). - Application inference profile —
arn:aws:bedrock:us-east-1:111122223333:application-inference-profile/id. A resource you create, carrying cost allocation tags. It is a different ARN prefix and a policy scoped only toinference-profile/*will not match it.
The policy
- Decide the exact model id and the exact Region. Model access in Bedrock is per-account and per-Region, so a policy that names us-east-1 grants nothing in eu-west-1 — which is a feature, not an obstacle, and is the cheapest way to stop a service quietly failing over into a Region you have not reviewed.
- Write the direct-invocation statement, naming the foundation model ARN with the empty account field.
- Attach it to the role, not to a user. On Lambda that is the execution role; on ECS it is the task role, not the task execution role.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InvokeOneModelInOneRegion",
"Effect": "Allow",
"Action": "bedrock:InvokeModel*",
"Resource": "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-haiku-20240307-v1:0"
}
]
}
That policy works, and it is the whole job, provided you call the model by its bare model id in a single Region. The moment you switch to an inference profile — which for several model families is the only way to call them at all — it starts returning AccessDeniedException.
Why an inference profile needs a second statement
An inference profile is a routing resource: you pass its ARN as the modelId, and Bedrock forwards the request to the same foundation model in one of several Regions. IAM authorises both hops. Amazon states this explicitly on the inference-profile prerequisites page: when you name an inference profile in the Resource field, you must also name the foundation model in each Region associated with it. One statement authorises the profile; the other authorises the models it can reach.
The two-statement form also gives you a lever the one-statement form does not. The condition key bedrock:InferenceProfileArn lets you permit the foundation model only when the call arrived through a profile you named, so a caller cannot bypass your routing by invoking the bare model id:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "UseTheProfile",
"Effect": "Allow",
"Action": "bedrock:InvokeModel*",
"Resource": "arn:aws:bedrock:us-west-2:111122223333:inference-profile/us.anthropic.claude-3-haiku-20240307-v1:0"
},
{
"Sid": "ReachTheModelOnlyThroughIt",
"Effect": "Allow",
"Action": "bedrock:InvokeModel*",
"Resource": [
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-haiku-20240307-v1:0",
"arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-3-haiku-20240307-v1:0"
],
"Condition": {
"StringLike": {
"bedrock:InferenceProfileArn": "arn:aws:bedrock:us-west-2:111122223333:inference-profile/us.anthropic.claude-3-haiku-20240307-v1:0"
}
}
}
]
}
Enumerating the member Regions by hand is tedious and it is also the point: the list is the blast radius, written down. If a profile can route to four Regions, four Regions can serve your prompts, and a policy that says so is a policy a reviewer can audit. Read how cross-region inference chooses a Region before you decide which ones you are willing to list.
Amazon documents a global profile family whose requests are not pinned to a Region at all. Its policy example pairs an account-scoped profile ARN with a foundation-model ARN that has neither account nor Region, and blocks global routing with an explicit Deny on aws:RequestedRegion equal to the literal string unspecified. If data residency matters to you, that Deny is worth adding whether or not you think you are using a global profile. Check the current profile list before pinning any of this.
Verifying it without invoking anything
You do not have to spend tokens to find out whether the policy works. aws iam simulate-principal-policy evaluates the same authoriser against a hypothetical request, including SCPs and permissions boundaries:
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::111122223333:role/my-inference-role \
--action-names bedrock:InvokeModel \
--resource-arns arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-haiku-20240307-v1:0
Read EvalDecision in the output. allowed means the identity policy is right; implicitDeny means nothing matched, usually an ARN typo or the missing second statement; explicitDeny means something above you — an SCP or a boundary — is refusing, and no amount of editing this policy will change it. What the simulator cannot tell you is whether the account holds a model agreement for that model in that Region, which is a separate check and a separate error at runtime. If you are staring at a denial in production, the two different AccessDeniedException strings tell you which of the two you have.
Two other checks are worth building into the pipeline rather than running by hand. IAM Access Analyzer’s policy validation, exposed as aws accessanalyzer validate-policy, catches the structural mistakes this exercise invites — a resource ARN whose service does not match its actions, a condition key that does not apply to the action it is attached to — and it runs without any account state, so it belongs in CI next to your linter.
And once the policy is live, CloudTrail is the only source that tells you what is genuinely being called. Filter management events on the event source bedrock.amazonaws.com and read the eventName values your role actually produces over a week. Least privilege written from a documentation page is a guess; least privilege written from a week of CloudTrail is a description. The usual outcome is that one or two of the actions you carefully granted have never once been used, and can go.
Top comments (0)