This guide covers the complete lifecycle of API keys: how a service administrator generates, configures, and manages keys; and how a client user obtains and uses a key to access the inference API.
┌──────────────┐ API Key (Bearer) ┌──────────────┐
│ Client User │ ─────────────────────────→ │ Gateway │
│ (tenant) │ Authorization: Bearer... │ (:8080) │
└──────────────┘ └──────┬───────┘
SHA-256 hash lookup
scope + rate limit check
│
┌──────▼───────┐
│ Model Server │
│ (loopback) │
└──────────────┘
┌──────────────────┐ restart gateway ┌──────────────┐
│ Service Admin │ after editing toml │ Supervisor │
│ (operator) │ POST .../servers/... │ (:8787) │
└──────────────────┘ /restart └──────────────┘
Development default is mortredctl init-trust (env tokens). conf/api_keys.toml
is optional multi-tenant auth on the gateway process. There is no
supervisor hot-reload for keys: edit the file, then restart the gateway child
(POST /api/v1/servers/__gateway/restart with MORTRED_API_TOKEN).
scope (inference | admin | all) only affects whether the gateway
accepts that Bearer for inference. It does not grant :8787 management.
4f8a7b2c9d0e..., a 64-character hex string)https://inference.example.com:8080)inference) and rate limit# Save to a file with restricted permissions
echo "your-api-key-here" > ~/.mortred-api-key
chmod 600 ~/.mortred-api-key
# Or set as an environment variable
export MORTRED_API_KEY="your-api-key-here"
Every request to the gateway must include the Authorization: Bearer <key> header:
Prefer the catalog path. The legacy server_uri still works with the same body.
IMG=$(base64 -w0 image.jpg)
curl -X POST http://localhost:8080/v1/models/YOLOV8/infer \
-H "Authorization: Bearer $MORTRED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"images":["'"$IMG"'"],"req_id":"my-request-1"}'
Submit the same envelope as /infer. Model knobs such as DDPM timesteps
belong in params, not at the root. Generative models ignore images[]
pixels; a dummy base64 string is a valid payload.
# 1. Submit
curl -X POST http://localhost:8080/v1/models/DDPM/jobs \
-H "Authorization: Bearer $MORTRED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"images":["aGVsbG8="],"req_id":"job-1","params":{"timesteps":10}}'
# Returns HTTP 202: {"job_id": "job_xxx", "state": "pending", "poll_url": "...", "result_url": "..."}
# 2. Poll
curl http://localhost:8080/v1/models/DDPM/jobs/job_xxx \
-H "Authorization: Bearer $MORTRED_API_KEY"
# 3. Get result
curl http://localhost:8080/v1/models/DDPM/jobs/job_xxx/result \
-H "Authorization: Bearer $MORTRED_API_KEY"
import requests
import base64
API_KEY = "your-api-key-here"
GATEWAY = "http://localhost:8080"
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
# Encode image
with open("image.jpg", "rb") as f:
img_b64 = base64.b64encode(f.read()).decode()
# Inference
resp = requests.post(f"{GATEWAY}/v1/models/YOLOV8/infer",
headers=headers,
json={"images": [img_b64], "req_id": "demo"})
print(resp.json())
| Header | Description |
|---|---|
X-Mortred-Key |
Your key name (e.g., tenant-a) — confirms which key was used |
X-Request-ID |
Echoed request ID for tracing |
Retry-After |
Present on 429 responses — wait this many seconds before retrying |
| Status | Meaning | What to do |
|---|---|---|
| 401 | Invalid or missing key | Check that your key is correct and enabled |
| 429 | Rate limit exceeded | Wait Retry-After seconds, then retry |
| 404 | Unknown model path | Check the model URI with the administrator |
| 503 | Model server not running | Contact the administrator |
export MORTRED_API_KEY="new-key"The gateway supports two external auth mechanisms, checked in order:
conf/api_keys.toml) — multi-tenant auth with scope and per-key rate limiting.MORTRED_GATEWAY_AUTH_TOKEN) — legacy single-token auth.The effective auth mode is decided at startup and printed in the listen log:
| Mode | Condition | Behavior |
|---|---|---|
api-keys auth |
conf/api_keys.toml loaded successfully |
Requests must match a key. The static-token fallback is not consulted when no static token is configured — an unauthenticated request gets 401. |
static-token auth |
no usable key file, token set | Legacy behavior: requests must match the token. |
There is no anonymous (AUTH DISABLED) mode. Missing tokens refuse to start.
Fail-closed startup rules:
conf/api_keys.toml (parse error) refuses to start when no static token is configured — an operator who configured keys wants authentication, and silently serving without it would be a fail-open security hole.Fail-closed is a startup gate. It does not terminate TLS, hide gateway
GET /metrics, or enforce token strength. Mortred itself is plain HTTP.
⚠️ Do not rely on an empty
MORTRED_GATEWAY_AUTH_TOKENas a “keys configured → still allow everything” escape hatch. The empty-token allow semantics only ever apply when no auth mechanism is configured at all (loopback development mode).
API keys are defined in conf/api_keys.toml:
[keys.tenant-a]
# SHA-256 hash of the API key string (never store the plaintext key)
# Generate: echo -n "your-secret-key" | sha256sum
hash = "a1b2c3d4..."
scope = "inference" # inference | admin | all
rate_limit_qps = 100 # 0 = unlimited
enabled = true
| Field | Type | Default | Description |
|---|---|---|---|
hash |
string | required | SHA-256 hex of the key |
scope |
string | “inference” | “inference”, “admin”, or “all” |
rate_limit_qps |
int | 0 | Per-key requests per second (0 = unlimited) |
enabled |
bool | true | Disable a key without deleting it |
When a new client (tenant) needs access:
# Step 1: Generate a random key (this is what you give to the client)
openssl rand -hex 32
# Example output: 3a7f9b2e8c4d1f6a0b5c3d8e2f7a4b9c6d1e0f3a5b8c2d7e4f1a6b3c8d0e5f
# Step 2: Compute the SHA-256 hash (this is what goes in the config)
echo -n "3a7f9b2e8c4d1f6a0b5c3d8e2f7a4b9c6d1e0f3a5b8c2d7e4f1a6b3c8d0e5f" | sha256sum
# Example output: 278ea5c810f26733365d39e13857a53bf2d6d1fd8a98f47f668c574cb5417c53
# Step 3: Record the hash in conf/api_keys.toml
# conf/api_keys.toml — add the new client
[keys.new-client]
hash = "278ea5c810f26733365d39e13857a53bf2d6d1fd8a98f47f668c574cb5417c53"
scope = "inference"
rate_limit_qps = 100
enabled = true
# Step 4: Restart the gateway child so it reloads conf/api_keys.toml
curl -X POST -H "Authorization: Bearer $MORTRED_API_TOKEN" \
http://localhost:8787/api/v1/servers/__gateway/restart
# Step 5: Give the key string to the client (NOT the hash)
# The client uses: Authorization: Bearer 3a7f9b2e8c4d...
# You store: hash = "278ea5c8..."
Key list and usage counters are not exposed on the supervisor. Counters live in the gateway process and reset on restart.
# conf/api_keys.toml
[keys.suspended-client]
hash = "..."
enabled = false # takes effect after the gateway child restarts
Then restart the gateway child.
[keys.suspended-client]
hash = "..."
enabled = true
Restart the gateway child.
Remove the entire [keys.name] section from conf/api_keys.toml, then restart
the gateway child.
[keys.tenant-a]
hash = "..."
rate_limit_qps = 200 # was 100
Restart the gateway child. Takes effect immediately for new requests.
Keep old and new [keys.*] entries in the file, restart the gateway child,
switch the client, then remove the old entry and restart again. The gateway
is down for the restart; overlapping hashes in one file avoid a client
outage if you restart once with both keys present.
# Restrict file permissions (only the service user can read)
sudo chown mortred:mortred conf/api_keys.toml
sudo chmod 600 conf/api_keys.toml
# Never commit the production key file to version control
# (add to .gitignore if deploying from git)
echo "conf/api_keys.toml" >> .gitignore
Per-key counters are in-process on the gateway and are not exported on
:8787. Use gateway access logs (X-Mortred-Key) or Prometheus HTTP
metrics. Counters reset when the gateway child restarts.
| Problem | Cause | Fix |
|---|---|---|
| All requests return 401 | no token, empty api_keys.toml, or wrong Bearer |
mortredctl init-trust or add a [keys.*] hash; empty/comment-only key file is not auth |
| New key doesn’t work | gateway still running old file | restart the gateway child |
| Key works but returns 429 | Rate limit reached | Increase rate_limit_qps in config and restart gateway |
| Client lost their key | Only hash is stored | Generate a new key, disable the old one |
| Gateway logs “failed to parse” | TOML syntax error | Fix the file; without a static token the gateway refuses to start |
| Gateway logs “empty key file is not auth” | copied example with no hashes | add keys or use init-trust tokens |
# 1. Generate a random key
openssl rand -hex 32
# 2. Compute the hash for the config file
echo -n "4f8a7b2c..." | sha256sum
# 3. Add to conf/api_keys.toml
# conf/api_keys.toml
# Real-time inference client with moderate rate
[keys.mobile-app]
hash = "a1b2c3d4e5f6..."
scope = "inference"
rate_limit_qps = 100
enabled = true
# Batch processing client with high rate
[keys.batch-processor]
hash = "b2c3d4e5f6a7..."
scope = "inference"
rate_limit_qps = 500
enabled = true
# Operator key (same inference path; does not unlock :8787)
[keys.ops-team]
hash = "c3d4e5f6a7b8..."
scope = "all"
rate_limit_qps = 0
enabled = true
# Temporarily suspended client
[keys.trial-expired]
hash = "d4e5f6a7b8c9..."
scope = "inference"
rate_limit_qps = 10
enabled = false
The gateway checks in order:
If either succeeds, the request is authorized. The X-Mortred-Key response header identifies which key was used.
There is no supervisor /api/v1/keys surface. After editing
conf/api_keys.toml, restart the gateway child:
curl -X POST -H "Authorization: Bearer $MORTRED_API_TOKEN" \
http://localhost:8787/api/v1/servers/__gateway/restart
curl -X POST http://localhost:8080/v1/models/YOLOV8/infer \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"images":["base64..."],"req_id":"demo"}'
openssl rand -hex 32ApiKeyManager::authenticate() returns a shared_ptr<const ApiKey>: the caller
owns the key for as long as it reads it (name/scope/counters), so a concurrent
reload() swapping the whole key set can never dangle the result. Callers must
not keep the raw pointer beyond the shared_ptr’s lifetime. Runtime counters and
rate-limiter state on ApiKey are mutable internal synchronization state - a
const key still counts and rate-limits, but its identity/config never changes.
This contract is enforced by test/api_key_manager_unittest.cc: a stress test
drives authenticate() against a continuous reload loop and carries the
sanitizer ctest label (TSAN gate in CI). The same test against the previous
raw-pointer implementation crashes with a heap-use-after-free under ASan.