The admin API is Airlock's control plane for live protection state: clearing a provider quarantine after a verified credit top-up, resetting a tripped model circuit, or clearing a client backoff — without restarting the proxy or flipping a global env flag.
It is off by default. When disabled, every /airlock/admin/* route returns
404 (Airlock does not confirm the routes even exist), and capability headers are
ignored. A config-free deploy behaves exactly as it did before 0.5.0.
The TUI's clear-quarantine keybinding (see TUI Dashboard) is just a loopback client of this same API — the admin API is the foundation, the TUI is one caller.
Add an admin: block to config.yaml:
admin:
enabled: true # off → /airlock/admin/* returns 404, capability hdr ignored
trust_loopback: true # treat loopback connections as the operator (Path A)
allow_insecure_tokens: false # fail-closed guard for token auth over plaintext
behind_tls_proxy: false # assert TLS is terminated by an upstream proxy| Key | Default | Meaning |
|---|---|---|
enabled |
false |
Master switch. false → all admin routes return 404. |
trust_loopback |
true |
A connection from 127.0.0.1/::1 is the operator tier, no credential (Path A). |
allow_insecure_tokens |
false |
Permit token auth on a non-loopback bind without TLS (downgrades the fail-closed startup refusal to a warning). |
behind_tls_proxy |
false |
Assert that TLS is terminated by an upstream reverse proxy, so the fail-closed check is satisfied. |
remote_tui |
false |
Enable only the host-console container profile below; requires native TLS and capability JWTs. |
Bearer tokens — both admin JWTs and guardrail-skip capabilities — are replayable
until they expire if sniffed over plaintext. So at startup, if the admin API
(or capability skips) is enabled and the bind is non-loopback (AIRLOCK_HOST
is not 127.0.0.1/::1/localhost) and native TLS is off (AIRLOCK_SSL_*
unset), Airlock refuses to start.
Resolve it by one of:
- terminating TLS in Airlock itself — set
AIRLOCK_SSL_CERTFILE/AIRLOCK_SSL_KEYFILE(see native TLS in the Operations guide); - asserting an upstream TLS-terminating proxy —
admin.behind_tls_proxy: true; - explicitly accepting the risk —
admin.allow_insecure_tokens: true(logs a loud warning instead of refusing).
Loopback-only deploys are unaffected.
A request is admitted if either path succeeds:
- Path A — loopback is the operator. A connection from
127.0.0.1/::1is treated as the operator tier with no credential, as long astrust_loopbackis on. Reaching loopback means shell access on the host, which already implies proxy privilege. This is the TUI path. - Path B — Bearer token.
Authorization: Bearer <token>, where the token is either the master key (AIRLOCK_MASTER_KEY, full admin + token minting) or a capability JWT carrying the scope the operation requires.
Some operations are loopback-only regardless of token (e.g. manual quarantine).
Reverse-proxy caveat. A reverse proxy forwarding to Airlock on loopback makes every request look local, which would grant Path A to the world. When you expose the proxy, either block
/airlock/admin/*at the edge, or settrust_loopback: falseto force Path B even locally. Airlock warns at startup iftrust_loopbackis on whileAIRLOCK_HOSTis not loopback.
Each operation requires a scope. A capability token carries a list of scopes; the master key (and any loopback operator) satisfies all of them.
| Scope | Grants |
|---|---|
admin:read |
The read-only GET endpoints |
admin:remote_tui |
Required anchor for the explicitly enabled host-console profiles below; grants no endpoint by itself |
admin:read_config |
The redacted startup provider-configuration view only |
admin:clear_quarantine |
Clear a provider or client→provider quarantine |
admin:reset_circuit |
Reset a model circuit |
admin:clear_backoff |
Clear a client threat backoff |
admin:force_quarantine |
Manually quarantine a provider (loopback-only) |
admin:erase_client |
Erase a client's FathomDB rows (loopback-only) |
When AIRLOCK_OPERATIONAL_READ_BACKEND=fathomdb is selected, the TUI and
Advisor use the proxy-owned operational-read bridge instead of opening the
embedded database from their separate processes. Its bounded records, error,
and search views are loopback-only: a capability token cannot read them
remotely, even with admin:read. Enable the local admin path with
admin.enabled: true and trust_loopback: true.
The bridge carries source, degradation, and truncation information. When it or FathomDB is unavailable, the consumer visibly falls back to bounded JSONL; default deployments continue to use JSONL.
Tokens are short-lived HS256 JWTs signed with AIRLOCK_JWT_SECRET (which falls
back to an HMAC derivation from AIRLOCK_MASTER_KEY when unset). Set a dedicated
AIRLOCK_JWT_SECRET so token lifetime is decoupled from your LLM master key, and
set AIRLOCK_JWT_SECRET_PREV during a rolling secret rotation so in-flight tokens
verify against the previous secret too.
Mint with the CLI — it signs locally (no network, no server, no DB) using the secret the operator already holds:
# Admin-ops token — --sub is an audit-actor label (authorization comes from the
# signature + scope, not the sub).
airlock admin mint-token --sub lme-ops --scope admin:clear_quarantine --ttl 15m
→ eyJhbGciOiJIUzI1Ni…
# Multiple scopes on one token:
airlock admin mint-token --sub ci-bot \
--scope admin:read --scope admin:clear_quarantine --ttl 1h--ttlaccepts durations like15m,1h; the cap is 24h.- Minting runs as the admin; the token it emits is handed to the client out-of-band (env var, secret manager, CI secret).
Guardrail-skip tokens (
guardrail:skip:*) are minted the same way but their--submust be the client's authenticated key-derived id (key:<last8>). See Guardrails → Per-request guardrail skips.
This opt-in profile is for a host console that administers an Airlock container. It is not a general remote Admin deployment, a peer-container mode, or a reverse-proxy integration. Docker reachability is not operator identity.
Use the dedicated complete manifest, not an overlay with the standard Compose
file: docker compose -f docker-compose.remote-admin.yml up. It publishes only
127.0.0.1:${AIRLOCK_PORT:-4000}:4000, mounts native TLS server material
read-only, and leaves the normal Compose deployment unchanged. Configure:
admin:
enabled: true
trust_loopback: false
remote_tui: true
allow_insecure_tokens: false
behind_tls_proxy: falseSet both native TLS environment variables and provide a CA bundle on the host. Mint a distinct, non-secret-subject token for each operator with a maximum 15-minute lifetime and all necessary scopes, including the anchor:
airlock admin mint-token --sub host-console-alice \
--scope admin:remote_tui --scope admin:read \
--scope admin:read_config --scope admin:clear_quarantine --ttl 15mPlace the emitted token in an owner-only (0600) regular file delivered by your
secret manager, then run:
airlock tui --remote-admin --host localhost --port 4000 \
--admin-token-file /secure/airlock-remote.jwt \
--admin-ca-file /secure/airlock-ca.pemThe remote UI validates the hostname even for localhost, uses only the Admin
HTTP perimeter, and is restricted to snapshots plus clear-quarantine. It never
reads local logs/configuration/state, manages a proxy, or accesses operational
history; force quarantine and erasure remain loopback-only. Failed TLS,
authentication, and scope checks show the console as unavailable without
displaying credentials.
For routine rotation, issue a new short-lived token. For an emergency
revocation, replace AIRLOCK_JWT_SECRET, omit AIRLOCK_JWT_SECRET_PREV, and
restart the proxy; this invalidates all tokens signed with the old secret. To
roll back the profile, stop the dedicated Compose deployment (or remove its
loopback mapping), remove remote_tui: true, and restart. Do not publish this
Admin port to all interfaces or substitute the master key, a Docker bridge, a
CIDR, or forwarded headers for the capability token.
This is a deliberately small extension of the host-console container profile, for an operator viewing several Airlock containers on the same host. It is read-only: it does not manage containers, configuration, secrets, lifecycle, or desired state, and it does not poll automatically. It is not remote fleet control, service discovery, or a systemd/Kubernetes mode.
Every target must use the preceding remote_tui: true profile plus
fleet_read_tui: true, and a separate
host-loopback published port, native TLS CA, and AIRLOCK_JWT_SECRET. Give its
token exactly admin:remote_tui and admin:read; the target rejects a
token carrying any other scope, so a fleet credential cannot authorize a
mutation through the Admin API. The distinct signing secret
prevents a token for one target being replayed at another.
Create owner-only regular files (0600, owned by the user running the TUI) for
the inventory, each CA bundle, and each token. A profile contains references,
never embedded secret values:
targets:
- id: payments
name: Payments proxy
origin: https://localhost:4101
ca_file: /secure/payments-ca.pem
token_file: /secure/payments-read.jwt
- id: support
name: Support proxy
origin: https://127.0.0.1:4102
ca_file: /secure/support-ca.pem
token_file: /secure/support-read.jwtRun it with explicit manual selection:
airlock tui --fleet-inventory /secure/airlock-fleet.yamlThe inventory accepts at most ten distinct targets. Origins are exact HTTPS
loopback origins (localhost, 127.0.0.1, or [::1]) with an explicit port;
no path, query, credentials, redirects, proxies, or remote/private-network
address is admitted. Airlock resolves and validates loopback addresses for each
connection, validates the TLS hostname and peer address, and directly connects
only to the vetted address. Refresh only named selected targets (maximum ten,
four concurrent), uses a 2-second connect and 5-second total timeout, and caps
responses at 64 KiB. Results show only fresh, stale, authentication,
authorization, TLS, or unavailable state; no token or raw remote error is
displayed or written locally. Stop using the command or remove the inventory
to roll it back.
All routes are under /airlock/admin/ and only exist when admin.enabled: true.
| Method | Path | Scope |
|---|---|---|
GET |
/airlock/admin/providers |
admin:read |
GET |
/airlock/admin/clients |
admin:read |
GET |
/airlock/admin/circuits |
admin:read |
GET |
/airlock/admin/config/providers |
admin:read_config |
These return a richer view of live protection state than the read-only
GET /health/circuits.
GET /airlock/admin/config/providers is deliberately distinct from live
provider health and requires admin:read_config (or the existing loopback or
master-key operator authority). It reports the LiteLLM child's bounded,
redacted startup configuration: configured aliases and capability metadata,
hostname-only API bases, opaque credential kind/presence, load time, and a
redacted-state fingerprint. It sends Cache-Control: no-store.
It never contains credential values or reference names, environment-variable
names, include paths, full URLs, provider errors, or a way to edit/reload
configuration. The view is not a disk watcher: apply configuration through the
normal reviewed deployment workflow and restart the proxy before expecting a
change. admin:read alone receives 403; disabled Admin still returns 404.
# Loopback operator — no credential needed:
curl http://localhost:4000/airlock/admin/providers
# Remote, with a scoped token:
curl https://airlock.internal/airlock/admin/circuits \
-H "Authorization: Bearer <admin:read-jwt>"Clears the provider-wide quarantine and cascades to every (client, provider)
bucket for that provider — "unblock everyone on openai".
curl -X POST https://airlock.internal/airlock/admin/providers/openai/clear-quarantine \
-H "Authorization: Bearer <admin:clear_quarantine-jwt>" \
-H "Content-Type: application/json" \
-d '{"mode":"probe"}'Scope: admin:clear_quarantine. Body mode:
probe(default) — drop the breaker to half-open: the next request is admitted as a one-shot probe; a success closes the circuit, a failure re-arms it on the policy cooldown. A mistaken clear (credits not actually topped up) self-corrects instead of re-storming.force— blind clear; lifts the quarantine immediately with no probe gate.
Clears exactly one victim bucket — the precise operation for a single client's per-client quarantine, leaving the rest of the provider's clients untouched.
curl -X POST \
https://airlock.internal/airlock/admin/clients/key:b35cf679/providers/openai/clear-quarantine \
-H "Authorization: Bearer <admin:clear_quarantine-jwt>" \
-d '{"mode":"probe"}'Scope: admin:clear_quarantine. Same mode semantics as above.
Closes a tripped per-model circuit breaker.
curl -X POST https://airlock.internal/airlock/admin/models/gpt-5.4/reset-circuit \
-H "Authorization: Bearer <admin:reset_circuit-jwt>"Scope: admin:reset_circuit.
Clears a client's threat-detector backoff.
curl -X POST https://airlock.internal/airlock/admin/clients/key:b35cf679/clear-backoff \
-H "Authorization: Bearer <admin:clear_backoff-jwt>"Scope: admin:clear_backoff.
Erasure is deliberately a local control-plane operation. Use the CLI against the running proxy, repeating the authenticated client id as confirmation:
airlock admin erase-client key:90abcdef --confirm key:90abcdefThe equivalent loopback-only HTTP operation is:
curl -X POST http://127.0.0.1:4000/airlock/admin/clients/key%3A90abcdef/erase \
-H 'Content-Type: application/json' \
-d '{"confirm":"key:90abcdef"}'Scope: admin:erase_client. A successful operation emits an admin_action
audit record with its erase report. HTTP 409 means erasure was incomplete and
must not be treated as done; retrying the same request is safe.
This affects the optional FathomDB search/analysis store only. It does not
remove the corresponding JSONL logs, whose retention is controlled separately
by AIRLOCK_MAX_LOG_DAYS. See Operations → Per-client erasure
before making a deletion commitment.
Manually arms a provider quarantine (break-glass).
curl -X POST http://localhost:4000/airlock/admin/providers/openai/quarantineScope: admin:force_quarantine. Loopback-only — there is no remote/token path
for this operation.
Every successful mutation emits an admin_action record into the same JSONL log
stream as request records (AIRLOCK_LOG_DIR). The record is the audit trail, the
crash-recovery entry, and the channel the TUI's own state replica reads to converge
on the change — the mutation and its audit record are one object.
Each admin_action record carries the actor (the token sub or loopback), the
operation, its target (provider / client / model), the mode, and a timestamp. Filter
the logs for them:
grep '"record_type": "admin_action"' logs/airlock-$(date +%Y-%m-%d).jsonl \
| python -m json.toolRequest records carry
"record_type": "request"(treated as the default when the key is absent, for back-compat with pre-0.5.0 logs).
The admin API and capability tokens are bearer credentials, so they want TLS. You can terminate it in Airlock itself rather than only at a reverse proxy — see native TLS in the Operations guide.