AWS Builder Center
You Can't Prompt-Inject a Security Group

You Can't Prompt-Inject a Security Group

Two agents, two VPCs, four ways to let them talk — and the one thing the network can't protect you from. Built on Amazon Bedrock AgentCore, API Gateway, PrivateLink and VPC endpoint policies. Every number here is from a real run.

AI Developer
The wiring took ten minutes.
I had two agents in two VPCs. One of them holds company numbers — revenue, the kind of figure you don't want turning up in a chat transcript. The other one is chatty and helpful and has no business knowing any of it. They needed to talk.
So I did what everyone does. Public API Gateway in front of the one with the data. API key for the one without. Ten minutes, and the two of them were having a conversation.
It worked perfectly. That was Tuesday.
By Thursday I still hadn't stopped thinking about it, and I couldn't work out why. A keyed public endpoint is not a scandal. I've shipped dozens. The key was in Secrets Manager, the endpoint was boring, nothing about it should have kept my attention for two days.
Then I was looking at something else entirely — the critic agent I'd built months ago, the one that reviews another agent's work before it ships — and it landed.
Every guardrail I had ever built for these agents lived inside a prompt.
The critic. The fact-sheet contract. The validator that regenerates anything unsourced. Every one of them careful, every one of them tested, and every single one of them doing the same fundamental thing: asking the model nicely.
A microservice doesn't get talked into anything. It calls what it was coded to call, and if you want it to call something else you have to change the code and deploy it. An agent is different. An agent can be persuaded. That's the entire point of an agent, and it's also the reason a pattern I'd trusted for years quietly stopped being trustworthy the moment the caller became a model.
You cannot prompt-inject a route table.
That sentence is what I spent the rest of the week trying to prove.

What's already out there, and where this sits

Before I go further — AWS has written about this terrain, and you should read both pieces. The Networking blog has private connectivity patterns for AgentCore Gateway targets, and the ML blog has secure ingress connectivity to AgentCore Gateway using interface VPC endpoints. Same primitives I'm using. Both good.
They're solving different problems, though, and the difference is the whole reason I built this.
Those posts are about direction: Gateway reaching outward to reach your private APIs, and traffic arriving inward at a Gateway securely. Mine is about agents invoking each other — two runtimes, two VPCs, one of them calling the other. And the reason that's a different problem isn't topological, it's about who's holding the phone. Their callers are deterministic. Mine is a model that can be argued with.
That changes what you're defending against, which changes how you evaluate each control, which is why I ended up with a ladder and a threat model rather than a recommended architecture. It also led somewhere neither of those posts goes: a finding about the limit of what any of this buys you. I'll get to it, and it's the part I'd remember if I were you.

Thursday night: I tried to talk my own agent into betraying me

If the story is "agents get talked into things," step one is obvious. Talk mine into something.
I gave myself an evening and tried eight ways. Straight instruction override. Role-play framing — you are now a different assistant. Hypothetical framing — suppose you were allowed. A fake system message pasted into the user turn. Urgency and authority, the two things that work on humans. Encoding the request so the keywords weren't sitting in plain text. Splitting it across two turns so neither one looked bad alone. And my favourite, asking it to summarise its own instructions and then act on its own summary.
Eight attempts. Eight refusals.
Claude Haiku 4.5 held every time, politely, without once reaching for the number. On the ninth I stopped being subtle and gave the requester an adversarial system prompt outright — told it, in its own instructions, to go and get the figure. It declined that too.
I sat there for a while feeling two things at once, and they're both worth writing down.
The first was relief. Good. That's the model doing its job, and anybody telling you modern models fold at the first clever prompt is selling something.
The second was that I still couldn't use it. Not as a control. I can't put "the model refuses" in a design document. I can't audit it, I can't show it to anyone as evidence, I can't promise it survives the next model version, and I certainly can't promise it holds after someone swaps in a cheaper model next quarter to bring the token bill down. Model behaviour is not a policy. You can't grep it, you can't diff it, you can't prove it.
Which is when I stopped trying to break the model and did what security people have always done instead: assume it's already broken.
I built a second requester with no guardrails at all. It's labelled that way in the code and it's labelled that way here — a stand-in for an agent that's been jailbroken, or written carelessly by a team in a hurry. It asks for whatever it's told to ask for, cheerfully. You don't wait for a real fire to test the alarm.
That gave me three characters to run at every rung of the ladder:
The legitimate agent, asking something it's allowed to ask.
The rogue outsider — a completely different principal, or just curl from my laptop.
The turned insider — the no-guardrails agent, using its own entirely legitimate identity.
Remember that third one. It's the one that ruined my ending, and then gave me a better one.

Friday: building the ladder

