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:

  1. Deploy the control plane at a URL your worker can reach (or accept the URL your operator gives you for hosted CP).
  2. Log into the console with your operator token.
  3. 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.
  4. 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 :5432 and 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_TOKEN when 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.

TokenLifetimeWhere mintedWhere usedWhat it gates
Operator tokenPermanent (rotated by env-var change)Set as AIRLOCK_CP_OPERATOR_TOKEN env var on CP at deploy timeConsole 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 tokenOne-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 keyPermanent (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 register writes it to /etc/airlock/worker_ed25519.pem mode 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:

  1. Use our hosted console pointed at your CP (works if your CP is internet-reachable; the console is just a thin operator UI).
  2. 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:

  1. Open /workers in the console.
  2. Click Add worker. Enter a worker id (convention: w_<tenant>_<role> like w_acme_prod).
  3. The wizard mints a single-use enrollment token and pre-bakes an install snippet for your platform.
  4. Copy the snippet — the token is shown ONCE.
  5. The wizard polls every 2s; when your worker phones home, the status flips green and the worker appears in the list.
<!-- SCREENSHOT: workers list (empty state) showing "Add worker" button --> <!-- SCREENSHOT: Add worker wizard with worker_id field + Mint button --> <!-- SCREENSHOT: Token-shown-once panel with copy button + status pending -->

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.

  1. Click Mint key.
  2. Give it a name — typically the calling system, e.g. litellm-production, cursor-dev, analytics-agent.
  3. Click Create API key.
  4. 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.

  1. Pick a snapshot id in the dropdown — any row identifier from your root_table (e.g. u_42).
  2. Type a benign prompt that requires data from your schema, like "summarize this user's recent transactions."
  3. 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.yaml with defaults and examples; the mask_columns reference 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_unavailable from 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's allowed_snapshots in CP config.yaml.
  • macOS dev/dev/shm doesn't exist on macOS. Set AIRLOCK_DATA_DIR=./data in the environment, or data_dir: ./data in airlock.yaml.