Bring Your Own Key (BYOK)

The Operations Assistant runs entirely on your LLM provider key. AuditTrail does not proxy your chat through a hosted endpoint — it stores the key (encrypted at rest), looks it up on each chat request, and makes the provider call directly from your backend. This keeps data, costs, and rate-limit quotas under your control.

Warning — Key storage is per-instance

Keys are encrypted with a derivation of your instance's AUDITTRAIL_SECRET_KEY. Rotating AUDITTRAIL_SECRET_KEY invalidates every stored key — you will need to re-add them. Plan rotations accordingly.

Supported providers

ProviderStorage labelNotes
OpenAIopenaiAll models exposed at api.openai.com/v1
AnthropicanthropicClaude 3 / 3.5 / 4 family
GatewaygatewayRoutes through AuditTrail's own gateway proxy at /api/v1/gateway/proxy/v1 — useful if you already centralise provider creds

A user can have multiple keys (one per (provider, label) tuple). The chat endpoint resolves the active key by (provider, label) with a fallback to "most-recently-used key on this user" when no provider is specified.

How the encryption works

  • Each tenant's key is encrypted with Fernet using a per-domain derived key (HKDF-SHA256 over the instance AUDITTRAIL_SECRET_KEY, salted with the provider domain).
  • Plaintext is NEVER written to disk. The fernet_ciphertext column stores the wrapped blob; decryption happens in memory inside the chat stream handler.
  • last4 is stored separately so the UI can show a recognisable preview (sk-…abc1) without touching the ciphertext.

UI — /settings/ai-assistant

  • Add key — paste a provider key + choose label. The server round-trips a cheap completion to verify the key before saving.
  • Test key — re-verify after a rotation.
  • Revoke — deletes the ciphertext row + 4-digit preview.
  • Last used — timestamp so you can prune stale keys.

Endpoints

MethodPathPurpose
GET/api/v1/assistant/keysList keys (last4 only, never ciphertext)
POST/api/v1/assistant/keysCreate / rotate a key
DELETE/api/v1/assistant/keys/{id}Revoke
POST/api/v1/assistant/keys/{id}/testRound-trip with the provider

All user-scoped.

When no key is configured

The chat endpoint returns 412 Precondition Failed with a structured {"requires_provider_key": true, "settings_url": "/settings/ai-assistant"} body. The dashboard surfaces this as a configure-CTA rather than a mock reply. Per the "no mocks, no shortcuts" rule, AuditTrail never falls back to a pretend response — the UI is always honest about whether it's wired to a real provider.

Rotating

Rotate by adding a new key with a different label, switching the assistant to it (POST /chat with label: "rotated"), then revoking the old row. Zero-downtime.

Audit trail

Every key mutation (create / rotate / revoke) is recorded in the audit_log table with actor id, resource id, and request IP.