Authentication¶
OrionBelt ships with authentication off by default — the public demo and
local-dev (uv run orionbelt-api) keep working with no credentials. Production
deployments turn it on with a single environment variable that governs every
surface: REST, Arrow Flight SQL, the Postgres wire protocol, the Gradio UI,
and the MCP server.
TL;DR¶
# 1. Generate a key
KEY="obsl_pat_$(python3 -c 'import secrets; print(secrets.token_hex(20))')"
echo "$KEY"
# 2. Turn on auth + register the key
export AUTH_MODE=api_key
export API_KEYS="$KEY"
# 3. Restart OrionBelt — every surface now requires this key
One variable, three protocols, two consumers. Rotation is the same variable with a comma-separated second key.
The AUTH_MODE selector¶
| Mode | Meaning |
|---|---|
none |
No authentication (default). Preserves public-demo / local-dev behaviour. |
api_key |
Validate a key from API_KEYS on every request. |
oidc |
OpenID Connect / JWT (planned; rejected at startup until it ships). |
Settings¶
| Variable | Default | Description |
|---|---|---|
AUTH_MODE |
none |
none, api_key, or oidc. |
API_KEYS |
— | Comma-separated valid keys (≥32 chars, high-entropy). Required when AUTH_MODE=api_key. |
API_KEY_HEADER |
X-API-Key |
REST header name. Authorization: Bearer is always accepted as a fallback. |
AUTH_ENABLED |
false |
Deprecated alias for AUTH_MODE=api_key (honoured one release with a startup warning). |
Startup fails fast (refuses to boot) if AUTH_MODE=api_key with an empty
API_KEYS, or if any key is shorter than 16 characters.
Generating keys¶
Out-of-band — there is no in-app key generation endpoint. Use any secure RNG:
# Python (matches OrionBelt's recommended format)
python3 -c "import secrets; print(f'obsl_pat_{secrets.token_hex(20)}')"
# OpenSSL
echo "obsl_pat_$(openssl rand -hex 20)"
The obsl_pat_ prefix is a convention, not a constraint. Keys must be at
least 32 characters and high-entropy — the server refuses to start on a short
or low-entropy key (these are vulnerable to offline attack on captured SCRAM
transcripts). The recommended 40-hex-char token easily satisfies this. The
prefix makes a leaked key easy to spot in code scanners and log alerting.
Detecting whether auth is required¶
GET /health is always unauthenticated and reports the active mode, so a
client can check before sending credentials:
Client recipes¶
REST (curl, httpx, requests)¶
# X-API-Key header (recommended)
curl -H "X-API-Key: $KEY" http://localhost:8000/v1/schema
# Authorization: Bearer (fallback for tools that can't set custom headers)
curl -H "Authorization: Bearer $KEY" http://localhost:8000/v1/schema
/health, /robots.txt, /docs, /redoc, /openapi.json, and /ui stay
unauthenticated; everything under /v1 requires a key.
Arrow Flight SQL (DBeaver, Tableau, programmatic)¶
DBeaver / Flight JDBC: set the username to anything (e.g. token, it is
ignored) and the password to your API key.
import pyarrow.flight as flight
client = flight.FlightClient("grpc://localhost:8815")
token = client.authenticate_basic_token(b"token", KEY.encode())
options = flight.FlightCallOptions(headers=[token])
# pass options on every call
The legacy FLIGHT_AUTH_MODE=token / FLIGHT_API_TOKEN still works for one
release with a deprecation warning. Migrate to AUTH_MODE=api_key + API_KEYS.
Flight can run unauthenticated
The Flight server is opt-in: it starts only when FLIGHT_ENABLED=true (it
does not auto-start just because ob-flight-extension is installed). Once
enabled it binds 0.0.0.0, and unless AUTH_MODE=api_key (or the legacy
FLIGHT_AUTH_MODE=token with FLIGHT_API_TOKEN) is set it accepts every
client (the server logs a loud warning). Setting FLIGHT_API_TOKEN alone,
without FLIGHT_AUTH_MODE=token, does not enable auth. For any non-local
deployment, set AUTH_MODE=api_key (Flight then validates against the shared
key store) or restrict access to the Flight port at the network layer.
Postgres wire (psql, Tableau, Power BI, Metabase, DBeaver)¶
When AUTH_MODE=api_key, pgwire requires a password (the API key). The
username is ignored — pick anything readable in your logs.
By default pgwire uses SCRAM-SHA-256, which never sends the key on the
wire. Clients that lack SCRAM support can fall back to cleartext password auth
by setting PGWIRE_AUTH_MODE=password on the server — in that case terminate
TLS in front of pgwire on untrusted networks, since the key is sent in plain.
Gradio UI¶
The UI is a thin REST client. Start it with the key in its environment; browser users never see it:
If OBSL_API_KEY is unset while the API enforces auth, the UI logs a clear
startup error (rather than surfacing cryptic 401s in the browser).
The UI is a privileged proxy
The UI holds an API key and can act on /v1 (create sessions, load models,
run queries, clear cache). /ui itself is not behind API-key auth
(browsers cannot send the key on navigation), so anyone who can reach /ui
acts as the key holder. For this reason the embedded (co-hosted) UI does
not auto-adopt a key from API_KEYS - you must set OBSL_API_KEY
explicitly, and when you do, restrict network access to /ui (reverse proxy
/ firewall / private network).
MCP server¶
The MCP server is a thin HTTP client of the REST API (separate repository). Set the key once in its environment; it forwards the key on every upstream call. LLM agents talking to MCP over stdio never see it.
Rotation¶
Multiple keys are valid simultaneously, so rotation is zero-downtime:
export API_KEYS="$OLD_KEY,$NEW_KEY" # both work; migrate clients to the new key
export API_KEYS="$NEW_KEY" # remove the old key; redeploy
Editing the variable and restarting is the revocation mechanism — there is no separate revocation endpoint at this scale.
Where keys live¶
API_KEYS is read from the environment; choose the mechanism that fits your
deployment:
| Deployment | How |
|---|---|
| Local dev | .env file (API_KEYS=obsl_pat_...) |
| Docker | -e API_KEYS=obsl_pat_... |
| Cloud Run | Secret Manager → --set-secrets=API_KEYS=obsl-api-keys:latest |
| Kubernetes | secretKeyRef in the Pod spec |
Production checklist¶
-
AUTH_MODE=api_key -
API_KEYSset to at least one random key (≥40 chars recommended) - Keys stored in your platform's secret manager, not committed to a repo
- HTTPS / TLS terminating in front of OrionBelt (reverse proxy or platform)
- Rotation cadence documented (90 days is a reasonable default)
- Public demo deployments use a separate key set from production