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 cp and rm. 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.