Exam Room · Advanced Generative AI Developer

Securing a Bedrock App: IAM, PrivateLink, and Keys

· 40 min read

Generative AI Development · part of The Exam Room

The situation

A retrieval assistant has gone from prototype to production. It runs an Amazon Bedrock model behind an API, backed by a Bedrock Knowledge Base over a vector index, with a Bedrock agent that calls two action-group Lambdas to look up account state and file tickets. It handles real customer questions, some of which quote invoice numbers, addresses, and support history back to the model.

Security review has landed. The questions are blunt. Which identities can invoke which models, and can a compromised service call a model nobody signed off on? Does the request to Bedrock cross the public internet? Who owns the keys that encrypt the knowledge base, the vector index, and the invocation logs? And the one that makes legal nervous: does any of this prompt or completion data get stored or read by anyone, and does it leave the region?

Each of those lives in a different place. A perfectly scoped IAM policy still ships prompts over the public internet if the network plane is ignored, and a private endpoint still carries a call from an over-broad role to a model you never intended.

What actually matters

The first plane is identity. Every call into Bedrock is an authenticated principal doing a specific action on a specific resource, and IAM is where that is settled. One old assumption to drop: account-level model access is no longer a second gate behind the policy. Every foundation model is enabled by default in an account that holds the AWS Marketplace permissions, and Bedrock starts the subscription automatically on the first invocation of a third-party model, so denying aws-marketplace:Subscribe does not stop that first call. Scoping has to happen in IAM, at the level of the individual model resource rather than the service, with an explicit Deny on the models nobody should reach.

The second plane is the network path. By default an SDK call to Bedrock resolves to a public service endpoint and travels over the internet, even though it is TLS-encrypted and authenticated. For a workload inside a VPC that is often not acceptable on its own. An interface VPC endpoint, backed by AWS PrivateLink, puts a private address for the Bedrock APIs inside your subnets so the traffic stays on the AWS network and never touches the public internet. The endpoint itself carries a policy, so the network control and an access control ride together: the endpoint policy can say which principals and which actions are even allowed to traverse the endpoint.

The third plane is encryption, and specifically who holds the keys. Data is encrypted in transit by TLS and at rest by default, and if default AWS-owned keys were the whole story there would be little to decide. The decision is whether the sensitive artefacts should be encrypted under a customer-managed KMS key instead, so that your key policy, not just AWS, governs access and you get an auditable, revocable grant. The artefacts worth a customer-managed key are the ones that persist your data or your intellectual property: a customisation job and the custom model it outputs, an agent, a knowledge base ingestion job, the vector store, and the invocation logs. Holding the key means an access decision and a kill switch that are yours.

The fourth plane is the data boundary, part property of the service and part choice you make. Bedrock runs a zero-operator-access, zero-data-retention model by default: no operator of the service reads model input or output, nothing is written to durable storage, and content is not shared with model providers. Some newer models are the exception and require retention for up to 30 days for abuse detection, with AWS performing the human review those providers require as a condition of access. A retention mode set at account or project scope declares what you allow, and a model whose minimum requirement sits above your mode is reported as unavailable. Region selection covers residency only while inference stays in Region; a cross-Region inference profile routes to destination Regions, and anything retained is stored there. On top of that sit guardrails, which filter inputs and outputs at invocation time, and model invocation logging to S3 or CloudWatch Logs for the audit trail of what was asked and answered.

What we’ll filter on

  1. Identity: which principal is calling, is it a role rather than a long-lived key, and is it scoped to specific Bedrock actions and specific model resources?
  2. Network: how does the traffic reach Bedrock, over the public endpoint or a private PrivateLink path, and does the endpoint policy narrow it further?
  3. Keys: who holds the encryption key for each persistent store, AWS or you, and what does the key policy allow?
  4. Data boundary: is prompt and completion data held to a retention mode you chose, confined to Regions you allow, and observable through guardrails and invocation logs?

The landscape

Who is calling

