Quickstart#
This walks you through getting Airlock running end-to-end against your own Postgres in about 10 minutes. By the end you'll have a worker dialed into the control plane from inside your VPC, an API key minted from the console, and a successful MCP tool call from your agent landing in the audit log.
We use the terms worker, control plane (CP), console, tenant,
and snapshot throughout — if any are unfamiliar, the
Concepts glossary defines each in one paragraph.
A snapshot is scoped to a row id from your root_table — typically
one end-user's record.
Self-hosting the control plane? Start with Self-host quickstart (docker compose) to get the full CP + console stack running on your machine in one command — that's the fastest path from zero to a working console you can log into. For production deployment patterns (Kubernetes, multi-region, hardening, ops), see Self-hosting the control plane. Both pages are only linked from here, so bookmark whichever fits.
Overview#
You're going to do four things, in order:
- Deploy the control plane at a URL your worker can reach (or accept the URL your operator gives you for hosted CP).
- Log into the console with your operator token.
- Add a worker. Mint a one-time enrollment token in the console,
paste it on a host inside your VPC, and watch the worker's status
flip to
connected. - Mint an API key and point your LiteLLM (or other MCP-speaking)
gateway at
<cp_url>/mcp/<tenant_slug>with the key as a bearer.
The whole flow involves three different credentials at three different points. They get confused constantly, so the Authentication tokens section below lays them out side-by-side before any commands run.
Prerequisites#
- A Postgres instance the worker can read from. Don't point at
production directly — use a read replica, a
pg_dump-restored staging copy, or any DB you're comfortable letting a sandboxed reader hit. - A Linux host (k8s pod, ECS task, single VM) inside your VPC
that can reach Postgres on
:5432and reach the CP URL on:443(outbound only — no inbound holes required). - A CP URL + operator token. If we're hosting CP for you, we send
these when you sign up. If you're self-hosting, you'll set the
operator token as
AIRLOCK_CP_OPERATOR_TOKENwhen you deploy CP (see Step 1).
Authentication tokens#
Three different credentials gate three different surfaces. Each is minted in a different place, lives a different length of time, and gates a different operation. Skim this table once before running through the steps — every command below references one of these by name.
| Token | Lifetime | Where minted | Where used | What it gates |
|---|---|---|---|---|
| Operator token | Permanent (rotated by env-var change) | Set as AIRLOCK_CP_OPERATOR_TOKEN env var on CP at deploy time | Console login form (top of /login); CP admin API calls as Authorization: Bearer … | Read/write access to all CP admin routes — tenant create, API-key mint, enrollment-token mint, config edits |
| Worker enrollment token | One-time use, short-lived (default 1h, max 24h) | Console /workers "Add worker" form, OR airlock-cp enrollment create --tenant <id> --worker-id <id> (CLI) | airlock register --enrollment-token … on the worker host (one time only) | Lets a freshly-installed worker exchange its locally-generated public key for a permanent identity in the CP registry |
| API key | Permanent (revocable; soft-delete) | Console /api-keys "Mint key" form, OR airlock-cp api-key create --tenant <id> --name <name> (CLI) | LiteLLM proxy / MCP client Authorization: Bearer ak_live_… against <cp_url>/mcp/<tenant_slug> | Tenant-scoped MCP access for the agent — tools/list, execute_sql, get_schema, null_rates |
A few sharp edges:
- The operator token is one shared bearer for the whole CP today (single-tenant on the operator side; OIDC / per-operator auth is on the roadmap). Treat it like a root password.
- The enrollment token never authenticates anything except a one-time identity exchange. It doesn't grant query access. Lose it and you mint a fresh one — they're cheap.
- The API key plaintext (
ak_live_…) is shown once when you mint it. CP only stores the Argon2id hash. Lose the plaintext, mint a new key.
The worker also has its own keypair. Separate from the three tokens above, the worker generates an Ed25519 keypair locally on first boot. Only the public half is uploaded to CP (during enrollment); the private half stays on the worker host and signs every WSS handshake. You don't manage it directly —
airlock registerwrites it to/etc/airlock/worker_ed25519.pemmode 0600. See Architecture.
Step 1 — Deploy the control plane#
If your operator runs a hosted CP, skip to
Step 2 — you'll be given a URL like
https://cp.airlocklabs.ai and an operator token, and that's
everything you need.
If you need to self-host CP (compliance, data residency, single-tenant audit guarantees), see Self-hosting the control plane. The short version: a single Python/Starlette service with a persistent volume; one Fly machine works for most teams. The page covers when self-host is the right pick, the reference Fly deploy, env-var reference, and operations.
Step 2 — Log into the console#
The console is a separate service from the control plane. With
hosted CP, you use our hosted console at console.airlocklabs.ai.
With self-hosted CP, you have two paths:
- Use our hosted console pointed at your CP (works if your CP is internet-reachable; the console is just a thin operator UI).
- Self-host the console too — see Self-hosting the control plane for the docker compose setup that runs both.
Open the console at the URL your operator gave you (hosted:
https://console.airlocklabs.ai; self-hosted: whatever you deployed
it at).
You'll see two sign-in surfaces:
- OIDC button (Google or Okta). The default for production once identity is wired up.
- Operator-token paste form. Use this for fresh deploys, for self-hosted consoles without OIDC configured yet, or as a break-glass. Paste the operator token from Authentication tokens, hit Continue. The token is stored in an httpOnly cookie for 7 days.
Either way, the console redirects you to / (Overview). The TopBar
shows your tenant slug and a worker-status pill.
The console is a thin operator UI on top of CP — it never holds raw customer data, only renders what CP returns. Your DB credentials, raw rows, and the worker's private key stay on the worker host inside your VPC. See Architecture.
Step 3 — Add a worker#
A worker is the process that runs inside your VPC and connects to your postgres. You add one in two phases: mint an enrollment token in CP, then bring up the worker on your host using that token.
Mint an enrollment token#
Pick whichever fits your workflow:
Console (recommended for first-time setup)#
- Open
/workersin the console. - Click Add worker. Enter a worker id (convention:
w_<tenant>_<role>likew_acme_prod). - The wizard mints a single-use enrollment token and pre-bakes an install snippet for your platform.
- Copy the snippet — the token is shown ONCE.
- The wizard polls every 2s; when your worker phones home, the status flips green and the worker appears in the list.
CLI (for ops / scripted flows)#
airlock-cp enrollment create --tenant t_acme --worker-id w_acme_prod
# stdout: enroll_<long_token>
This hits the same endpoint as the console wizard. The output is just the token plaintext — pipe it to your config-management tool of choice.
Run the worker on your host#
Once you have an enrollment token, run the worker. Pick whichever matches your platform — these are deployment templates, not alternative flows; all three end with the worker dialing CP and showing up green in the console.
Docker (the one-liner)#
docker run -d \
-e DATABASE_URL='postgresql://airlock_reader:...@db.internal:5432/prod' \
-e AIRLOCK_ENROLLMENT_TOKEN=<enrollment_token> \
-e AIRLOCK_CP_URL=<your_cp_url> \
-v $PWD/airlock.yaml:/etc/airlock/airlock.yaml \
ghcr.io/airlockai/worker:latest
Manual (using the airlock binary)#
The airlock CLI ships in the worker image and is also installable
from PyPI (pip install airlock-worker). Run it directly without the
container — useful for testing on a workstation, debugging a stuck
enrollment, or scripting a deploy yourself.
The single-command path (enrolls, generates the keypair, runs the worker in the foreground):
airlock up \
--token=<enrollment_token> \
--cp=<your_cp_url> \
--database-url=postgresql://airlock_reader:...@db.internal:5432/prod \
--config=airlock.yaml \
--root-table=users \
--root-filter-col=external_id
airlock up runs airlock init for you if airlock.yaml doesn't
exist, then airlock register --enrollment-token …, then starts the
worker. Ctrl-C to stop.
If you'd rather run the steps manually:
# 1. Generate airlock.yaml from your DB schema (auto-inferred masks).
airlock init \
--root-table=users \
--root-filter-col=external_id \
--output=airlock.yaml
# 2. Enroll with the control plane (writes control_plane: block,
# generates and persists the Ed25519 keypair).
airlock register \
--cp-url=<your_cp_url> \
--enrollment-token=<enrollment_token> \
--config=airlock.yaml
# 3. Lint before booting (catches schema drift, unmasked PII heuristics).
airlock yaml lint --config=airlock.yaml
# 4. Optional: dry-run a snapshot for one user / one row id.
airlock yaml preview --config=airlock.yaml --subject=u_42
Kubernetes / Fly / Render#
For platform-specific manifests, see worker deployment recipes (TBD — currently same flag-set as Docker; tell us at hello@airlocklabs.ai which platform you'd prioritize).
All paths generate the keypair at /etc/airlock/worker_ed25519.pem
and POST only the public half to CP. Your DATABASE_URL and your
private key never leave the host.
Within ~5 seconds the console's /workers row flips to
status: connected and the worker logs:
INFO: tunnel up: tenant=t_acme worker=w_acme_prod session=…
INFO: metrics listening on :9090/metrics
If the worker doesn't appear within ~30 seconds, the most common
causes are: outbound :443 to CP is blocked, the host can't reach
Postgres on :5432, or DATABASE_URL is wrong. The worker logs
will say which.
Step 4 — Mint an API key#
Open /api-keys in the console. This is the bearer token your agent
side (LiteLLM, Cursor, raw mcp Python, custom client) uses to
authenticate inbound MCP calls. Separate from the worker's Ed25519
identity — see the Authentication tokens
table.
- Click Mint key.
- Give it a name — typically the calling system, e.g.
litellm-production,cursor-dev,analytics-agent. - Click Create API key.
- Copy the plaintext from the warn card. It's an
ak_live_…token, shown once. The card disappears the moment you dismiss it; the plaintext is never recoverable. CP only stores the Argon2id hash.
Use the plaintext as the bearer in your gateway config:
curl <your_cp_url>/mcp/<your_tenant_slug> \
-H "Authorization: Bearer ak_live_<your_plaintext>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Today every API key has access to every MCP tool the worker exposes
(get_schema, execute_sql, null_rates). Per-key tool scoping is
on the roadmap; for now, scope at the tenant level — one tenant per
"agent surface" keeps the audit log clean.
Step 5 — Verify#
Open /playground in the console. This is a sandbox for verifying
your worker is connected and your masks are doing what you expect.
- Pick a snapshot id in the dropdown — any row identifier from
your
root_table(e.g.u_42). - Type a benign prompt that requires data from your schema, like "summarize this user's recent transactions."
- Hit Send.
In the right column you'll see the live audit feed scoped to this
chat's trace id. Every tool call lands as a row with
outcome: ok. The first call for a fresh snapshot takes ~1–2s
while the worker exports it to tmpfs; subsequent calls
within the TTL window land in 30–150ms.
Click into any tool-call row to see the result rows the agent got
back. Confirm masked columns look masked — if a column you
declared mask_columns: {ssn: "null"} shows up with real SSNs, the
mask isn't taking effect (most often: the column lives on a child
table you forgot to mask, or airlock.yaml didn't reload — workers
have no hot-reload, you must restart).
Then open /audit. Every chat turn from above produces one or more
audit rows here, each with a trace_id badge. Click the badge to
filter the feed to that single trace. Audit rows record metadata
only (tool, snapshot id, latency, outcome) — never SQL text or
result rows. See
Concepts → Audit log.
Optional: confirm the egress guard is on#
In /playground, expand the Raw SQL disclosure below the prompt
input. It opens a textarea pre-filled with a malicious sample SQL
that uses read_csv_auto('https://…') to attempt an external read.
Click Run as raw SQL — the request goes straight to the worker
over MCP with no LLM in the loop. The egress validator parses
the SQL, rejects it, and the audit row shows up with
outcome: error · error_class: egress_blocked. The same panel
also accepts any SQL you type — useful for confirming what the
worker accepts vs. what it rejects without driving an agent through
it. See Concepts → Egress block.
Console reference#
Once the four steps above are working, the rest of the console is self-explanatory. Short index of every page and when you'd use it:
/playground#
Sandbox for verifying that a worker is connected and masks are working. Pick a tenant + snapshot id, type a prompt, watch MCP tool calls land in the right-column audit feed. Expand the Raw SQL panel and click Run as raw SQL to fire arbitrary SQL straight at the worker (no LLM) — useful for confirming the egress guard without driving an agent through it. First call to a fresh snapshot takes ~1–2s (cold export); subsequent calls within TTL land in 30–150ms.
/schema#
Tenant-level view of every table and column the agent can see, with
the mask policy rendered as a per-column badge. Use it to spot
unmasked PII, to point compliance reviewers at "what data does our
agent have access to?", and to filter columns when you have 30+
tables. The page is a viewer of airlock.yaml; edits go through the
worker host (no in-console editing yet).
/audit#
Live feed of every MCP tool call routed through CP. Last 100 events
seed the page; new ones stream over SSE as they happen. Filter by
the trace-id badge to scope to one chat turn. Watch for the
BLOCKED badge — egress_blocked is the system working as
designed, worker_unavailable means a tunnel dropped. SQL text and
result rows are intentionally not in the audit log; if you need
to debug an exact query, look at worker-host logs (DuckDB writes
SQL there).
/workers#
The list of every worker dialed into CP for this tenant. Status is
connected / disconnected / revoked. Use Add worker to mint
a fresh enrollment token (Step 3 above). Click Revoke on a
stale entry — the active tunnel drops and future MCP calls routed
to that worker fail with worker_unavailable until another worker
on the same tenant picks them up.
/api-keys#
The list of bearer tokens that authenticate inbound MCP calls. Use
Mint key to issue a new ak_live_… (Step 4 above). Plaintext is
shown once; CP holds the Argon2id hash. Rotation is non-atomic
today — mint the new key, deploy it to your gateway, verify in
/audit that traffic is on the new key id, then revoke the old.
/config#
Live editor for the tenant's airlock.yaml. Apply persists the new
YAML to the worker host's disk via the config_put admin op. The
running worker keeps serving the OLD config until restarted — apply
changes, then roll your worker (systemctl restart, redeploy your
container, etc.). Hot-reload is on the
roadmap.
The full reference is at Configuration.
Where to go next#
- Concepts — one-paragraph definitions of every
term the rest of the docs assume (
tenant,snapshot,mask policy,egress block,Mode A vs Mode B). - Configuration — every field in
airlock.yamlwith defaults and examples; themask_columnsreference is the most-asked section. - Security — the customer-facing whitepaper with threat model, cryptography, audit, and compliance posture.
When something goes wrong#
tunnel rejected— the enrollment token expired or was already used. Mint a fresh one in/workers; tokens are single-use by design.No record in <root_table>during export — the snapshot id you asked about doesn't exist in the source DB. Try a known-good record.-32001 worker_unavailablefrom the agent — the worker dropped the tunnel. CP retries on the next call; check worker logs for the cause.-32005 scope_denied— the API key is scoped to specific snapshot ids and the caller asked for one outside that list. Check the key'sallowed_snapshotsin CPconfig.yaml.- macOS dev —
/dev/shmdoesn't exist on macOS. SetAIRLOCK_DATA_DIR=./datain the environment, ordata_dir: ./datainairlock.yaml.