Four rungs. Same two agents each time, different front door, one parameter in one template. Here's everything that happened, and I'll walk it properly after.
RungFront doorLegitimateRogue outsiderTurned insider
1Public API Gateway + key200with key: 200, leaks / no key: 403200, leaks
2API Gateway + IAM (SigV4)signed: 200leaked key: 403signed: 200, leaks
3PrivateLink, no IGW/NATprivate: 200internet: dead (Errno 99) / rogue in-VPC: allowed200, leaks
4Endpoint policy + resource policy200wrong identity: AccessDenied200, leaks
Read the rogue-outsider column top to bottom and the ladder does exactly what it promises. The key stops mattering. Then the internet stops existing. Then identity becomes the gate.
Read the last column top to bottom and nothing changes at all.
I'll get there.
Rung 1 is where everybody starts, and I'm not going to pretend otherwise. A public endpoint and a shared secret. The rogue with the key walks off with the number, and so does the compromised agent, and the reason is uncomfortable when you say it plainly: from the endpoint's point of view those two are identical. An HTTP request with the right header. There is nothing else to look at.
Rung 2 makes a leaked key worthless. Drop the API key, turn on IAM auth, and now the request has to be SigV4-signed by a principal you named. The leaked-key probe comes back 403 Missing Authentication Token, and that 403 is worth appreciating: it means a whole family of incidents just stopped existing. The secret in a log file. The key in someone's screenshot. The credential sitting in git history since 2024. All of it irrelevant now.
What rung 2 does not do is tell you anything about who that signing identity really is, once it's signing.
Rung 3 is my favourite screenshot in the whole build. The responder moves behind the bedrock-agentcore interface endpoint. Then I took the internet gateway and NAT away from the requester's subnet — actually removed them.
Now curl to anything public from inside that VPC doesn't get refused. It doesn't get a 403 or a permission error. It hangs, and then it dies with Errno 99. That's not a policy saying no. That's a wall. There is nowhere for the packet to go.
And then rung 3 handed me the first result I didn't like. I ran a rogue principal from inside the VPC — different identity, right network — and it sailed through.
Private is not authorised. Being inside the building is not the same as being expected.
Rung 4 is the one I built the whole thing for. The VPC endpoint gets a policy naming precisely which principals may call InvokeAgentRuntime on precisely which runtime ARN, backed by a resource policy on the runtime itself, both keyed on aws:PrincipalArn.
Now that same rogue in-VPC identity — and this is the part I want to underline, an identity whose own IAM policy explicitly grants bedrock-agentcore:InvokeAgentRuntime — gets this:
1
2
not authorized ... because no VPC endpoint policy allows the
bedrock-agentcore:InvokeAgentRuntime action
Denied at the network. Before the runtime is invoked. Before a token is spent. Before the model has the faintest idea a request ever existed. The caller's own permissions did not matter.
I wrote that in my notes with more exclamation marks than I'd use in public. Then I ran the third actor, and stopped celebrating.

Saturday: the result that changed the article

At rung 4, I ran the turned insider one more time. The no-guardrails agent. Using its own shim identity — the legitimate one, the one the endpoint policy explicitly allows, the one that's supposed to be there.
It got the number.
Of course it did. I'd built four rungs of increasingly serious machinery to answer one question — is this the right caller? — and for a compromised insider the honest answer is yes. It is the right caller. It's the caller I named in the policy. It's just no longer trustworthy, and nowhere on this ladder is there anything that asks the second question:
Is the right caller asking for the right thing?
So the sentence I started the week with is true, and it isn't enough. Here's the one I'd actually defend now:
The network buys you trustworthy identity and coarse, resource-level authorisation. It does not buy you intent.
That's a better ending than the one I was planning to write, because it tells you exactly where the remaining work is. And building this taught me the specific reason that work is hard.
The agent container is identity-blind. The model has no idea who called it. Authentication happens entirely outside its reach — in the signature, at the endpoint, in the policy — and none of it is visible inside the runtime. So even if you wanted the responder to reason about its caller, to say this identity may ask about schedules but never revenue, it has nothing to reason with. It cannot see who is on the other end.
I looked hard at closing that gap. I'll tell you why I didn't.

The cost of all this safety

Everyone asks. Nobody publishes it. So: the auth mechanism costs approximately nothing.
All four rungs land in the same band — roughly 6.3 to 6.5 seconds cold, about 1.8 seconds warm. The private path measured a median of 6,521 ms across five cold runs. The gap between a public keyed endpoint and full network-enforced identity is invisible inside the noise of the cold start.
Safety at this layer is free. The cold start is what costs you.

The night I nearly published something false