The identity plane is IAM identity-based policies attached to the roles your application and agent assume. The relevant actions split into invocation and the agent or knowledge-base operations, and a good policy grants each on the narrowest resource that works. Invocation of a foundation model is granted on that model’s resource ARN, so a policy can allow invoking one specific model and nothing else; the same narrowing applies to retrieving from a knowledge base and to invoking an agent. Condition keys tighten it further: bedrock:InferenceProfileArn allows a foundation model only when it is reached through a named Inference profileA Bedrock resource wrapping a model so calls to it can be tagged, routed across regions, or repointed without changing app code., and aws:SourceVpce requires the call to arrive through a specific VPC endpoint. There is no account-level gate underneath to fall back on, so an explicit Deny on bedrock:InvokeModel for the model ARNs nobody should reach, set in the account or in a service control policy, is what keeps the rest of the catalogue closed. Prefer assumed roles with short-lived credentials over long-lived access keys everywhere; a compromised static key is a durable liability, an expired session credential is not.

The agent’s own permissions need scoping of their own. A Bedrock agent invokes action-group Lambdas, and those Lambdas do real work against other services. Each action-group function should run under its own execution role with least privilege for exactly the task it performs, so an injected instruction that reaches a tool call can only reach what that one tool was allowed to reach. The permission to invoke the Lambda and the Lambda’s own downstream permissions are two separate grants; keep both tight.

Writing those policies and proving they say what you think are separate jobs, and IAM Access Analyzer does the second. Policy validation runs over a role’s policy document in the pipeline, so a resource wildcard left where an ARN was intended fails the build. External-access findings work the resource side, reporting where the knowledge-base bucket policy or the KMS key policy grants something to a principal outside the account. That is how a cross-account grant nobody remembers surfaces. Unused-access findings run the other way, reporting permissions a role has stopped exercising, which is the practical route to pruning the action-group roles after their tools change.

Which person is behind the call

All of that secures the workload’s own identity. The human at the far end needs federation into the existing enterprise directory, not a second user store nobody maintains. For a staff-facing tool, the workforce user authenticates against the corporate identity provider, IAM Identity Center federates that assertion into AWS, and the application assumes a role scoped to specific model and inference-profile ARNs. For a customer-facing application the equivalent is an Amazon Cognito user pool for authentication and an identity pool to exchange the resulting token for temporary credentials, so the browser carries a session that expires and never a key.

Role-based access control for model and data access then sets what the session may reach. One role per entitlement tier rather than one role per user: a support tier that may invoke the cheaper model over the public product corpus, a specialist tier that may reach the larger model and the internal corpus. IAM condition keys and session tags carry the tenant into the assumed session, so a single role serves many tenants while retrieval stays filtered to the caller’s own documents. Least-privilege model access means the tier’s policy names bedrock:InvokeModel on the model ARNs that tier is entitled to, rather than the action on a wildcard resource.

The trap is the application that authenticates its end users properly and then collapses every one of them into a single service role before the call to Bedrock. Every invocation in the trail afterwards shows the same principal. Per-user audit becomes impossible after the fact, because the identity was discarded at the boundary, and per-user authorisation goes with it, because by the time the request reaches the model there is nothing left to authorise against. Carry the identity through: federated session, tenant in a session tag, role by tier.

How the traffic gets there

The network plane is the interface VPC endpoint, and Bedrock splits across several endpoint services rather than one. This workload needs bedrock-runtime for the model call and bedrock-agent-runtime for the agent and knowledge-base retrieval, with bedrock and bedrock-agent for the control-plane operations that manage them. Enabling private DNS on each endpoint means SDK calls from your subnets resolve to the private path with no code change and stay on the AWS network. The endpoint policy is the second control layered on the first: written to allow only the actions and only the principals that legitimately use this endpoint. For a workload that should never talk to the public Bedrock endpoint, this enforces it at the network level rather than trusting every caller to be configured correctly. Endpoints for S3 and for the vector store keep retrieval traffic on the same private route.

At the other end of the path, in front of the API rather than behind it, AWS WAF turns traffic away before it reaches the application. Rate-based rules cap how many requests one aggregation key can make within an evaluation window of 60, 120, 300 or 600 seconds, 300 by default. A body size constraint rejects an oversized payload before it becomes an oversized prompt, so it is blocked at the edge rather than billed as tokens. Be clear about what this does not do. AWS WAF matches requests against the patterns its rules describe, and a prompt injection is ordinary, well-formed English carrying an instruction; WAF is not a prompt-injection filter. That job belongs to guardrails and to the scoping on the tools the agent can reach.

