mortred_model_server

API Key Management Guide

| English | 中文 | |—|—|

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.

Overview

┌──────────────┐     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.

For Client Users

How to Get an API Key

  1. Contact the service administrator — API keys are issued by the operator of the Mortred deployment. You will receive:
    • Your API key string (e.g., 4f8a7b2c9d0e..., a 64-character hex string)
    • The gateway address (e.g., https://inference.example.com:8080)
    • Your assigned scope (typically inference) and rate limit
  2. Store the key securely:
    # 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"
    
  3. The key is shown to you only once — the server stores only its SHA-256 hash, so if you lose the key, you must request a new one.

How to Use Your API Key

Every request to the gateway must include the Authorization: Bearer <key> header:

Sync Inference (real-time)

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"}'

Async Job (long-running: diffusion, SAM)

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"

Python Example

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())

Response Headers

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

Error Responses

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

Key Rotation (when your key expires)

  1. Request a new key from the administrator
  2. Update your environment: export MORTRED_API_KEY="new-key"
  3. The old key stops working after the administrator removes it
  4. No downtime — both keys work simultaneously during the transition

For Service Administrators

Authentication Modes

The gateway supports two external auth mechanisms, checked in order:

  1. API keys (conf/api_keys.toml) — multi-tenant auth with scope and per-key rate limiting.
  2. Static token (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:

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_TOKEN as 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).

Configuration

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

Fields

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

Generating an API Key for a Client

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..."

Managing Keys

Key list and usage counters are not exposed on the supervisor. Counters live in the gateway process and reset on restart.

Disable a key (without deleting)

# conf/api_keys.toml
[keys.suspended-client]
hash = "..."
enabled = false   # takes effect after the gateway child restarts

Then restart the gateway child.

Re-enable a key

[keys.suspended-client]
hash = "..."
enabled = true

Restart the gateway child.

Delete a key

Remove the entire [keys.name] section from conf/api_keys.toml, then restart the gateway child.

Change a key’s rate limit

[keys.tenant-a]
hash = "..."
rate_limit_qps = 200   # was 100

Restart the gateway child. Takes effect immediately for new requests.

Key Rotation Procedure

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.

Configuration File Security

# 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

Monitoring Key Usage

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.

Troubleshooting

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

Configuration Reference

conf/api_keys.toml Format

# 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

Complete Example

# 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

Authentication Priority

The gateway checks in order:

  1. API Key (multi-key): hash the Bearer token → lookup in conf/api_keys.toml → check scope → check rate limit
  2. Static token (legacy): compare with MORTRED_GATEWAY_AUTH_TOKEN — only when a token is actually configured; an empty token never authorizes a request when API keys are enabled

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"}'

Key Rotation

  1. Generate a new key: openssl rand -hex 32
  2. Add the new hash alongside the old key in conf/api_keys.toml
  3. Restart the gateway child
  4. Distribute the new key to the client
  5. Remove the old key and restart the gateway child again

Security Notes

Concurrency and Hot Reload

ApiKeyManager::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.