Architecture and Least Privilege: The Real Defense

Everything before this post raised the probability barrier against injection. This post lowers the impact — and impact is what actually protects you. The load-bearing defense against prompt injection isn't a prompt or a filter; it's an architecture where a fully-hijacked model still can't do anything catastrophic, because it was never granted the power to.

This is the most important post in the series. If you take one idea from all eight, take this one: since you cannot stop the model from being fooled, design so that a fooled model is harmless. Every technique so far reduces how often injection succeeds. Architecture reduces what success is worth to the attacker — and unlike the probabilistic prompt-level defenses, architectural limits are enforced, not persuaded.

Least privilege: the core principle

The oldest principle in security applies directly: grant the model the minimum capabilities it needs, and nothing more. Before wiring any tool, data source, or permission to an LLM, ask the question from post 1 — “if the model executed the worst possible instruction right now, using this capability, what’s the damage?” — and grant accordingly.

Concretely: - Read vs. write. A model that can read your database and one that can modify it are worlds apart in risk. Default to read-only. Grant write access only where essential, and scope it tightly (specific tables, specific operations). - Scope every tool. A “send email” tool that can email anyone is a data-exfiltration weapon in a hijacked model. A tool that can only email the authenticated user is far safer. A “run SQL” tool is dangerous; a “look up order by ID” tool is not. Prefer narrow, purpose-built tools over general, powerful ones — the narrowness is the security. - No ambient authority. The model should never operate with broad standing credentials (an admin API key, root access) “just in case.” It gets exactly the scoped tokens for the task at hand.

Least privilege turns injection from a catastrophe into an annoyance. If the worst a hijacked model can do is return a wrong answer or call a read-only lookup with odd arguments, injection stops being a security incident.

The confused-deputy trap and privilege inheritance

Recall from post 2 that indirect injection is a confused-deputy attack: the model, acting with someone’s authority, is tricked into misusing it. The critical design error is letting the model inherit privileges the requester shouldn’t be able to exercise through it.

The rule: the model should act with the privileges of the least-trusted party in the chain, not the most-trusted. If a user asks the assistant to summarize a web page, and that page contains injected instructions, the assistant is now taking orders from the page’s author — so it must not be able to do anything the page’s author shouldn’t be allowed to do. In practice: - Actions the model takes on a user’s behalf run with that user’s permissions, never elevated ones — so injection can’t cross a privilege boundary. - When the model processes third-party content (retrieved docs, web pages, emails), treat any “instruction” in that content as coming from an anonymous, untrusted party — because it is. - Never let content the model read silently expand what the model can do.

This is why “the model has an admin API key so it can help with anything” is the single most dangerous pattern in LLM applications: it hands the most-trusted authority to a component you’ve already established can be commandeered by the least-trusted party.

Human-in-the-loop for consequential actions

For actions that are irreversible, costly, or sensitive — sending money, deleting data, emailing externally, publishing, executing code with side effects — the strongest control is simple: require explicit human confirmation.

A hijacked model can propose “transfer $10,000 to account X,” but if a human must approve the transfer with the details shown clearly, the injection dies at the confirmation step. The human is the un-injectable check. Design confirmations so they: - Show the actual action and its concrete parameters (the real recipient, the real amount), not a model-generated summary the model could also manipulate. - Are required for the consequential actions specifically, so you don’t train users to click through everything (confirmation fatigue defeats the control).

Human-in-the-loop is friction, so spend it where impact is high. But for genuinely dangerous actions, it is the most reliable defense there is, because it doesn’t depend on the model resisting anything.

The dual-LLM pattern: quarantine the untrusted

A powerful architectural pattern for cases where a model must process untrusted content and take privileged action: split the work across two models with an enforced boundary between them (an idea articulated by Simon Willison as the “dual LLM” pattern).

The two communicate through a constrained interface controlled by your code, not by free text — the quarantined model returns structured, validated data (never instructions) that the privileged model consumes as data. Because the model that can act never reads attacker-controlled text, and the model that reads attacker text can’t act, an injection in the content has nothing to hijack. It’s more complex to build, but it’s one of the few designs that genuinely contains indirect injection rather than just discouraging it.

Related patterns push the same idea further: generating code/plans in a sandbox with no network or secrets, having the model emit a constrained action request that deterministic code validates against a policy before executing, and keeping tool outputs (which can themselves carry injection) on the untrusted side of the boundary.

Designing the whole system to contain injection

Pulling the architecture together, a system that stays safe under injection tends to have these properties:

Notice what all of these share: they don’t require detecting the attack. They keep you safe whether or not the injection is recognized, whether it came from the user or a document, whether the model was fooled or not. That is the whole game. Prompts and filters try to recognize and resist the attack, which can’t be done reliably; architecture removes the reward for the attack, which can. Build the containment into the system, and the unsolvable model-level problem stops being your problem.

Key takeaways

Further reading

Sources & References