Why Airlock#
Airlock is an MCP server. LLM agents run SQL against a per-user, PII-masked DuckDB snapshot of your production data, generated inside your VPC. The snapshot is read-only, and your prod credentials never leave your network.
The two unsafe defaults#
Today, if you want to ship an AI feature that touches real user data, you have two options.
1. Give the LLM your prod credentials#
Wire the agent into a read-only Postgres role and let it write SQL
directly. One prompt injection at the LLM — a malicious support
ticket, a poisoned merchant name, a clever jailbreak in a user
message — and the agent runs DROP TABLE users or SELECT * FROM users; INSERT INTO attacker_log …. The CVE catalog of agentic-AI
breaches is mostly this. You are vulnerable to prompt injection
attacks capable of draining your entire database.
Row-level security and similar in-DB controls don't fix it. The schema
still leaks, RLS bypasses are common (recursive CTEs, leaky
predicates, materialized-view gaps), and a single missed
FORCE ROW LEVEL SECURITY and Alice's session reads Bob's rows.
2. Pre-build a narrow tool for every question#
Decline to expose SQL at all. Hand-roll a function-tool API:
get_user_orders(user_id), get_user_balance(user_id),
list_recent_payments(user_id). This is what every consumer MCP
integration looks like today.
Every new question requires a new tool. Each tool is a new code path, a new authz check, a new audit entry to design — and the long tail of "what might a user actually ask?" never closes. Agents are limited to your imagination. You've reinvented a query language, badly.
Airlock's third option#
Airlock allows the agent to perform free-form SQL on a single user's scoped data so that nothing it does touches prod.
When a session starts, the worker exports a small DuckDB file
containing only the rows tied to a single subject_id, with PII
masked at export. The agent runs SQL against that file. After the
configured TTL window (default 5 minutes) the file is reaped — and
on worker exit, every snapshot is wiped atomically because they live
in tmpfs, not on persistent disk.
- Free-form SQL — agents write whatever query they need; you don't pre-write a tool for every question.
- Per-user scope — the snapshot only contains rows tied to the row id passed in; the agent cannot read other users' data even if the LLM is fully compromised, because those rows are not in the file.
- Masked + ephemeral — PII is hashed or nulled at export per
airlock.yaml; the DuckDB lives in tmpfs and is reaped after a short TTL.
The blast radius of any single agent session is one snapshot's masked rows, for the duration of the TTL window.
Concrete example#
Imagine a fintech support agent powered by an LLM. End-user Alice opens a chat and asks, "what was my biggest charge last month?"
Without Airlock. The agent has a query_transactions tool wired
to a read-only Postgres role. That role can SELECT from
transactions for every customer — Alice, Bob, the CEO, everyone.
A prompt injection in a support ticket, a malformed merchant name, or
a clever jailbreak is now a data-exfiltration primitive against the
whole table.
With Airlock. When Alice's chat starts, the worker exports a
~5 MB DuckDB file containing only Alice's rows from transactions,
accounts, and the handful of related tables — with PAN, SSN, and
DOB columns masked per airlock.yaml. The file lives in tmpfs at
<data_dir>/alice.duckdb. The agent runs SQL against that. After
the TTL window expires, the reaper deletes the file — and a worker
restart wipes everything in tmpfs unconditionally. No prompt injection
in the world hands the agent Bob's data, because Bob's data was never
in the sandbox.
Before vs after#
The blast radius shrinks from "everything the DB role can see" to "one snapshot's masked rows, gone at the end of the TTL window."
BEFORE AFTER
┌──────────────────────┐ ┌──────────────────────────────┐
│ Agent │ │ Agent │
│ (LLM + SQL tool) │ │ (LLM + MCP tool) │
└──────────┬───────────┘ └───────────────┬──────────────┘
│ SELECT * │ MCP JSON-RPC
│ FROM transactions │ query("SELECT ...")
▼ ▼
┌──────────────────────┐ ┌──────────────────────────────┐
│ Prod DB │ │ Airlock control plane │
│ ┌────────────────┐ │ │ (routes, audits, no data) │
│ │ alice rows │ │ └───────────────┬──────────────┘
│ │ bob rows │ │ │ WSS tunnel
│ │ carol rows │ │ ▼
│ │ ... │ │ ┌──────────────────────────────┐
│ │ (every │ │ │ Worker (in your VPC) │
│ │ customer) │ │ │ ┌────────────────────────┐ │
│ └────────────────┘ │ │ │ tmpfs: │ │
│ │ │ │ alice.duckdb (~5 MB) │ │
│ Reachable by any │ │ │ - alice's rows only │ │
│ prompt injection. │ │ │ - PII masked │ │
│ │ │ │ - reaped on TTL │ │
│ │ │ │ expiry / restart │ │
│ │ │ └────────────────────────┘ │
└──────────────────────┘ └──────────────────────────────┘
Blast radius: every row. Blast radius: one snapshot,
masked, gone on TTL expiry.
Why DuckDB#
DuckDB is the right shape for the snapshot:
- Column-oriented — analytical queries (
SUM,GROUP BY, window functions) run fast on the kinds of questions LLMs actually ask. - Single-file — the snapshot lifecycle is
cpandrm. Trivial to reason about, trivial to wipe atomically on worker exit. - Embedded — no extra process, no separate port, no extra failure domain. The worker links DuckDB in-process and queries it directly.
- Native Postgres ATTACH — the export pipeline reads source rows
with
ATTACH '...' AS pg (TYPE postgres)and writes the masked result into a local DuckDB file in one pass, without staging through CSV or an intermediate process.