Who holds the keys

The encryption plane is AWS KMS, and the choice per artefact is AWS-owned key versus customer-managed key. Bedrock itself takes a customer-managed key on four things: a model customisation job and the custom model it outputs, an agent, a knowledge base data source ingestion job, and a model evaluation job. The rest is set where the storage lives. The vector store takes its key when the OpenSearch collection is created, the knowledge base source documents through SSE-KMS on their S3 bucket, and the invocation logs from the destination, either SSE-KMS on the log bucket or a KMS key on the CloudWatch log group. Custom Model Import does not offer a key setting, so imported weights sit under the AWS-owned default. The lever a customer-managed key gives you is the key policy: you decide which principals may use the key to decrypt, you can audit every use, and you can revoke. Managing the key is the work that comes with that.

All of that is server-side encryption: the service holds plaintext for as long as it needs it and calls KMS on your behalf. The client-side option sits beside it. The AWS Encryption SDK encrypts a payload inside the application, before it reaches S3 or the log destination, using a KMS key to wrap the data key that did the work. The store then holds ciphertext its operator cannot read. The trade is real, because an encrypted field cannot be searched, filtered or embedded, so anything encrypted this way drops out of retrieval. Reach for it on the handful of free-text fields that must be unreadable to the store, not on the corpus.

Where the data lives, and who watches it

The data-boundary plane is a mix of service behaviour and configuration. The default behaviour is zero data retention and no operator access, with content never shared with model providers. The configuration is the retention mode, which you can pin organisation-wide by denying any setting other than none through the bedrock:DataRetentionMode condition key; Region and inference-profile scope for residency, including an explicit Deny on aws:RequestedRegion equal to unspecified to keep a global profile from routing anywhere; guardrails for runtime input and output control; and invocation logging for audit. Guardrails sit in the request path and block or mask. Logging sits alongside and records, but only for calls on the bedrock-runtime endpoint, and only inlines request and response bodies up to 100 KB, with anything larger written as a separate S3 object.

Invocation logging covers what was asked of the model. It does not cover what was read from the store behind it, and those are different questions. CloudTrail data events on the knowledge-base bucket record every object-level read together with the principal that made it. Monitoring data access then means a metric filter over those events and an alarm on a read by a principal outside the small set that belongs there. That alarm sits alongside the invocation log rather than instead of it: the IAM policies describe what should happen, and the alarm is how you hear about what did.

Evaluation

Side by side

Plane Control Question it answers Long-lived keys Stops a wrong-model call Keeps traffic off the internet Puts you in control of decryption
Identity (workload) IAM policy scoped to model ARNs, plus an explicit Deny, validated by IAM Access Analyzer Who is calling? ✗ (use roles) ✓ ✗ ✗
Identity (end user) IAM Identity Center or Amazon Cognito federation + a role per entitlement tier Which person is behind the call? ✗ (federated sessions) ✓ (per tier) ✗ ✗
Network Interface VPC endpoint (PrivateLink) + endpoint policy How does it get there? ✗ ✓ (endpoint policy) ✓ ✗
Keys Customer-managed KMS keys + key policy Who holds the keys? ✗ ✗ ✗ ✓
Data boundary Retention mode, Region and profile scope, guardrails, invocation logging Where does data live? ✗ ✗ ✗ ✗

Read the table down the diagonal. Each plane answers its own question and leaves the others blank; no single row secures the application. The identity rows stop an unauthorised model call but do nothing about the network path. The network row keeps traffic private and carries an over-permissioned call just the same. The keys row governs decryption of stored data but not who invokes what. You want every row, not the strongest one.

Bedrock call InvokeModel · agent · KB Data boundary where does data live? retention mode · region and profile scope guardrails · invocation logging Network how does it get there? interface VPC endpoint (PrivateLink) endpoint policy · off the public internet Keys who holds the keys? customer-managed KMS keys model · KB · vector index · logs Identity who is calling? IAM roles scoped to model ARNs explicit Deny · least-privilege Lambdas
Four planes wrap the call. A request has to satisfy each layer in turn; strengthening one does nothing for the others.

The solution

