Vocabulary for this course

The terms the chapters assume, in plain words. You can open this from any chapter.

allow-list
A list of what is permitted, with everything not on it refused by default.
fail closed
Behaving so that an error blocks the action rather than letting it through unchecked.
false refusal
A refusal of a request that was actually allowed, which costs a real user an answer.
incident
An event where the system did something harmful or outside policy, handled from detection to review.
least privilege
Giving each part of the system only the access it needs for its current job, and no more.
moderation
Checking content against rules about what is acceptable, and blocking or flagging what is not.
normalisation
Rewriting input into one standard form, such as lower case or expanded characters, before checking it.
PII
Information that can identify a person, alone or combined with other details, such as a name or address.
policy
A written rule that says what the system must allow, block or escalate, kept in code with a change history.
prompt injection
Text in input or retrieved content that tries to make the system follow the instructions of an attacker instead of yours.
provenance
The record of where a piece of content came from and how it reached the system.
red team
People or scripts that attack the system on purpose to find failures before real users do.
refusal
A reply that declines a request and says what the system can do instead, without revealing protected details.
threat model
A written list of what you are protecting, who might attack it and how, made before you build.

Guardrails and safety

Threat model for an LLM feature

A threat model is a list of what you are protecting, who can reach it, and where the model sits between them. Without one, every guardrail is a guess about which danger matters.

Part 1 of 10

Intermediate14 min readBack to Courses

Assets, effects and blast radius

Start with what the feature can touch, not with the model. A support assistant can read customer records, write a reply, issue a refund and spend tokens; a coding assistant can read a repository, edit a file and run a command. Those are the assets and the effects, and they exist whether the model is a transformer or a lookup table.

For each effect, write the blast radius: one user or every tenant, reversible or not, who notices and how quickly. Drafting an email and sending it are different products. The blast radius is what decides how much control the effect needs, and it is the first thing a reviewer should ask about.

Write the asset list in nouns that a lawyer and an engineer both recognise: the customer record, the refund, the repository, the model context. A list of abstractions such as data or trust cannot be reviewed, because nobody can point at it or say whether it is in scope.

Actors, entry points, trust boundaries

The actors are the people and systems that can influence the feature: the user, another tenant, a retrieved document, a tool result, the author of a plugin, an insider with log access. The entry points are where their text arrives: the input box, an upload, a fetched page, a webhook, a previous turn. List them; the list is short and it is usually incomplete.

Then draw the boundary. The model sits on it, not inside the trusted zone. Everything it reads may be attacker-controlled, and everything it emits is untrusted until a check says otherwise. That one sentence reorganises the design, because it puts the model in the same category as a browser request rather than the same category as your own code.

A trust boundary is only useful if something crosses it. Mark the crossings: a document entering the prompt, a model output entering a template, a tool result entering a decision. Each crossing is a place where a check can live, and a boundary with no crossing is a diagram.

A threat as data

from typing import Literal
from pydantic import BaseModel, Field


class Threat(BaseModel):
    asset: Literal["customer_record", "refund", "source_code", "context"]
    entry: Literal["user", "document", "tool_result", "plugin"]
    effect: Literal["read", "write", "send", "spend"]
    reversible: bool
    tenants: Literal["one", "many"]
    control: str = Field(min_length=4)


THREATS = [
    Threat(asset="customer_record", entry="document", effect="send",
           reversible=False, tenants="one", control="provenance check before send"),
    Threat(asset="refund", entry="tool_result", effect="spend",
           reversible=False, tenants="many", control="amount cap and human approval"),
]

Score the capabilities

Run the inventory and look at the order. Then add a capability that reads every tenant’s data and rescore; decide whether it belongs in the product at all.

Output
Click "Run" to execute your code

Likelihood times impact, roughly

You are ordering work, not doing actuarial maths. Give impact a small integer from one to three and reachability the same, then multiply. A remote path that any user can take outranks a theoretical one that needs a compromised insider, and the product makes that ordering explicit enough to argue about.

Re-score after each control. A control that only logs does not reduce the score, because it changes what you know afterwards, not what can happen. A control that blocks, caps or requires approval does, and the new score is what tells you whether the next control is worth building.

The score is not a promise of safety; it is a queue. Two people scoring the same feature will disagree by a point, and that is fine as long as the disagreement is about impact or reach rather than about whether the effect exists at all.

The model is never the control. A sentence in the system prompt is a request, not an enforcement point; the control is the code that runs after the model has spoken.

Keep it alive

A threat model is a file in the repository, reviewed when a tool, a data source or a permission is added. The tool list is the attack surface, so a diff to the tool list is a diff to the model. If the review is a document nobody opens, it is a compliance artefact rather than a design tool.

After an incident, add the case to the red-team suite in chapter six and re-score. The model is a living list, not a certificate, and the version that matters is the one that matches the code on main.

Assign an owner. A threat model with no owner is updated by nobody, and the first sign is a tool that appeared in production without a line in the document. The owner reviews the diff that adds it, the way they would review a new permission.

Mark the trust boundary

Run the trace and find the row that is both untrusted and asking for an effect. Then add a tool result that mentions a refund and confirm it is flagged too.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Where does a threat model for an LLM feature start?