Skip to content

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.

chris-os network topology — 5 hosts, 3 VLANs, and 5 Docker bridge networks

HostRole
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).
VLANPurpose
Primary LAN (default)Primary trusted LAN. All infrastructure hosts.
IoT VLANIoT / smart home devices. Isolated SSID.
Management VLANNetwork 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.

Five isolated bridge networks separate service tiers. Services can only reach other services that share a network — there is no catch-all default bridge.

NetworkMembers
net-datapostgres, redis, n8n, authelia, dashboard-api, mcp-proxy-postgres, mcp-proxy-memory, grafana, postgres-exporter, blackbox-exporter, docker-socket-proxy, discord-bot
net-appn8n, authelia, waha, caddy, mcp-proxy-n8n, blackbox-exporter, discord-bot
net-mcpmcp-proxy-{postgres, n8n, memory, unifi}, mcp-auth-{postgres, n8n, memory, ha}, caddy, cloudflared
net-frontendcaddy, cloudflared, mcp-auth-{postgres, n8n, memory, ha}, dashboard-api, dashboard-nginx, grafana, prometheus, docker-socket-proxy
net-monitoringgrafana, 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.

chris-os request flow — external user through Cloudflare to Caddy, Authelia auth decision, and protected services

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.

Requests from LAN clients are routed directly to n8n.

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 service
Claude Desktop (LAN)
→ Caddy (MCP listener)
→ mcp-auth-{service}
validates API key
→ mcp-proxy-{service}
→ upstream service

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

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.yml is 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:

TargetPolicy
Health check pathsbypass
n8n webhooks and test webhooksbypass
MCP endpointsbypass (own auth layer)
Status pageone_factor
Dashboard docs pathsone_factor
Dashboard (all other paths)two_factor
n8n editortwo_factor
Grafanatwo_factor (Grafana handles OIDC directly)
Everything elsetwo_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 issues OIDC tokens for native app clients:

ClientTypeAuth PolicyPurpose
Home Assistantpublic (PKCE S256)one_factorHA OAuth login
Hudson iOSpublic (PKCE S256)two_factoriOS app access
Grafanaconfidential (PKCE S256)two_factorGrafana SSO

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:

ContainerUpstream
mcp-auth-postgresmcp-proxy-postgres
mcp-auth-n8nmcp-proxy-n8n
mcp-auth-memorymcp-proxy-memory
mcp-auth-haHome 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)

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

LayerDetails
Cloudflare edgeTerminates the browser’s TLS connection. Browser sees a Cloudflare-issued certificate.
Cloudflare to TunnelEncrypted QUIC/H2 between Cloudflare edge and the cloudflared container. No inbound port required on the production host.
cloudflared to CaddyEncrypted connection between Cloudflare and Caddy.
CaddyHolds the *.ataraxis.cloud wildcard certificate (Let’s Encrypt, DNS-01 challenge via Cloudflare API). Auto-renewed.
Caddy to containersPlaintext 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:

SubdomainBackendAuth
auth.ataraxis.cloudAutheliaNone (auth portal itself)
dashboard.ataraxis.cloudDashboard API + SPAAuthelia 2FA
n8n.ataraxis.cloudn8nAuthelia 2FA (editor) / bypass (webhooks)
grafana.ataraxis.cloudGrafanaOIDC direct
ha.ataraxis.cloudHome AssistantHA own auth
status.ataraxis.cloudGatusAuthelia one_factor

Services reached via Cloudflare Tunnel directly (bypassing Caddy, for MCP):

EndpointBackendAuth
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).

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