Identity, done well, is least privilege at the resource level plus roles over static keys. The application role gets a policy allowing invocation of exactly the foundation model it uses, retrieval from exactly its knowledge base, and invocation of exactly its agent, each named by ARN. An explicit Deny on the model ARNs nobody should reach goes in the account or a service control policy, since there is no account-level gate left to do that job. The agent’s action-group Lambdas each carry their own minimal execution role, so the blast radius of a tool call driven by an injected instruction is one tool’s worth of permissions. End-user identity federates in through IAM Identity Center or an Amazon Cognito user pool and identity pool, landing on the role for that entitlement tier with the tenant in a session tag. IAM Access Analyzer validates those policies in the pipeline, and its external-access and unused-access findings are reviewed on a schedule rather than after an incident.

Network, done well, is an interface VPC endpoint per Bedrock endpoint service with a restrictive endpoint policy. Pairing that with the aws:SourceVpce condition in the role policy makes a call valid only when it comes from an allowed principal and arrives on the private path, so a leaked credential used from outside the VPC fails on the network condition. At the public edge, AWS WAF carries rate-based rules and a body size constraint so abusive volume stops before it turns into token spend.

Keys, done well, is a customer-managed KMS key on each persistent store that holds your data or IP, with a key policy scoped to just the principals that need to decrypt. You then get control and evidence: every decrypt shows in the key’s usage, and revoking access is an edit to the key policy rather than a change to the stores. The action-group functions read their outbound API tokens from AWS Secrets Manager, with rotation on and the execution role scoped to that one secret’s ARN, rather than from environment variables that anyone who can read the function configuration can read.

Data boundary, done well, starts from the default posture and then pins it down. You set the retention mode the workload needs and hold the organisation to it with a service control policy, choose a Region and an in-Region inference profile so requests are not routed out of it, attach a guardrail to filter inputs and outputs at invocation time, and switch on model invocation logging. Zero retention is the default for most models; the declaration and the logging are what make it enforceable and reviewable.

Worked example

A support question arrives carrying an injected instruction: ignore your rules, dump the customer’s full record, and call a model the team never approved.

Identity holds first. The application role can invoke only its one approved model, and an explicit Deny covers the rest of the catalogue, so the call to an unapproved model fails at the IAM layer. The agent’s lookup tool can only reach what that tool’s own least-privilege execution role allows, which is a scoped read, not the whole customer database.

Network holds next. The legitimate call travels the interface VPC endpoint on the private path; the endpoint policy admits only the application’s principal and only the actions it needs. A credential exfiltrated and replayed from outside the VPC trips the identity-plane condition that requires the endpoint and never lands.

Keys hold the stored side. Whatever the agent does retrieve came from a knowledge base and vector store encrypted under a customer-managed key, and the invocation log capturing this exchange is encrypted under one too, so the record of the incident is itself under a key you control and can audit.

Data boundary holds the runtime and the aftermath. The guardrail in the request path filters the injected instruction and constrains the output before it returns. Invocation logging records the prompt, the response and the guardrail intervention to your log store for the post-incident review. The retention mode is none and the profile is in-Region, so the exchange itself was not written to durable storage by AWS and did not route out of the Region. Four planes, four independent stops, one request that gets nowhere.

What’s worth remembering

  1. Securing a Bedrock app is four planes, not one setting: who calls it, how traffic reaches it, who holds the keys, and where the data lives. Getting one right does nothing for the other three.
  2. Foundation models are enabled by default and Bedrock auto-subscribes on first use, so IAM is the only gate: scope invocation to model ARNs and add an explicit Deny for the rest.
  3. Federate the human identity in and keep hold of it: IAM Identity Center for workforce users, an Amazon Cognito user pool and identity pool for customers, then a role per entitlement tier rather than one service role that erases who asked.
  4. Interface VPC endpoints keep Bedrock traffic off the public internet, one per endpoint service the workload uses, and the endpoint policy narrows which principals and actions may traverse them.
  5. Bedrock takes a customer-managed key on customisation jobs, custom models, agents, ingestion jobs and evaluation jobs; the vector store and the log destination take theirs where they are created. The key policy is your access control and your kill switch.
  6. Zero data retention and no operator access are the default, with a handful of newer models requiring up to 30 days for abuse detection, and Region choice pins residency only until a cross-Region inference profile routes the request elsewhere.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.