diff --git a/Architecture.md b/Architecture.md index 6c6946f..4bf341f 100644 --- a/Architecture.md +++ b/Architecture.md @@ -1,143 +1,180 @@ -# Architecture +# Atlas OS — System Architecture -Atlas OS runs on **one box**. Not a cluster, not a hyperscaler region, not a sprawl of managed services — a single EU-sovereign Hetzner VPS (NL/DE) that hosts the entire stack. This page explains why that is a deliberate design choice, how the edge funnels every request through one identity gate, and how internal services stay invisible to the public internet. +## Overview: The One-Box Philosophy -> EU-sovereign by design: hosting sits in NL/DE, outside the reach of the US Cloud Act. +Atlas OS is deliberately a single-server architecture. One Hetzner CPX32 VPS +(8 vCPU · 16 GB RAM · 150 GB NVMe · Nuremberg) runs the complete operational +infrastructure of Atlas Corporation: CRM, finance, communications, AI, document +management, fleet control, and 11 websites. + +**Why one box?** For a solo founder, operational simplicity outweighs redundancy. +One server = one bill, one SSH target, one Caddy config, one Authentik instance, +and zero inter-datacenter latency. The Tailscale overlay provides device-level +redundancy for access even if Caddy is down. --- -## The one-box philosophy +## Network Topology -Everything Atlas needs to operate as a business lives on a single host: - -- **~55 Docker containers** running the full application estate -- **~24 public services** exposed to the internet -- **One Caddy edge** terminating TLS and routing every hostname -- **One identity provider** (Authentik) gating access before any app is reached - -The advantage is operational gravity: one place to back up, one place to patch, one network namespace to reason about, one TLS configuration, one SSO policy. A solo operator can hold the whole system in their head. The trade-off — a single failure domain — is mitigated by nightly backups and the discipline of keeping the surface small. - -| Layer | Component | Role | -|---|---|---| -| Edge | Caddy | TLS termination, reverse proxy, `forward_auth` to SSO | -| Identity | Authentik (embedded outpost) | SSO gate in front of protected apps | -| Apps | ~55 Docker containers | Cockpit, Work, AI, Files & Comms, Ops, Sites | -| Private spine | Tailscale | Internal-only reach via `tailscale serve` | -| Mesh | NATS | Node-to-node messaging | -| Remote | MeshCentral | Manage client POS / devices | - -Admin identity across the box: **atlasshb**. - ---- - -## Request path: Caddy edge → Authentik SSO → apps - -Every public hostname resolves to Caddy. Caddy terminates TLS, then — for protected routes — performs a `forward_auth` handshake against the **embedded Authentik outpost**. Only after Authentik confirms the session does the request reach the upstream container. - -```mermaid -%%{init:{'theme':'base','themeVariables':{'primaryColor':'#0c1428','primaryTextColor':'#cfe2ff','primaryBorderColor':'#1F6FE0','lineColor':'#4EA0FF'}}}%% -flowchart TD - U[Browser / client] -->|HTTPS| C[Caddy edge
TLS termination] - C -->|forward_auth| A[Authentik
embedded outpost] - A -->|session valid| C - A -.->|no session| L[Login / MFA] - L --> A - C -->|authed request| OS[Atlas OS desktop
command.atlascorporation.nl/os/] - C --> APPS[Protected apps] - - subgraph box["One Hetzner VPS — EU-sovereign · ~55 containers"] - C - A - OS - APPS - subgraph internal["Internal-only (no public ingress)"] - T[Loopback-bound services] - TS[tailscale serve] - end - APPS --- T - T --- TS - end - - TS -.->|private spine| TN[Tailscale tailnet] - APPS --> M[(The Mind
kernel.sqlite spine)] - M --> R[LLM router :8888] +``` + ┌──────────────────────────────────────────────┐ + Public Internet │ atlas-01 (Hetzner CPX32) │ + ───────────── │ ┌─────────┐ ┌──────────┐ │ + Browser/App ──────►│ │ Caddy │───►│Authentik │──► Services │ + │ │ (TLS) │ │ (SSO) │ │ + │ └─────────┘ └──────────┘ │ + │ │ + Tailscale mesh │ ┌────────────────────────────────────────┐ │ + ──────────────── │ │ Private tailnet (100.x.x.x) │ │ + iOS / PC / Laptop─►│ │ Webtop desktop · internal dashboards │ │ + │ └────────────────────────────────────────┘ │ + └──────────────────────────────────────────────┘ ``` -The browser desktop at **command.atlascorporation.nl/os/** is itself one of these protected apps — 39 apps organised into Cockpit, Identity, AI, Work, Files & Comms, Ops, and Sites categories. See [[Atlas-OS]] for the full app catalog and [[Security]] for the SSO and MFA model. +### Public → SSO path (every service except mail ports) +1. Request hits Caddy (443 → Docker port) +2. Caddy runs `forward_auth` against Authentik outpost +3. Authentik validates session cookie / issues redirect to login +4. On success: Caddy copies `X-Authentik-*` headers and proxies to service + +### Tailscale-only services +- Webtop remote desktop (`tailscale serve :8497`) +- Internal dashboards (portal, status feeds) +- atlas-01 SSH (blocked on public firewall, tailnet-only) --- -## Loopback-binding + `tailscale serve` for internal services +## Service Layers -Not everything should be reachable from the public internet. Atlas keeps its internal surface small with two complementary techniques: +### Tier 0 — Identity & Security +- **Authentik** (`auth.{DOMAIN}`) — OIDC/SSO, MFA, app proxies +- **Vaultwarden** (`vault.{DOMAIN}`) — end-to-end encrypted password vault +- **Caddy** — automatic TLS, reverse proxy, the sole public entry point -### 1. Loopback binding +### Tier 1 — Core Business +- **Odoo 19** — CRM, invoicing, project management, contacts +- **Nextcloud 31** — file storage, DAV (calendar/contacts sync to iOS) +- **Stalwart** — self-hosted email: SMTP, IMAPS, JMAP, Sieve +- **Forgejo** — self-hosted git (this very repo) -Internal-only services bind to **loopback** rather than a public interface. They are reachable from the box itself (and from other containers/proxies on the host) but have **no public ingress**. Caddy can still reverse-proxy to them when an authenticated, SSO-gated route legitimately needs them — but the open internet cannot hit them directly. +### Tier 2 — Finance & Operations +- **Firefly III** — double-entry bookkeeping, budgets, reconciliation +- **Enable Banking** — PSD2 bank feed (ING, Knab → Firefly) +- **Paperless-ngx** — document management, OCR, auto-tagging +- **n8n** — workflow automation (webhooks, scheduled tasks) +- **Uptime Kuma** — service health monitoring with alerting -### 2. `tailscale serve` over the private spine +### Tier 3 — AI & Intelligence +- **LiteLLM proxy** — unified OpenAI-compat API to all models (local + cloud) +- **Open-WebUI** — ChatGPT-style UI on top of LiteLLM +- **Atlas Brain** (`atlas_brain.py`) — Python signal collector, runs every 15 min +- **Atlas Loop** — background nudge cycle, publishes to NATS +- **Atlas Meridian** — autonomous decision engine / battle responder +- **Langfuse** — LLM observability: every call traced with latency, cost, tokens +- **Local model router** (`:8888`) — OpenAI-compat router for local Ollama models -Where a service needs to be reached from another machine — but should never be public — it is exposed via **`tailscale serve`** on the Tailscale tailnet. This is the private spine: only devices enrolled in the tailnet can reach those services. The rule is explicit and absolute: +### Tier 4 — Fleet & Infrastructure +- **MeshCentral** — remote device access, POS terminal fleet management +- **NATS** — real-time pub/sub event bus across all Tailscale devices +- **Atlas heartbeat sink** — aggregates device heartbeats from the NATS mesh +- **Atlas cockpit feeds** — on-disk JSON feeds consumed by the OS desktop +- **Webtop** — full XFCE Linux desktop in the browser (tailnet-only) -> Internal services are reached via `tailscale serve` — **never** exposed publicly. +### Tier 5 — Managed Sites (11 sites) -This is why the public service count (~24) is far smaller than the container count (~55): the majority of containers are either dependencies of other services or deliberately kept off the public edge, reachable only over loopback or the tailnet. +All served via Caddy. WordPress sites run in isolated Docker stacks: -| Exposure tier | How it's reached | Example surface | -|---|---|---| -| Public | Caddy → Authentik → app | Atlas OS desktop, public sites | -| Loopback | Bound to localhost; proxied via authed Caddy route only | Backend dependencies | -| Tailnet | `tailscale serve`, tailnet members only | Internal-only services | - -The **Remote Desktop** (Webtop `ubuntu-xfce`, Selkies/WebRTC) at `desktop.atlascorporation.nl` is a good illustration of the edge model in practice: it sits behind Authentik SSO, and ships with Claude Code + Chromium pre-installed so OAuth logins work in a real browser. See [[Remote-Desktop]]. +| Domain | Stack | Notes | +|--------|-------|-------| +| `atlasworks.nl` | Docker: works-wordpress | Live WooCommerce (Caddy :8111) | +| `atlascorporation.nl` | Docker: corp-wordpress | Corporate site | +| `atlasagency.nl` | Docker: agency-wordpress | Agency services | +| `atlasacademy.nl` | TransIP nginx (static HTML) | Academy landing | +| `atlaspos.nl` | TransIP | AtlasPOS product site | --- -## Data flows through the Mind +## Data Flow — Atlas Brain Cycle -Atlas is not just a pile of apps behind a proxy — it has a reasoning core, and operational data flows through it. - -- **The spine is `kernel.sqlite`** — the canonical store of decisions and events (~458 decisions, ~3415 events recorded). -- **Atlas Brain** is the reviewed-execution autonomous core. It can act, but it acts under a gate. -- **The policy gate ALWAYS requires human approval** for anything touching **money**, **external communications**, or **self-modification**. There is no autonomous path around it. -- **The LLM router at `localhost:8888`** picks the cheapest capable model for each task, escalating only when needed: - -```mermaid -%%{init:{'theme':'base','themeVariables':{'primaryColor':'#0c1428','primaryTextColor':'#cfe2ff','primaryBorderColor':'#1F6FE0','lineColor':'#4EA0FF'}}}%% -flowchart LR - Q[Task] --> R{LLM router
localhost:8888} - R -->|first| LOC[Local models
LLaMA 1B · hermes3 · Qwen Coder · Qwen 7B] - R -->|then| FREE[Free cloud
Groq llama-3.3-70b · Cerebras · OpenRouter] - R -->|last| PAID[Claude — paid] - LOC --> H[Headroom proxy
context compression] - FREE --> H - PAID --> H +``` +Every 15 minutes (atlas-brain.service): + ┌──────────────────────────────────────────┐ + │ Signal collection phase │ + │ ├── 7 IMAP mailboxes (read-only) │ + │ ├── Odoo REST API (leads, contacts) │ + │ ├── Enable Banking (bank transactions) │ + │ └── atlas-01 health/fleet feeds │ + └──────────────────┬───────────────────────┘ + │ + ▼ + atlas-state.json + │ + ▼ + LLM synthesis (model router): + atlas-auto (local) → Groq 70B → Claude Opus + │ + ▼ + atlas-brief-spoken.json (daily brief) + │ + ▼ + Atlas OS cockpit panels (Today · Money · Inbox · Pipeline) ``` -The router binds locally (`localhost:8888`) — itself an example of the loopback-binding principle: the model router is an internal service, not a public endpoint. The **Headroom proxy** compresses context so requests stay cheap. Local models are tried first, free cloud tiers next, and Claude (paid) only as a last resort. +--- -For how the Mind, the policy gate, and the router fit together operationally, see [[AI-Stack]] and [[Atlas-Brain]]. +## The AI Mesh + +``` +atlas-01 (NATS JetStream server) + │ + ├── subjects: + │ atlas.events.* system events (disk, CPU, service state) + │ atlas.heartbeat.* device health pings + │ atlas.jobs.* delegated compute jobs (Ollama on NEBULA) + │ atlas.brief.* daily brief push to all devices + │ + └── subscribers: + Caddy PC (this machine) ← atlas-events-collector + NEBULA (HP EliteBook) ← atlas-delegate-worker (5 Ollama models) + MET-Desktop ← Odoo failover + fleet monitor +``` --- -## Why this holds together +## Caddy SSO Pattern -| Principle | Implementation | -|---|---| -| One failure domain, one control plane | Single Hetzner VPS, ~55 containers | -| Sovereign data | EU hosting (NL/DE), no US Cloud Act exposure | -| One front door | Caddy terminates TLS for every hostname | -| One identity gate | Authentik `forward_auth` on the embedded outpost | -| Small public surface | Loopback binding + `tailscale serve` keep internals private | -| Cheapest-capable compute | LLM router: local → free cloud → Claude last | -| No silent autonomy on what matters | Policy gate requires human approval for money / comms / self-modify | +Every SSO-gated service uses this Caddyfile block (see `deploy/caddy/Caddyfile.template`): + +```caddy +app.example.com { + # Authentik outpost endpoints (must come first) + reverse_proxy /outpost.goauthentik.io/* authentik-server:9000 + + # SSO gate — redirects to login if no valid session + forward_auth authentik-server:9000 { + uri /outpost.goauthentik.io/auth/caddy + copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email + X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt + } + + # Only reached after successful authentication + reverse_proxy localhost:APP_PORT +} +``` --- -### See also +## Docker Compose Topology -- [[Atlas-OS]] — the 39-app browser desktop -- [[AI-Stack]] — the Mind, the router, and the policy gate -- [[Security]] — EU-sovereign hosting, MFA, backups, secrets -- [[Fleet-and-Mesh]] — MeshCentral, Tailscale spine, NATS -- [[Remote-Desktop]] — Webtop behind SSO +| Stack | File | Services | +|-------|------|----------| +| Core | `deploy/compose/atlas-core.yml` | Authentik, Nextcloud, Stalwart, Forgejo, Vaultwarden | +| AI | `deploy/compose/atlas-ai.yml` | LiteLLM, Open-WebUI, Langfuse | +| Business | `atlas-odoo.yml` | Odoo 19 + PostgreSQL | +| Finance | `atlas-finance.yml` | Firefly III + Paperless-ngx + Enable Banking | +| Fleet | `atlas-fleet.yml` | MeshCentral + NATS + Uptime Kuma | +| Sites | Per-site compose | WordPress stacks | +| Desktop | `deploy/webtop-docker-compose.yml` | Webtop (tailnet-only) | + +All stacks share the `atlas-net` Docker network. Caddy is the **sole** process with +public port bindings (80/443). All internal services bind only to `127.0.0.1:PORT`.