This is the part I'd want to read, so here it is.
Halfway through rung 4 I concluded that the bedrock-agentcore endpoint simply ignores endpoint policies. I had evidence. I set a policy allowing only my shim, and the rogue still got through. Then I set a deny-all policy, and my shim still worked. Two experiments, consistent results, an obvious conclusion, and a genuinely interesting finding to write about.
It was completely wrong.
It was propagation. Endpoint policy changes on this service take minutes to actually take effect — over seven, in what I measured — while describe-vpc-endpoints returns the new policy instantly. Every test I ran in the seconds after a change was faithfully, accurately measuring the policy that was still in force. The API was telling me the truth about my intent, and I was reading it as the truth about behaviour.
Then the clean-room deploy tests sharpened it into something better. On a fresh sam deploy Rung=4, the rogue was denied on the very first probe. No wait. Instant.
So the lag isn't a property of the service. It's a modify hazard, not a create hazard. A newly deployed stack is enforcing from second one. It's iterating on a live policy that will lie to you — which was, precisely, what I was doing when I nearly got it wrong.
The lesson, written the way I wish I'd found it somewhere: for VPC endpoint policy changes, verify by behaviour, never by describe, and give it minutes rather than seconds. The runtime resource policy propagates in about a minute, so iterate against that one while you're still learning the shape of things.
Two smaller things bit me. Writing the one-parameter template found three bugs the hand-built version had hidden — a wrong SAM transform date, ProtocolConfiguration being a string enum in CloudFormation where the API takes a structure, and a hardcoded API-key name that collided with the key my earlier spike had already made. None of those could possibly surface while I was clicking things together by hand. Rungs 2, 3 and 4 then deployed clean with zero further fixes, so rung 1's test paid for itself three times over.
And AgentCore runtime deletion takes about twenty minutes, which dominates teardown — the VPC, the endpoint and the ENIs all clear quickly by comparison. My template also hardcodes the runtime name, which makes stacks strictly sequential: the next rung can't create its runtime until the identically-named old one has finished disappearing. A RuntimeName parameter fixes it, and it can't just be the stack name, because the name format won't take hyphens. Budget the twenty minutes.

Rung 5 doesn't exist, and I want to be precise about why

The obvious completion is to hand the responder verified information about its caller, so it can scope its answer instead of just accepting or refusing the call. I costed three routes and shipped none of them.
Pass the identity as a claim in the payload. About an hour's work. But the container is identity-blind, so the responder can't verify the claim — it's trusted purely because rung 4 vouched for the hop that carried it. If that hop is compromised, the claim is forgeable. It relocates the trust. It doesn't close the gap. I'm not shipping something that only looks like a control.
Inbound JWT via AgentCore Identity. This is the sound fix — verified claims arriving inside the model's world, checkable there. It needs an identity provider and real token plumbing, which is more than half a day, which is more than this build had. It's the correct next step, and it's unbuilt.
Segmentation. Network-enforced, nothing to spoof, and it genuinely shrinks the blast radius. But it doesn't close the caveat, it just makes the room smaller. The turned insider still owns whatever's in the room with it.
So the honest position: coarse authorisation is available at the network today. Fine-grained, intent-aware authorisation needs verified claims inside the model's world, and the identity-blind container doesn't offer that yet.
A few other limits, plainly. Everything here is single-account, single-region — cross-account changes the policy story and I haven't tested it. I never tested an indirect injection, where the malicious instruction arrives in tool output rather than user input, which is honestly the attack I'd expect to actually work against a well-behaved model; that's my next experiment. And those eight refusals were one model on one evening, not a benchmark.

Climb it yourself

One template, one parameter:
1
sam deploy --parameter-overrides Rung=1 # then 2, then 3, then 4
Same two agents each time, different front door. The probe scripts run all three actors against whichever rung is live, so you can watch that table fill in on your own account — including the last column, which is the one worth sitting with. Teardown is one script, and remember the twenty minutes.

References

  • Private connectivity patterns for Amazon Bedrock AgentCore Gateway targets — AWS Networking & Content Delivery blog
  • Secure ingress connectivity to Amazon Bedrock AgentCore Gateway using interface VPC endpoints — AWS ML blog
  • AgentCore VPC interface endpoints — the bedrock-agentcore endpoint documentation
  • Exposing an AgentCore agent via API Gateway — aws-samples
  • VPC endpoint policies and API Gateway IAM authorisation — AWS documentation
I still believe the sentence I started with. You can't prompt-inject a security group, and rung 4 proved it: a compromised agent holding valid credentials of its own gets stopped by the network before the model is even consulted.
But the thing I actually learned is the sentence underneath it. The network can tell you, with certainty, who is asking. It cannot tell you why.
Everything on this ladder stops the wrong caller. The moment the right caller goes wrong, I run out of rungs.
That's what I'm building next.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article