Build1 publisher3 min readPublished
Two Actions, One Loose Policy: The Bedrock Wildcards That Widen A Least-Privilege Grant
A developer audited the IAM policy on his own Bedrock agent and found one statement scoped to a single secret and another that reaches every Claude model, in every region Bedrock runs.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened
- The role backs an agent that does two things: it reads one secret from Secrets Manager, and it invokes a Claude model on Bedrock to write a daily report.
- The author wrote the IAM policy himself on the first try, believed he had done it right, and on re-reading it line by line found that he had not, entirely.
- The first statement, Sid SecretsManagerReadConfig, has Effect Allow, Action secretsmanager:GetSecretValue, and Resource arn:aws:secretsmanager:us-east-1:111122223333:secret:solar-daily-brief/config-*
- The trailing -* in the secret ARN only covers the random suffix Secrets Manager appends to every ARN, so that statement grants access to exactly one thing that exists.
- The second statement, Sid BedrockInvokeModel, allows Action bedrock:InvokeModel on two resources: arn:aws:bedrock:*::foundation-model/anthropic.claude-* and arn:aws:bedrock:us-east-1:111122223333:inference-profile/us.anthropic.claude-*
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A developer published the IAM policy from his own Bedrock agent, read it line by line, and found that half of it did not meet his own standard [2]. That is worth studying because the loose half looks like the tight half at a glance: two statements, two actions, no wildcard in any Action field, nothing that would stop a reviewer skimming for admin verbs [18].
The role, according to Mario Gongora writing on dev.to, backs an agent that does two things: it reads one secret from Secrets Manager, and it invokes a Claude model on Bedrock to write a daily report [1]. The first statement allows `secretsmanager:GetSecretValue` on `arn:aws:secretsmanager:us-east-1:111122223333:secret:solar-daily-brief/config-*` [3]. The trailing `-*` there is not a hedge. Secrets Manager appends a random suffix to every secret ARN, so that wildcard resolves to exactly one resource that exists [4].
The second statement allows `bedrock:InvokeModel` on two ARNs: `arn:aws:bedrock:*::foundation-model/anthropic.claude-*` and `arn:aws:bedrock:us-east-1:111122223333:inference-profile/us.anthropic.claude-*` [5]. Here the wildcards do work nobody asked for. `anthropic.claude-*` matches every Claude model within the account's reach rather than the one model the code calls [6], which in practice means Claude 3, Claude 3.5, Claude 3.7, Opus, and any future model that happens to match the prefix [9]. The foundation-model ARN also opens with `bedrock:*::`, a region wildcard [7]. Foundation-model ARNs are not account-scoped, taking the form `arn:aws:bedrock:REGION::foundation-model/MODEL-ID`, so `bedrock:*::` is how people quietly grant a model in every region Bedrock runs, not only the `us-east-1` the agent actually calls [8]. Two independent wildcards on one ARN means the permitted set is a cross product: regions where Bedrock is available, multiplied by Claude models matching the prefix [19].
The code settles what memory could not. Gongora reports that `deye_client.py` and `collector.py` make exactly one `get_secret_value()` call against one secret name, resolved at startup and cached for the process lifetime [10], and that the report step calls `bedrock-runtime.invoke_model()` with a single model ID read from config, not a family and not a runtime choice [11]. No path in the project needs a second secret or a second model [12]. His diagnosis of how the gap opened is worth quoting in substance: writing IAM by intuition feels faster than writing it from evidence, so you grant roughly what the code does and round up to be safe [13]. The Bedrock wildcard specifically was meant as a hedge against changing model versions [14].
The prescribed fix is unglamorous and cheap: pin the resource to the specific model ID or inference profile the config already names, and accept that a model upgrade later costs a one-line policy change [15]. That is the same trade the Secrets Manager statement makes for free, and the same reason adding a second secret next year should grow the policy by one line rather than by a wildcard covering whatever else lands in that account [17]. The general method he recommends is to open the code, list every AWS API call, note the exact resource each call touches, and write the policy from that list rather than from an SDK doc example [16].
The tell to look for in your own policies is an ARN with an empty account field, because a resource type that is not account-scoped will not fail closed for you [8]. The second tell is a wildcard standing in for a version or a model generation rather than for a suffix the service generates [4][9]. Whether the pinned version of this policy actually ships, and how much friction the next model upgrade creates, is the part that decides if the discipline holds.