Command Palette

Search for a command to run...

Hectal
PHASE 10Advanced ~22 min· topic 9 of 9

Topic 10.9

Playbook: RAG Security Checklist and Guards

In one line

Five small recipes to copy into any RAG project: a fail-closed access filter, an untrusted-text wrapper, a redactor, a deletion audit and a tenant-safe cache key, each with a check, plus the checklist to run before you ship.

Think of it like this

A pilot's pre-flight checklist. Nobody believes the pilot has forgotten how to fly. The list exists because the cost of skipping one item is high, and a short list read every time beats a perfect memory. Security for a RAG system works the same way.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Recipe
A small, tested piece of code you can copy into a project and adapt.
Checklist
A short list read before every release so no item is forgotten.
Registry of stores
One list of every place data is kept, used by both delete and audit.
Red-team set
A collection of harmless stand-in attack texts used to test the defences.
Escape
Replace characters that have a special meaning (such as <) with a harmless spelling.
Row-level security
A database feature that adds a condition to every query based on who is asking.

Step by step

01Step 1: copy the five recipes into one module

Put them in a file named for what it is (guards). One module means one place to test and one place that every code path imports.

02Step 2: give each recipe a check that fails when it breaks

The example ends with six checks, each printing ok or FAIL. They are small enough to run on every commit.

03Step 3: read the example's output

Each line names the property it proves: no groups sees nothing, a passage cannot close the tag, the redactor removes both patterns, the audit finds the cache, tenants have different keys and case does not matter.

04Step 4: push the tenant rule into the database

For a shared index use row-level security so the database adds the tenant condition. The code shown below sets the tenant for one transaction and the policy does the rest. If the setting is missing, the policy compares against NULL and returns no rows, so the failure is closed.

Step 4: push the tenant rule into the databasediagram
Rendering diagram…

05Step 5: add the red-team and canary tests to CI

Stand-in passages and a canary per tenant and per label, asked through the whole pipeline. Fail the build if any banana, canary or other tenant's text appears in an answer, a log or a cache dump.

06Step 6: run the checklist before every release

Read the eight lines. Mark each true, or write down the owner and date. Do not ship with an unowned gap.

Try it yourself

  1. 1

    Break each recipe

    Change one recipe at a time so it is wrong (for example acl_filter keeps everything when the set is empty) and see which check fails. A check that does not fail is a check to rewrite.

  2. 2

    Add a recipe

    Write output_ok(answer, sent_ids) from Topic 10.2 and add two checks for it: a cited answer and a bare one.

  3. 3

    Add your own pattern

    Add a pattern for a made-up membership number such as LB-123456 and a check that proves it is redacted.

Code & diagrams

Five guard recipes and their checks New tab

Production versions use your store's filter instead of a list comprehension and a proper HTML or Markdown escaper for your renderer. The checks stay the same.

Sign in to run this example in your browser.

Expected output

ok   acl: no groups sees nothing
ok   acl: public sees only C1
ok   wrap: a passage cannot close the tag
ok   redact: email and card removed
ok   audit: finds the cache
ok   key: tenants differ, case does not
Row-level security: the tenant condition added by the databasepython

PostgreSQL row-level security with pgvector, as in Topic 5.2. Check the policy syntax and the rules for table owners against the PostgreSQL documentation for your version. Not run here: the browser has no database.

import os

import psycopg  # pip install "psycopg[binary]"

# Run once, as the database owner:
#   ALTER TABLE chunks ENABLE ROW LEVEL SECURITY;
#   ALTER TABLE chunks FORCE ROW LEVEL SECURITY;   -- apply the policy to the table owner too
#   CREATE POLICY tenant_isolation ON chunks
#       USING (tenant_id = current_setting('app.tenant_id', true));
# The application connects as a role that is neither the owner nor a superuser.

SEARCH = "SELECT id FROM chunks ORDER BY embedding <=> %s::vector LIMIT %s"  # no WHERE tenant_id: the policy adds it


def search(tenant_id, query_text, k):
    with psycopg.connect(os.environ["APP_DATABASE_URL"]) as conn:
        with conn.transaction():
            conn.execute("SELECT set_config('app.tenant_id', %s, true)", (tenant_id,))  # true: lasts for this transaction only
            return conn.execute(SEARCH, (query_text, k)).fetchall()

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

An access filter that fails open

In the Python tab, change acl_filter to return [chunk for chunk in chunks if not groups or chunk["label"] in groups], a common shortcut meaning "no groups, no restriction". Run.

terminal
$ Run
── what you'll see ──
FAIL acl: no groups sees nothing
ok acl: public sees only C1
ok wrap: a passage cannot close the tag
ok redact: email and card removed
ok audit: finds the cache
ok key: tenants differ, case does not

