Skip to content

Security

What Axium protects, how, and what is left to you.

Secrets at rest

Secret Storage Mechanism
Provider credentials (API keys, service-account JSON) llm.providers.api_key AES-256-GCM with MASTER_ENCRYPTION_KEY, random 12-byte IV per value, stored as base64(iv):base64(tag):base64(ciphertext). Decrypted on every proxied request. Never returned by the API; only the last 4 characters are exposed as api_key_masked.
Console user passwords core.users bcrypt, 10 rounds. Minimum 8 characters.
Console session tokens core.users.token 40 random hex characters (20 bytes from the system CSPRNG). Stored in clear, compared directly.
Runtime API keys rm.api_keys.key_hash SHA-1 of the plaintext ax_… key, plus the last 4 characters. The plaintext is shown once, at creation, and cannot be recovered.

The master key cannot be rotated

There is no re-encryption procedure and no KMS integration. Losing or changing MASTER_ENCRYPTION_KEY means re-entering every provider's credentials by hand. Store it with the same care as the database backups, ideally in a secret manager, and never in the same place as the backup itself.

Console sessions

  • Login (POST /v1/users/login) returns a bearer token. It is not a JWT: it is an opaque value looked up in the database on every request.
  • Tokens do not expire. They are rotated (and the previous session invalidated) on each login, on logout, when the user is deactivated and when an administrator resets the password.
  • No cookies, no refresh tokens, no server-side session memory. Any replica can validate any token.

Because tokens never expire, encourage administrators to log out, and deactivate accounts of people who leave rather than just deleting their roles.

Authorization

Role-based. Permissions are module:action strings across eight modules (core, rm, llm, observability, governance, chat, mcp, agents) and nine actions (read, view, create, update, delete, manage, execute, test, export). Each route lists the permissions that open it and the caller needs at least one. The seeded super-admin role holds all 72 and cannot be removed from the last user who has it (409 LAST_SUPER_ADMIN).

There is no multi-tenancy: one installation is one tenant. Isolation between teams is done with proxy apps, API keys scoped to them, and model allow-lists.

Runtime API keys

  • Format ax_ followed by 40 hex characters.
  • Accepted as Authorization: Bearer ax_… or x-goog-api-key: ax_…. A ?key= query parameter is not accepted and is stripped before forwarding.
  • Scoped to one proxy app. Using a key against another app's URL answers 403. Unknown, inactive and expired keys answer an identical 401.
  • Optional expires_at. Deactivate (is_active=false) rather than delete to keep history.

Details in Authentication.

What reaches the provider

Before forwarding, the core removes authorization, x-goog-api-key, api-key, x-api-key, cookie, host, hop-by-hop headers and the key query parameter, and injects the provider's own credential. No x-axium-* header is forwarded; they are consumed for tracing. The request body goes through as-is, except the model field (replaced by the deployed model name) and, in streamed chat completions, stream_options.include_usage=true.

Prompts and responses are not persisted by observability. The only place provider output is stored is the response cache (rm.request_io), and only when a proxy app enables the cache policy in strict mode.

Data-loss prevention

The dlp_prompt policy scans prompts for personal data (European identifiers, IBANs, phone numbers, …) and can mask values before they leave your network, block the request (403) or just record what it found. The trace records entity names and kinds, never the values. Configured per proxy app; see Proxy apps & gateways.

Network

  • TLS: terminate it in front of Axium, set TRUST_PROXY=true. See Reverse proxy & TLS.
  • CORS: the core answers Access-Control-Allow-Origin: * without credentials. This is not configurable. It is what lets the console run on another hostname; bearer tokens, not cookies, carry the identity, so the open origin does not enable cross-site request forgery.
  • Inbound firewall: the firewall.inbound policy restricts which client CIDRs may use a proxy app (first matching rule wins, up to 256 rules). It needs TRUST_PROXY=true behind a proxy to see real addresses; an unknown IP falls to default_action.
  • Egress: the core needs HTTPS to your providers and to meta.axium-lab.com. Nothing else.

Lock these down at the ingress

Route Why
GET /v1/install/status, POST /v1/install Public by design so a fresh install can be driven without credentials. Inert once installed, but they reveal the schema version. Restrict to your admin network.
GET /v1/health Harmless, but there is no reason for the internet to probe it.
The console (ui) Behind SSO, a VPN or at least an IP allow-list if your team is small.

Process

  • The container runs as the unprivileged bun user; nothing is written to disk.
  • Secrets arrive only through environment variables or a mounted license file. On Cloud Run the installer leaves them as plain environment variables; move them to Secret Manager for production.
  • There is no audit log of console actions in this version (the table exists in the schema but nothing writes to it). Observability traces cover runtime requests, not administrative changes.