← Dashboard

markdown package not installed — serving raw text.

# Self-hosting

The Axiom Intent Firewall runs as a single FastAPI app. You can host
it anywhere you can run Python 3.11+ or a Docker container.

This guide is the short version. For the prod-grade AWS deploy, see
[`deploy/firewall/README.md`](../../deploy/firewall/README.md).

## Minimum runtime

- Python 3.11+ (3.10 works but isn't tested)
- 256 MB RAM, 1 vCPU per replica
- A persistent volume for tenant SQLite files
- A way to terminate TLS (Caddy, nginx, Traefik, ALB, Cloudflare
  Tunnel — your choice)

## Run with Docker

```bash
docker build -t orivael/axiom-firewall:local \
    -f deploy/firewall/Dockerfile .

docker run -d --name firewall \
    -p 8004:8004 \
    -e AXIOM_MASTER_KEY="$(openssl rand -hex 32)" \
    -e AXIOM_FIREWALL_SESSION_SECRET="$(openssl rand -hex 32)" \
    -v /var/lib/axiom-firewall:/data/tenants \
    --restart unless-stopped \
    orivael/axiom-firewall:local
```

Hit <http://localhost:8004/healthz> to verify.

## Run with docker-compose + Caddy (auto TLS)

See `deploy/firewall/docker-compose.yml` and `deploy/firewall/Caddyfile`.

```bash
cd deploy/firewall
cp .env.example .env
# fill in AXIOM_ENV=production, AXIOM_MASTER_KEY,
# AXIOM_FIREWALL_SESSION_SECRET, FIREWALL_HOST. See the Production
# checklist below for what each one does and the rotation gotchas.
docker compose --profile tls up -d
```

## Run from source

```bash
git clone https://github.com/Orivael-Dev/axiom
cd axiom
pip install fastapi uvicorn jinja2 python-multipart itsdangerous

export AXIOM_MASTER_KEY=$(openssl rand -hex 32)
export AXIOM_FIREWALL_SESSION_SECRET=$(openssl rand -hex 32)

uvicorn axiom_firewall.dashboard:app \
    --host 0.0.0.0 --port 8004 \
    --workers 2 --proxy-headers --forwarded-allow-ips='*'
```

## Required environment

| Variable | Required | Purpose |
|---|---|---|
| `AXIOM_ENV` | prod | Set to `production` in any prod deploy. Turns the session-secret check into a hard error (boot fails instead of warning) and forces the `Secure` flag on session cookies. Defaults to `development`. |
| `AXIOM_MASTER_KEY` | yes | HMAC root for signing verdicts **and** the pepper for the API-key hash table. 64 hex chars. **Must stay stable across restarts** — see [Production checklist](#production-checklist) below. |
| `AXIOM_FIREWALL_SESSION_SECRET` | yes | Cookie signing key. 32+ chars; with `AXIOM_ENV=production` the app refuses to boot if this is the dev default or shorter. |
| `AXIOM_FIREWALL_TENANT_DIR` | no | Where to keep tenant SQLite files. Default `tenants`. Mount this to a persistent volume. |
| `AXIOM_FIREWALL_PUBLIC_URL` | no | The URL the dashboard is served at. Used for Stripe redirects. |
| `AXIOM_FIREWALL_CORS_ORIGINS` | no | Comma-separated origins permitted to call `/v1/guard/check` from a browser. Empty = server-side only. |

## Production checklist

Three gotchas to wire into your prod compose file / k8s manifest before
the first request lands:

1. **Set `AXIOM_ENV=production`.** Without it, a missing or weak
   `AXIOM_FIREWALL_SESSION_SECRET` is a runtime warning the team can
   miss in a busy log stream. With it, the app refuses to boot — the
   failure surfaces in `docker compose up` immediately and you fix it
   before traffic arrives.

2. **Generate a real `AXIOM_FIREWALL_SESSION_SECRET`** and put it in
   your secret store. 32 chars minimum (the boot check enforces this in
   prod):

   ```
   python3 -c 'import secrets; print(secrets.token_hex(32))'
   ```

   If this leaks, anyone can forge session cookies for any tenant.
   Rotate the value to force a global logout.

3. **Keep `AXIOM_MASTER_KEY` stable across restarts.** It's used in two
   places now:
   - HMAC signing of every verdict (changes invalidate prior receipts).
   - The peppered hash of API keys stored in the tenant SQLite files.
     The legacy plaintext-to-hash migration runs **once at write time**,
     not on every read — so rotating `AXIOM_MASTER_KEY` after keys are
     in the DB makes every existing API key fail authentication
     permanently.

   Treat it like a database encryption key: generate once, store in
   AWS Secrets Manager / GCP Secret Manager / a sealed-secrets CRD,
   never rotate without a migration plan. If you must rotate, the
   safe sequence is: pause traffic → re-hash every row with the new
   pepper → swap the env var → resume.

A minimal prod `.env` snippet that satisfies all three:

```
AXIOM_ENV=production
AXIOM_MASTER_KEY=<64 hex chars, generated once and stored in your secret manager>
AXIOM_FIREWALL_SESSION_SECRET=<64 hex chars, rotatable>
AXIOM_FIREWALL_TENANT_DIR=/data/tenants
```

## Optional: enable Stripe billing

Run [`scripts/stripe_setup.py`](#) with your test- or live-mode secret
key. It prints the env vars to set:

```
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...   # from the webhook endpoint you create
STRIPE_PRICE_INDIE=price_...
STRIPE_PRICE_TEAM=price_...
STRIPE_METER_INDIE=axiom_firewall_indie
STRIPE_METER_TEAM=axiom_firewall_team
```

If you leave Stripe unset, the Firewall runs in free-tier-only mode:
all paid-tier routes return 503, free tier signup + verdict path work.

## Behind a reverse proxy

The Firewall trusts `X-Forwarded-*` headers when started with
`--proxy-headers --forwarded-allow-ips='*'`. Pass through:

| Header | What it's used for |
|---|---|
| `X-Forwarded-For` | Client IP (for per-IP signup rate limit). |
| `X-Forwarded-Proto` | Required so redirects use `https://`. |
| `X-Forwarded-Host` | Optional; only used for log output. |

Restrict `--forwarded-allow-ips` to your proxy's CIDR when the upstream
is on a known network.

## Health endpoints

| Path | Purpose | Code |
|---|---|---|
| `/healthz` | Liveness — process is up | 200 |
| `/readyz`  | Readiness — DB writable    | 200 / 503 |

## Backups

Tenant SQLite files are self-contained. Back up `$AXIOM_FIREWALL_TENANT_DIR`
on the cadence your data-retention policy requires:

```bash
tar czf /backups/axiom-firewall-$(date +%Y%m%d).tar.gz \
    -C $AXIOM_FIREWALL_TENANT_DIR .
```

Restoring is a `tar xzf` into a new mount on a fresh deploy.

## Upgrades

The dashboard performs `ALTER TABLE ... ADD COLUMN` migrations on
startup automatically. Forward-only — no destructive migrations are
planned through Phase 3. Read the release notes before upgrading
across a major release.