Defense in depth for LLM apps
Layer input checks, context construction, model guardrails, output validation, capability limits and monitoring so no single failure is catastrophic: which layers are deterministic, how layers map to attacks, fail-open versus fail-closed, guardrail models and a practical order of work.
No single control stops every attack. Defense in depth stacks independent layers so that when one fails (and each will sometimes fail), another catches the problem. The goal is not a perfect wall but a system where no single failure is catastrophic, and where an attacker must defeat several different kinds of control at once.
- Input controls · auth, rate and size limitsready
- Context building · no secrets, per-user data, untrusted isolatedready
- Model guardrails · prompt hardening, safety classifierready
- Output validation · schema, escaping, secret scanready
- Capability limits · least privilege, confirmation, sandboxready
- Monitoring · logs, alerts, kill switch (detects)ready
Deterministic layers (limits, authorisation, validation) are reliable. Probabilistic ones (prompt, classifiers) sometimes fail, which is why you stack them.
#The layers
- Input controls. Authentication, rate limits, size limits, file-type checks, and optional injection or abuse classifiers.
- Context construction. Include only data the user is authorised to see; delimit untrusted text; keep secrets out.
- Model-level guardrails. A well-chosen system prompt, a safety-tuned model, and policy classifiers (for example Llama Guard-style models run locally) to screen prompts and outputs.
- Output validation. Schema checks, escaping, URL allowlists, PII and secret scanners.
- Capability limits. Least-privilege tools, server-side authorisation, sandboxes, confirmation for consequential actions.
- Monitoring and response. Logging of tool calls, anomaly alerts, kill switches, and an incident process.
Each layer in a sentence of detail:
| Layer | Examples | Stops |
|---|---|---|
| Input controls | API keys, per-key rate limits, max prompt tokens, upload type and size checks | Abuse, denial of wallet, oversized or malformed input |
| Context construction | Per-user retrieval filters, no secrets in prompts, untrusted text delimited and labelled | Cross-user leakage, secret extraction, some injection |
| Model guardrails | Hardened system prompt, safety classifier on input and output, refusal behaviour | Casual misuse, many policy violations |
| Output validation | JSON-schema checks, HTML escaping, link allowlists, secret and PII scanners | XSS, exfiltration through links and images, malformed output reaching systems |
| Capability limits | Narrow tools, per-user authorisation, sandbox, confirmation for sends and deletes | Damage from a fooled model |
| Monitoring and response | Tool-call logs, alerts, kill switch, runbook | Prolonged or unnoticed incidents |
#Which layers are deterministic?
| Layer | Deterministic? | Reliability |
|---|---|---|
| Auth, rate limits, size limits | Yes | High |
| Server-side authorisation of tools and data | Yes | High |
| Schema validation, escaping, allowlists | Yes | High |
| Sandboxing | Yes | High |
| System prompt | No | Low to medium |
| Safety classifiers | No (probabilistic) | Medium |
| Model alignment | No | Medium |
| Monitoring | Detects, does not prevent | Depends on alerts being acted on |
Put your trust in the deterministic layers and treat probabilistic ones as a way to reduce volume and add friction. A useful test of any control: would it still work if the model were fully controlled by the attacker? If yes, it is a real boundary. If no, it is a speed bump.
#Why layers multiply, and when they do not
If three layers each independently stop 90 percent of attempts, the chance an attempt passes all three is 0.1 × 0.1 × 0.1 = 0.1%. That is the Swiss-cheese idea: holes rarely line up.
The catch is independence. Two layers that fail for the same reason (both are prompts, both are the same classifier family, both rely on the model noticing something) fail together. A hardened system prompt plus a prompt-based guard model that uses the same base model are highly correlated. Mix different kinds of controls: a deterministic permission check, a schema validator, a rate limit and a classifier fail in unrelated ways.
#Mapping attacks to layers
Use a table like this in your design review. It shows which attacks have only one layer of protection (a red flag):
| Attack | Input | Context | Guardrails | Output | Capability | Monitoring |
|---|---|---|---|---|---|---|
| Giant prompt / cost attack | ✓ | ✓ | ||||
| "Print your system prompt" | ✓ (no secrets) | ✓ | ✓ (scan) | ✓ | ||
| Hidden instruction in a document | ✓ (isolate) | ✓ | ✓ (no tools) | ✓ | ||
| "Delete all records" via tool | ✓ | ✓ (authorise, confirm) | ✓ | |||
| Script tag in output (XSS) | ✓ (escape) | |||||
| Cross-tenant data access | ✓ (filter) | ✓ (authorise) | ✓ |
Rows with a single tick need another layer. Try it live in the diagram above: switch layers off and see which attacks get through.
#Guardrail models
Open classifier models can screen both inputs and outputs for categories such as violence, self-harm, illegal activity and prompt-injection patterns. They:
- add latency and cost (an extra model call),
- produce false positives (blocking legitimate users) and false negatives,
- need evaluating on your traffic, since categories and tone differ by domain,
- are themselves attackable (an injection that targets the classifier).
Tips: run them in parallel with the main call where possible, use a small fast model, tune thresholds with data, log what they block, and treat them as one layer among several. For structured, narrow apps, rules and schemas often beat classifiers: if the output must be one of five labels, enforce that with a grammar and skip the classifier (structured output).
#Fail safely
Decide what happens when a layer fails or is unavailable:
| Risk of the feature | Policy when a safety layer is down |
|---|---|
| High (payments, deletion, external messages, medical or legal output) | Fail closed: refuse or route to human review |
| Medium (internal summaries with private data) | Fail closed for sensitive data; degrade to a restricted mode |
| Low (public FAQ answers) | Fail open with a notice, and alert |
Write the decision down per layer. The worst outcome is the accidental one: a classifier timeout silently returning "safe".
#Kill switches
Every feature with side effects needs a way to turn it off fast: a feature flag that disables a tool, a switch that forces confirmation for all actions, a way to revoke a model release. Test the switch before you need it, and make sure on-call knows where it is (reliability patterns).
#Security testing as a layer
Include adversarial test cases (injection, leakage, abuse) in your release evaluations, track the success rate over time, and re-run them whenever the model, prompt or tools change. Test layers individually (does the output scanner catch a planted canary? does the authoriser reject a cross-user ID?) and together (what is the end-to-end attack success rate?). The red-team lab builds a small harness, and CI shows how to run it on every change.
#A worked example: a support assistant
| Concern | Layer and control |
|---|---|
| Anonymous users burning GPU | Input: auth, per-key limits, max tokens |
| One customer reading another's data | Context: per-customer retrieval filter. Capability: tools take identity from the session |
| Hidden instructions in uploaded PDFs | Context: PDFs processed in a tool-less call, validated summary passed on |
| Model leaks an internal code | Context: no secrets in the prompt. Output: scan for the canary |
| Phishing link in a reply | Output: link allowlist, markdown sanitiser |
| Refund abuse | Capability: refund tool creates a pending request, amount cap in code, human approval above threshold |
| Unnoticed attack | Monitoring: tool-call logs, alert on refund-request spikes, kill switch for the refund tool |
Seven concerns, each with a primary deterministic control, and probabilistic layers on top.
#Practical order of work
- Remove secrets from prompts and set up authorisation on tools and data (largest risk reduction)
- Limit tools and add confirmations
- Validate and escape output
- Add rate limits and size caps
- Add monitoring and logging with redaction
- Add classifiers and prompt hardening on top
Items 1 to 4 are mostly ordinary engineering and give most of the protection. Items 5 and 6 add detection and polish.
#Common mistakes
- Counting probabilistic layers as boundaries.
- Stacking correlated layers (three prompts, one model) and calling it depth.
- A guardrail with no false-positive measurement, which annoys users into disabling it.
- No kill switch.
- Defending only the input, ignoring outputs and tools.
- No tests, so layers silently decay as prompts and models change.
#Exercise
An internal assistant summarises uploaded contracts and can email the summary to colleagues. Today the only defence is a system prompt saying "never send confidential data outside the company".
- Which kind of control is that, and how reliable?
- Propose four layered controls.
Answers:
- A probabilistic, model-level control: low to medium reliability, and it can be bypassed by an instruction hidden in a contract.
- (a) Capability: restrict recipients to company-domain addresses in code and require confirmation showing recipient and body. (b) Context: process contracts in a tool-less call producing a schema-validated summary. (c) Output: scan the summary for secrets and external URLs. (d) Monitoring: log every send, alert on unusual volume or new recipients, and provide a kill switch for the email tool.
Quick quiz
+10 XP per correct answer, +25 XP for a perfect score
1. Why use several defensive layers?
2. Which layer remains effective even when the model is completely fooled?
3. Three layers each stop 90% of a given attack, independently. What fraction gets through all three?
4. A guardrail classifier is unavailable. For a feature that can send money, what should happen?
5. Which is the most valuable first step for a new LLM app?