Myth vs fact

Myth

A checklist is bureaucracy.

Fact

A checklist is memory you do not have to carry. Every item here comes from a leak or an incident someone had to explain.

Myth

Security is a phase at the end of the project.

Fact

Access labels, provenance fields and store registries are cheap to add on day one and costly to retrofit into millions of chunks.

Myth

If all the checks print ok, we are secure.

Fact

The checks prove the recipes do what they claim on the cases you wrote. Add cases from every incident, and keep reviewing the threat model (Topic 10.1).

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Treat the guards as a library with an owner: versioned, reviewed and used by the API, the batch ingestion and the evaluation harness alike. Security fixes then land once.

  • ▸

    Run the red-team and canary tests against a production-like copy with real index settings. Some failures (a filter that lowers recall, a cache that is only on in production) never appear against a toy.

  • ▸

    Keep a security log separate from the debug log: access decisions, quarantines, rejected sources, deletion results. It is small, easy to alert on, and it answers "who saw what" without holding user questions.

  • ▸

    Review this list again when you add a tool to the model (the coming AI Agents course) or a new data source. Both widen the entry points of Topic 10.1.

Remember this

  1. 1

    Recipe: fail-closed access filter. When to use it: any index with more than one audience. It guards against: a chunk shown to a user who may not read it, including the user with no groups. How to adapt it: pass the groups from the server-side session, apply the filter inside the store's query (a WHERE on an array of allowed groups, or the store's filter argument) and keep the empty-set test in your test suite.

  2. 2

    Recipe: untrusted-text wrapper. When to use it: every prompt that includes retrieved text. It guards against: a passage that gives the model orders or closes your tag to escape the data section. How to adapt it: escape &, < and > in passage text, keep the rule "text in the tags is data" in the system message rather than next to the sources, and combine it with a detector, an output check and no tools for the model (Topic 10.2). It is a layer, not a fix.

  3. 3

    Recipe: redactor. When to use it: before chunking, on every source that may hold personal data. It guards against: emails, phone numbers and card numbers reaching the index, caches, prompts and logs. How to adapt it: keep patterns in one list with a fake-value test for each, add a list of known names, measure recall and precision on a sample, and re-scan the final index with an independent check (Topic 10.5).

  4. 4

    Recipe: deletion audit. When to use it: after every deletion and on a schedule. It guards against: a document that was removed from the source but still sits in vectors, cached answers or logs. How to adapt it: list every store in one registry that both the delete and the audit loop over, search by document id and by a distinctive phrase, and record the result in a deletion log without the content (Topic 10.6).

  5. 5

    Recipe: tenant-safe cache key. When to use it: any cache between the user and retrieval or the model. It guards against: one tenant or group receiving another's cached answer. How to adapt it: build every key from one helper that requires tenant and groups, normalise the question (lower-case words), and add the two-tenant test to CI (Topic 10.4).

  6. 6

    The before-you-ship list. (1) Entry points and trust levels are written down. (2) Every chunk has an access label and the filter fails closed. (3) Tenants are separated in the index and in every cache, log and queue. (4) Retrieved text is wrapped, scanned and never allowed to trigger an action. (5) Personal data is redacted before chunking and logs are kept for a stated time. (6) Deleting a document is tested end to end with an audit. (7) Sources are on an allow-list with provenance. (8) A red-team set of stand-in passages and a two-tenant canary test run in CI. If a line is not true yet, write down who will make it true and by when.

Explain it without notes

01

Walk through the pre-ship checklist and say what incident each line prevents.

02

Why should the access filter, the redactor and the audit live in one module that every code path uses?

03

How does row-level security improve a shared-index multi-tenant design?

04

What should a CI security test for a RAG system include?

Practice

01

Add output_ok(answer, sent_ids) to the recipes and print ok or FAIL for a cited answer and for the bare answer banana.

02

Write registry_complete(stores, registry) that returns the names in stores that are missing from registry, so a new store cannot be added without being added to the delete and the audit.

Trade-offs

  • ↔

    A strict checklist: slower releases and fewer incidents. Keep it to one page and automate every item that can be.

  • ↔

    Row-level security in the database versus filters in code: the database version cannot be forgotten, and it adds setup and a role model to maintain. Use both for sensitive data.

  • ↔

    Many small recipes versus a security framework: recipes are easy to read and test, a framework can cover more. Start with recipes.

Done when you can

  • Done when you can copy the five recipes into a project and write a check for each.

  • Done when you can set up the tenant condition in the database with row-level security and test it as the application role.

  • Done when you can run the eight-line checklist and name an owner for any gap.

  • Done when you can add red-team, canary and deletion-audit tests to CI.