Authentication & Networking
All external traffic enters through Cloudflare — there are no publicly exposed ports on the production host’s IP address. A Cloudflare Tunnel running on the production host maintains an outbound encrypted connection to Cloudflare’s edge, and all inbound requests arrive through that tunnel.
Network Topology
Section titled “Network Topology”Host Fleet
Section titled “Host Fleet”| Host | Role |
|---|---|
| Caroline (Pi 5) | PRIMARY production host. 37 Docker containers: PostgreSQL, n8n, Caddy, Authelia, all MCP services, dashboard, observability stack, Home Assistant, voice pipeline. |
| Atlas (M4 Pro Mac) | Primary Ollama inference host. Sleep disabled, LAN-accessible. Also serves as autonomous ops orchestrator. |
| Nightwatch (AMD GPU node) | Voice services only. On-demand via Wake-on-LAN; 5-minute idle suspend. Woken automatically on satellite wake-word detection. |
| Relaxation-Vault (MBA M1) | Headless media server. 14 arr-stack containers + Plex. Gatus status page. |
| Companion-Cube (Synology NAS) | Storage and off-site backup target. DSM frozen. Docker containers stopped (media stack operates from Relaxation-Vault). |
Network Segments
Section titled “Network Segments”Physical / VLAN Segments
Section titled “Physical / VLAN Segments”| VLAN | Purpose |
|---|---|
| Primary LAN (default) | Primary trusted LAN. All infrastructure hosts. |
| IoT VLAN | IoT / smart home devices. Isolated SSID. |
| Management VLAN | Network management. Wake-on-LAN broadcast domain for NAS and Nightwatch. |
The production host has tagged VLAN interfaces for all three segments. The IoT VLAN interface is what gives Home Assistant (which runs with network_mode: host) direct visibility to IoT devices without an extra gateway hop.
Docker Networks (all on Caroline)
Section titled “Docker Networks (all on Caroline)”Five isolated bridge networks separate service tiers. Services can only reach other services that share a network — there is no catch-all default bridge.
| Network | Members |
|---|---|
net-data | postgres, redis, n8n, authelia, dashboard-api, mcp-proxy-postgres, mcp-proxy-memory, grafana, postgres-exporter, blackbox-exporter, docker-socket-proxy, discord-bot |
net-app | n8n, authelia, waha, caddy, mcp-proxy-n8n, blackbox-exporter, discord-bot |
net-mcp | mcp-proxy-{postgres, n8n, memory, unifi}, mcp-auth-{postgres, n8n, memory, ha}, caddy, cloudflared |
net-frontend | caddy, cloudflared, mcp-auth-{postgres, n8n, memory, ha}, dashboard-api, dashboard-nginx, grafana, prometheus, docker-socket-proxy |
net-monitoring | grafana, prometheus, loki, tempo, alloy, node-exporter, cadvisor, postgres-exporter, blackbox-exporter, otel-collector, docker-socket-proxy-grafana, unpoller, n8n |
The key design constraint: Caddy sits on net-frontend + net-app + net-mcp. It bridges the public-facing layer to application and MCP services, but it never reaches net-data directly. PostgreSQL and Redis are only accessible from services that explicitly share net-data.
Request Flow
Section titled “Request Flow”Traffic Flows
Section titled “Traffic Flows”External browser request
Section titled “External browser request”Browser → Cloudflare CDN (TLS termination, Cloudflare certificate) → Cloudflare Tunnel (encrypted QUIC/H2, outbound from production host) → cloudflared container (net-frontend) → Caddy (net-frontend, wildcard TLS cert via Let's Encrypt DNS-01) → Authelia forward_auth subrequest (net-app) ↳ 200: Remote-User/Remote-Groups headers set, request continues ↳ 401: redirect browser to auth.ataraxis.cloud → backend service (dashboard-api, n8n, grafana, etc.)External webhook (e.g., Google Pub/Sub to n8n)
Section titled “External webhook (e.g., Google Pub/Sub to n8n)”Caddy has a bypass path for webhook URLs — Authelia is never called. The request goes directly to n8n, which validates its own per-service credential.
LAN webhook (e.g., Home Assistant to n8n)
Section titled “LAN webhook (e.g., Home Assistant to n8n)”Requests from LAN clients are routed directly to n8n.
MCP from claude.ai (OAuth Worker path)
Section titled “MCP from claude.ai (OAuth Worker path)”claude.ai → Cloudflare Worker (OAuth provider, GitHub auth backend) issues scoped Bearer tokens after GitHub OAuth → Cloudflare Tunnel → mcp-auth-{service} validates Cloudflare Access JWT → mcp-proxy-{service} → upstream serviceMCP from Claude Desktop (API key path)
Section titled “MCP from Claude Desktop (API key path)”Claude Desktop (LAN) → Caddy (MCP listener) → mcp-auth-{service} validates API key → mcp-proxy-{service} → upstream serviceMemory MCP from Claude Code (stdio bridge)
Section titled “Memory MCP from Claude Code (stdio bridge)”Claude Code → mcp-memory-bridge.cjs (stdio) → HTTP/SSE → mcp-auth-memory → mcp-proxy-memory → mcp-ai-memory child process → PostgreSQL (memory schema) → Ollama on inference host (embeddings)Home Assistant MCP
Section titled “Home Assistant MCP”claude.ai or Claude Desktop → Cloudflare Tunnel → mcp-auth-ha validates credential, injects HA access token → Home Assistant API (host network, port 8123) → /api/mcp (ha-mcp v7.0.0, 89 tools)Authentication Architecture
Section titled “Authentication Architecture”Authelia SSO
Section titled “Authelia SSO”Authelia is the single sign-on provider for all browser-accessible services. It runs as a Docker container, accessible only on Docker-internal networks.
- Session cookie domain:
.ataraxis.cloud— one login covers all subdomains - 2FA: TOTP (6-digit, 30s) + WebAuthn
- Session store: Redis (Docker-internal, password-authenticated)
- TOTP secrets: PostgreSQL (AES-encrypted at rest)
- Credentials: File-based (
users.yml, argon2id). NOTE:users.ymlis rehashed on every login. It cannot be restored from git. Hourly backup runs automatically.
Caddy integrates via forward_auth — every request gets a subrequest to Authelia before being proxied. On success, Caddy copies user identity headers downstream. On failure, the browser is redirected to the login portal.
Access policy summary:
| Target | Policy |
|---|---|
| Health check paths | bypass |
| n8n webhooks and test webhooks | bypass |
| MCP endpoints | bypass (own auth layer) |
| Status page | one_factor |
| Dashboard docs paths | one_factor |
| Dashboard (all other paths) | two_factor |
| n8n editor | two_factor |
| Grafana | two_factor (Grafana handles OIDC directly) |
| Everything else | two_factor |
Grafana and Home Assistant bypass Authelia’s forward_auth at the Caddy level and manage their own authentication:
- Grafana: OIDC flow directly with Authelia as IdP
- Home Assistant: own auth provider (HA login), with OIDC config present but disabled
Authelia as OIDC Provider
Section titled “Authelia as OIDC Provider”Authelia issues OIDC tokens for native app clients:
| Client | Type | Auth Policy | Purpose |
|---|---|---|---|
| Home Assistant | public (PKCE S256) | one_factor | HA OAuth login |
| Hudson iOS | public (PKCE S256) | two_factor | iOS app access |
| Grafana | confidential (PKCE S256) | two_factor | Grafana SSO |
MCP Auth Middleware
Section titled “MCP Auth Middleware”Each MCP endpoint has a dedicated auth middleware container. Two credential types:
Type 1: Cloudflare Access JWT (claude.ai via OAuth Worker)
- GitHub OAuth -> OAuth Worker issues Cloudflare Access JWT
- Auth middleware validates JWT against team name and per-service audience
Type 2: API key (Claude Desktop, scripts, n8n)
- Per-service keys for blast-radius isolation
- Multiple keys per service supported for zero-downtime rotation
Auth middleware containers:
| Container | Upstream |
|---|---|
| mcp-auth-postgres | mcp-proxy-postgres |
| mcp-auth-n8n | mcp-proxy-n8n |
| mcp-auth-memory | mcp-proxy-memory |
| mcp-auth-ha | Home Assistant API (host network) |
mcp-auth-ha additionally injects a long-lived HA access token on all forwarded requests — the downstream HA instance sees standard Bearer auth.
Additional protections in the auth middleware:
- Rate limiting per IP (sliding window)
- Strips credential query parameters before forwarding to the upstream proxy
- Injects a separate upstream credential header on all forwarded requests (defense-in-depth)
Dashboard API Auth (Fastify)
Section titled “Dashboard API Auth (Fastify)”The dashboard API (dashboard-api) accepts two parallel auth mechanisms:
Authelia SSO path (browser): Caddy sets user identity headers after forward_auth succeeds. Fastify validates a proxy secret before trusting those headers to prevent container-peer forgery.
API key path (machines: n8n, scripts, iOS app, hooks): Machine-to-machine paths accept API key validation inside Fastify. Caddy strips user identity headers on these paths to prevent header injection.
Hudson iOS JWT path: iOS API paths bypass Authelia in Caddy. Fastify validates Bearer JWT tokens (Authelia-issued, 1h access / 30d refresh).
| Layer | Details |
|---|---|
| Cloudflare edge | Terminates the browser’s TLS connection. Browser sees a Cloudflare-issued certificate. |
| Cloudflare to Tunnel | Encrypted QUIC/H2 between Cloudflare edge and the cloudflared container. No inbound port required on the production host. |
| cloudflared to Caddy | Encrypted connection between Cloudflare and Caddy. |
| Caddy | Holds the *.ataraxis.cloud wildcard certificate (Let’s Encrypt, DNS-01 challenge via Cloudflare API). Auto-renewed. |
| Caddy to containers | Plaintext HTTP over Docker bridge networks. All intra-container communication is unencrypted on private Docker subnets. |
LAN MCP endpoints: Caddy provides HTTP listeners for LAN clients on dedicated ports. The Cloudflare Tunnel path always provides TLS.
Home Assistant: Binds directly to the host network on port 8123. LAN access is plain HTTP. External access via ha.ataraxis.cloud goes through the encrypted Cloudflare Tunnel path.
Public DNS (ataraxis.cloud): All *.ataraxis.cloud subdomains point to Cloudflare’s anycast edge (orange-cloud proxy). No A records expose the production host’s public IP directly.
Services reached via Cloudflare Tunnel through Caddy:
| Subdomain | Backend | Auth |
|---|---|---|
| auth.ataraxis.cloud | Authelia | None (auth portal itself) |
| dashboard.ataraxis.cloud | Dashboard API + SPA | Authelia 2FA |
| n8n.ataraxis.cloud | n8n | Authelia 2FA (editor) / bypass (webhooks) |
| grafana.ataraxis.cloud | Grafana | OIDC direct |
| ha.ataraxis.cloud | Home Assistant | HA own auth |
| status.ataraxis.cloud | Gatus | Authelia one_factor |
Services reached via Cloudflare Tunnel directly (bypassing Caddy, for MCP):
| Endpoint | Backend | Auth |
|---|---|---|
| Dedicated Cloudflare Tunnel endpoints (4 services) | mcp-auth-{db, n8n, memory, ha} | CF Access JWT or API key |
Internal DNS: Resolved by the UniFi gateway. Used for NAS and workstation hostnames on the LAN.
Docker DNS: Docker’s embedded resolver handles container-name resolution within shared networks. Services reference each other by container name (e.g., postgres, redis, authelia, n8n).
Auth Decision Tree
Section titled “Auth Decision Tree”Incoming request to *.ataraxis.cloud │ ├── Caddy bypass path? (webhooks, health, machine-to-machine API paths, MCP domains) │ YES → skip Authelia; may require API key / JWT inside the backend │ ├── MCP endpoint (dedicated Cloudflare Tunnel subdomains)? │ YES → mcp-auth-* validates CF Access JWT or API key │ ├── ha.ataraxis.cloud? │ YES → HA handles its own auth │ ├── grafana.ataraxis.cloud? │ YES → Grafana handles OIDC flow directly with Authelia │ └── All other *.ataraxis.cloud Caddy calls Authelia forward_auth → one_factor (status, docs paths) or two_factor (everything else) → success: user identity headers set, request continues → failure: redirect to auth.ataraxis.cloud