Module 5: Securing The Ai Workload
2. Least privilege for a model, not a service
Description
Module 3, lesson 4 built BedrockManifestExtractorRole: an IAM role whose permissions policy scopes bedrock:InvokeModel to the exact ARN of a single model — arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-lite-v1:0 — never to the whole service. At that point, the only verification available was reading the JSON in terraform plan's output with your own eyes. This lesson precisely names the exact antipattern that discipline avoids — bedrock:* — and sets up lesson 3 to turn it into a rule that applies itself, every time, without depending on anyone remembering to check.
Connection to the module
cloud-security-and-guardrails-guide, Module 2, already established least privilege as a general IAM discipline — and its Module 4, lesson 7, wrote least-privilege-iam.rego, the policy that catches any Statement with Action: "*" in any aws_iam_role_policy in the project. This lesson doesn't repeat that discipline from scratch: it applies it, for the first time in this ecosystem, to a domain where the "everything" you need to avoid isn't a generic asterisk, but a service-specific action space (bedrock:*) that guide's generic policy isn't designed to catch.
Analogy: a room's key, revisited under a different lens
Module 3, lesson 4 of this same guide already used this analogy: a key that opens exactly the room where someone works, not the master key that opens the whole building. It's worth returning to it here, because this lesson adds something that first version didn't have: who checks that the correct key was actually issued. In a real building, someone — a security officer, an audit process — has to confirm, repeatably, that each new key that gets cut really does open only the assigned room, and doesn't "by mistake" also open the server room three floors down. Without that confirmation, the discipline of "only your room's key" depends entirely on the person cutting the key never making a mistake — a reasonable bet the first time, a risky bet the twentieth.
bedrock-least-privilege.rego, which lesson 3 builds, is that security officer, turned into code. It doesn't cut the key — bedrock.tf's HCL still does that — but it confirms, every time someone proposes a change, that the key about to be cut really opens only what it should.
The exact antipattern: bedrock:*, and why it's worse than it sounds
It's easy to hear "don't use wildcards" as a style rule, almost cosmetic. It isn't. It's worth being precise about what bedrock:* actually authorizes in an IAM policy, compared to what Andes Cargo needs:
WHAT extract-shipment-manifest-fields NEEDS WHAT "bedrock:*" AUTHORIZES
bedrock:InvokeModel bedrock:InvokeModel
on ONE model bedrock:InvokeModelWithResponseStream
(amazon.nova-lite-v1:0) bedrock:CreateGuardrail
bedrock:DeleteGuardrail
bedrock:UpdateGuardrail
bedrock:CreateModelInvocationJob
bedrock:PutModelInvocationLoggingConfiguration
bedrock:CreateProvisionedModelThroughput
bedrock:DeleteProvisionedModelThroughput
... (dozens more actions)
The list on the right isn't hypothetical or exaggerated: these are real actions in the bedrock: namespace of AWS's IAM action catalog. A role with bedrock:* can, among many other things, delete the guardrail this guide's Module 4 built (bedrock:DeleteGuardrail), create new billing infrastructure (bedrock:CreateProvisionedModelThroughput, which starts charging per hour, not per token), or modify invocation logging configuration (bedrock:PutModelInvocationLoggingConfiguration) — none of that has anything to do with what extract-shipment-manifest-fields needs to do its job: read text from a manifest and ask a model to extract fields.
This is what separates "least privilege as general good practice" from "least privilege applied with precision to a specific service": the right question is never "did I avoid the asterisk?" in the abstract — it's "does the exact set of actions this role can do match, one to one, what this code needs to do, not one action more?" BedrockManifestExtractorRole, as Module 3 left it, answers that question correctly: one single action (bedrock:InvokeModel), one single resource (a model's ARN).
Why the ARN matters too, not just the action
There's a second dimension to the same problem, and it's just as real as the first: even if a role correctly scopes the action to bedrock:InvokeModel — never bedrock:* — it could still leave the resource wide open:
{
"Action": "bedrock:InvokeModel",
"Effect": "Allow",
"Resource": "*"
}
This policy is, on the surface, more restrictive than bedrock:* — it only allows invoking models, nothing else. But Resource: "*" means extract-shipment-manifest-fields could invoke any model available in the account, not just Amazon Nova Lite. In a real scenario, that matters: different Bedrock models have different per-token prices — Nova Premier costs several times more per million tokens than Nova Lite (this guide's Module 2, lesson 2) —, so a role with Resource: "*" doesn't just widen the security surface, it widens the cost surface: a code error, or a compromised credential, could start billing against a much more expensive model with no IAM limit stopping it. BedrockManifestExtractorRole closes both dimensions at once: one action, one resource.
The two attack vectors lesson 3 will block with code
VECTOR 1 -- Overly broad action VECTOR 2 -- Overly broad resource
"Action": "bedrock:*" "Action": "bedrock:InvokeModel"
"Resource": "*"
Authorizes creating/deleting guardrails, Authorizes invoking ANY model
provisioning throughput, modifying in the account, not just Nova Lite --
logging -- the service's entire COST surface, not just
security surface security
bedrock-least-privilege.rego, rule 1 bedrock-least-privilege.rego, rule 2
Both vectors share the same root — "more than the code actually needs" —, but deserve two independent deny rules, not one combined rule: they're different questions ("is the action too broad?" and "is the resource too broad, given that the action itself is correct?"), and combining them into a single rule would produce the same problem cloud-security-and-guardrails-guide, Module 4, lesson 7 already avoided with no-public-buckets.rego: an error message that doesn't precisely distinguish which of the two real problems was found.
Common mistakes
Thinking that scoping the action (bedrock:InvokeModel, not bedrock:*) is already enough, without checking the resource (solving half the problem). What happens: someone carefully writes "Action": "bedrock:InvokeModel", feels satisfied for having avoided the obvious wildcard, and doesn't check whether "Resource" is still "*". How to spot it: if your policy has a specific action but a generic Resource, with no ARN cited. How to fix it: least privilege on Bedrock is a two-axis decision, action and resource — the section above develops this precisely. BedrockManifestExtractorRole scopes both; a policy that only scopes one of the two is still, in practice, broader than necessary.
Arguing that bedrock:* "is simpler to maintain" because you'd never have to update the policy when a new model gets added (convenience over discipline). What happens: someone, anticipating that Andes Cargo might want to try a different model in the future, prefers to leave access open "so we don't have to touch the HCL later." How to spot it: if the justification for a broad policy is about future convenience, not a current need. How to fix it: the same discipline terraform-and-iac-guide and cloud-security-and-guardrails-guide already established for other resources applies here without exception — declare what the code needs today; if Andes Cargo decides to invoke a second model tomorrow, that change is a new line in resources = [...], reviewed by the same policy-check this module builds, not a reason to leave the door open from now on.
Confusing "least privilege" at the IAM action level with "least privilege" at the prompt content level (mixing two different security layers). What happens: someone, after reading this lesson, starts talking about "prompt least privilege" — what information gets passed to the model — as if it were the same concept as this lesson's IAM least privilege. How to spot it: if your explanation of this lesson starts mentioning what text gets sent to Bedrock, instead of what actions the role can invoke. How to fix it: these are completely different layers, with different owners. What information reaches the model is the guardrail's responsibility (Module 4) and the prompt itself's (the boundary with AI Engineering, Module 1, lesson 4); what AWS actions the code calling the model can invoke is IAM's responsibility (this lesson, and Module 3, lesson 4). Neither replaces the other.
Exercises
Exercise 1 — Without looking at bedrock.tf, write from memory the complete IAM policy (action and resource) BedrockManifestExtractorRole should have, in JSON format. Then compare it against this guide's Module 3, lesson 4.
See solution
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InvokeManifestExtractionModelOnly",
"Effect": "Allow",
"Action": "bedrock:InvokeModel",
"Resource": "arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-lite-v1:0"
}
]
}
One single action, one single resource, with a Sid that describes exactly what this Statement authorizes — the same JSON, literally, that Module 3, lesson 4 showed in its terraform plan.
Exercise 2 — Explain why bedrock:DeleteGuardrail within bedrock:*'s scope is, in practice, more dangerous for Andes Cargo than a single PII leak. Think about what Module 4's guardrail protects, and what would happen if that protection disappeared for all future manifests, not just one.
See solution
A single PII leak — an email address that leaks into one invocation's response — is a contained incident, painful but limited to that specific case. bedrock:DeleteGuardrail, on the other hand, if executed by mistake or by a compromised credential, would delete the managed guardrail — Module 4's six policies — for all future invocations, until someone noticed and redeclared it with terraform apply. It's the difference between a one-time data incident and the silent removal of an entire defense layer, with nothing in the system flagging it immediately — the reason bedrock:* is categorically different from "a bit more access than needed": it opens the door for the very protection mechanism to be turned off.
Exercise 3 — Design, in prose, a third attack vector bedrock-least-privilege.rego (lesson 3) does NOT cover, related to IAM but outside the scope of "action" and "resource" of a single Statement. Hint: think about who can assume the role, not what it can do once assumed.
See solution
A reasonable answer: BedrockManifestExtractorRole's trust policy (assume_role_policy) could, in theory, declare an overly broad principal — for example, any AWS service ("Service": "*" instead of "lambda.amazonaws.com" specifically), or even an entire external account —, which would let a resource completely unrelated to the extract-shipment-manifest-fields Lambda assume this role and inherit its permission to invoke the model. bedrock-least-privilege.rego, as designed in lesson 3, evaluates the permissions policy (inline_policy_json/permissions_policy_json), not the trust policy — a real vector, but outside this policy's specific scope. Closing it would require an additional rule, dedicated to assume_role_policy, following the same pattern least-privilege-iam.rego already uses for the project's other roles.
Summary and next step
This lesson precisely named, without exaggeration, the exact antipattern this module exists to block: bedrock:* as an action (authorizes creating, deleting, and reconfiguring entire Bedrock resources, not just invoking a model) and Resource: "*" as a resource (authorizes invoking any model in the account, with cost implications on top of security ones). You saw the two vectors as independent questions, each with its own deny rule in the policy lesson 3 builds, and confirmed that BedrockManifestExtractorRole, as Module 3 left it, already answers both questions correctly.
Before moving on you should be able to: explain why bedrock:* is qualitatively different from "a bit of extra access"; name the two axes (action, resource) Bedrock least privilege needs to cover; and anticipate why this lesson needed two deny rules, not one.
Lesson 3 turns this thesis into real code: policy/bedrock-least-privilege.rego, run for real against Module 3's plan, with a real PASS on the correct role and a real FAIL on an attempt with bedrock:*.
Resources
- AWS Docs — Actions, resources, and condition keys for Amazon Bedrock — complete official catalog of the
bedrock:namespace's actions, the source for the list cited in this lesson. - This guide's Module 3, lesson 4 (
04-hands-on-bedrockmanifestextractorrole-least-privilege.md) — the original construction of the role this lesson revisits. cloud-security-and-guardrails-guide, Module 4, lesson 7 (least-privilege-iam.rego) — the general IAM least-privilege discipline this lesson applies to a specific domain.- This guide's Module 2, lesson 2 — the per-token prices of different Bedrock models, the source of the cost concern behind scoping
Resource.