From 6a5cb95f92435c2fac6117796d80c5ce3046c8e9 Mon Sep 17 00:00:00 2001 From: Bjoern Runden Date: Sat, 3 Oct 2026 13:22:51 +0200 Subject: [PATCH] final bis auf prio 2 Todos --- .ai/sultan-system-architecture.md | 8 + CLAUDE.md | 2 +- README.md | 6 + docs/community.md | 274 ++++++++++++++++++++++++++++++ docs/hoster.md | 166 ++++++++++++++++++ docs/wartung.md | 145 ++++++++++++++++ 6 files changed, 600 insertions(+), 1 deletion(-) create mode 100644 docs/community.md create mode 100644 docs/hoster.md create mode 100644 docs/wartung.md diff --git a/.ai/sultan-system-architecture.md b/.ai/sultan-system-architecture.md index da364db..e6038f6 100644 --- a/.ai/sultan-system-architecture.md +++ b/.ai/sultan-system-architecture.md @@ -873,12 +873,20 @@ services: * offen: systemd-Unit, `install.sh`, Signatur von `SHA256SUMS` (Ed25519, Schlüssel nur im Runner), Gitea Actions (Tests bei Push, Release bei Tag `v*`), Image-Digest in der veröffentlichten Compose-Vorlage, optional Spiegel auf ghcr.io * Installationsskript für `curl | sh`: Herkunft mit öffentlichem Zertifikat und/oder Prüfsumme bzw. Signatur. Die eigentliche Vertrauensfrage ist das Skript, nicht der Token. +**Projektstand 2026-10-03: vorerst fertig.** Erster Feldtest (webproxy11 + dockweb11) erfolgreich; Betriebsdoku unter `docs/` (`community.md`, `wartung.md`, `hoster.md`). Weitere Iterationen nach Schwierigkeiten im Betrieb. Offen aus dem Abschluss: +* **Ungetestet im Feld:** Agent als systemd-Dienst (ohne Docker); kommerzieller Betrieb (Lizenzen, Mahnwesen, Vertragsende, pro/enterprise). +* **`tenant update`** (Vertragsende, Domains, Tier, Routenlimit) – Voraussetzung für zahlende Kunden; `store.UpdateTenant` existiert, CLI fehlt. +* **Lander-Vorlagen gestalten** („hübsch“): die eingebauten Seiten sind bewusst minimal ohne CSS. +* **Registry mit Vanity-URL:** `zot.chmln.de` ist noch nicht weiter getrieben (Image-Name unabhängig vom Registry-Host, z. B. `cr./sultan`). +* **Geparkt:** Veröffentlichung auf GitHub (Quellen, Releases, ggf. ghcr.io-Spiegel). + **Weitere offene Bausteine (ohne feste Reihenfolge):** * **Lastverteilung einer Domain über mehrere Knoten** (gemeinsamer Load-Balancer statt getrennter Router) – Voraussetzung dafür, dass `drain` seine Stärke ausspielt. * Admin-API (nur kommerziell): Mandanten, Lizenzen, Zahlungsstatus für Rechnungssystem und Ansible; Authentifizierung per API-Token, TLS über ACME. * *(umgesetzt 2026-10-02: Rate-Limit für `/enroll`, Umgebungsvariablen für alle Flags, „wieder online“-Meldung, Checkmk mit Sekunden und Zeitzone)* * Agent: Healthchecks auch für Docker-Container (Docker-Health-Status oder Labels `sultan.healthcheck.*`). * CLI: `tenant update` (Domains/Tier/Laufzeit ändern), JSON-Ausgabe für Skripte. +* **`public_reason` zweisprachig** (klein): Die Wartungsseite ist de/en, der öffentliche Grund bisher nur ein Text. Vorschlag: `--public-reason` mehrfach mit Sprachpräfix (`de:…`, `en:…`; ohne Präfix = für alle Sprachen), Label analog `sultan.maintenance.public_reason.`; Übertragung im bestehenden Header `X-Sultan-Public-Reason` als URL-kodierte Form (`de=…&en=…`), Lander wählt je Sprachblock, fehlt eine Sprache → der andere Text. * Sicherheit (3.4): geordneter Zertifikatswechsel, optionale Root-CA. * **Ausblick Monitoring:** Die Zahl der Policy-Verstöße der letzten 24 h (`Sultan_Policy_Violations`) zählt der Server derzeit im Speicher; nach einem Neustart beginnt sie bei 0. Für Checkmk reicht das. Wird sie dauerhaft gebraucht (Auswertung, Audit), ist das eine kleine Erweiterung: Verstöße mit Zeitpunkt in einer Tabelle ablegen und im Aufräum-Job nach der Frist löschen. diff --git a/CLAUDE.md b/CLAUDE.md index 477025f..a87aec1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,7 +38,7 @@ Go-Modul `sultan` (Go 1.26). `main.go` ist nur eine Hülle um `pkg/cli`; umgeset **CLI-Umgebung:** `pkg/cli/env.go` – `annotateEnv`/`applyEnv` machen jedes Flag als `SULTAN_` setzbar (nach `fs.Parse`, nur für nicht gesetzte Flags; Listen-Flags `stringList` kommagetrennt). Neue Flags bekommen das automatisch. Rate-Limit: `pkg/server/ratelimit.go` (`EnrollLimit`, `-1` = aus). -Was noch fehlt: Betrieb (Spec Abschnitt 9, Schritt 5) und offene Fachfragen: Spec Abschnitt 9. +Betriebsdoku: `docs/community.md`, `docs/wartung.md`, `docs/hoster.md` – bei Verhaltensänderungen mitpflegen. Projekt gilt seit 2026-10-03 vorerst als fertig; offene Punkte (tenant update, systemd-Agent und kommerzieller Betrieb ungetestet, Lander-Design, Vanity-Registry, GitHub geparkt) und Fachfragen: Spec Abschnitt 9. Tests werden zuerst als Vorgaben geschrieben, dann implementiert. Zeitabhängige Tests nutzen kurze Intervalle über die Konfigurationsfelder bzw. das injizierbare `Client.sleep`; mit `-race -count=30` auf Stabilität prüfen. diff --git a/README.md b/README.md index 872714b..572053f 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,12 @@ Ein einziges Binary, drei Rollen: Die ausführliche Architektur steht in [.ai/sultan-system-architecture.md](.ai/sultan-system-architecture.md). +## 📚 Betriebsdoku + +- 🏠 [docs/community.md](docs/community.md) – Server neben Traefik (host-Netz oder Docker-Netz) und Agent einrichten, aktualisieren +- 🔧 [docs/wartung.md](docs/wartung.md) – maintenance, drain, `stop` vs. `down`, Reboot-Ablauf +- 💼 [docs/hoster.md](docs/hoster.md) – Kunden-Onboarding für Hoster (pro/enterprise), Mahnwesen, Vertragsende + ## 🚀 Installation > 🚧 **Hinweis:** Sultan befindet sich noch am Anfang der Entwicklung. Die folgenden Schritte zeigen, wie die Installation aussehen *wird* – Befehle und Flags können sich noch ändern. diff --git a/docs/community.md b/docs/community.md new file mode 100644 index 0000000..4751859 --- /dev/null +++ b/docs/community.md @@ -0,0 +1,274 @@ +# Sultan im Eigenbetrieb (Community) + +Anleitung für alle, die Sultan selbst betreiben: ein Ingress-Host mit Traefik und `sultan server`, dazu beliebig viele Docker-Hosts mit `sultan agent`. Das betrifft Homelab, kleine Firmen und einen Hoster in eigener Sache. + +```text +Besucher ──▶ Traefik ──(pollt alle 3 s)──▶ sultan server ◀──(TLS, ausgehend)── sultan agent ── Docker + │ │ + └──────────────── HTTP zum veröffentlichten Port ──────────────────┘ +``` + +Zwei Verbindungen müssen funktionieren: + +1. **Agent → Server**, TCP 8443 (TLS 1.3). Nur ausgehend vom Agent, funktioniert also auch hinter NAT/CGNAT. +2. **Traefik → Backend**, auf den *veröffentlichten* Port des Containers, z. B. `9080` bei `ports: "9080:80"`. Das ist normales HTTP. Der Ingress muss den Backend-Host also erreichen können: per LAN, öffentlicher IP oder VPN (z. B. NetBird). + +--- + +## 1. Image holen + +Server und Agent nutzen dasselbe Image. Die Rolle bestimmt der Befehl. + +```bash +docker pull zot.chmln.de/sultan: # z. B. v0.1.1; :latest = letztes Release +docker run --rm zot.chmln.de/sultan: sultan version +``` + +Auf Servern eine **feste Version** in die `.env` schreiben (`SULTAN_IMAGE=…:v0.1.1`). `latest` wird bei `docker compose up` nicht neu geladen; für ein Update ist immer `docker compose pull` nötig. + +Update später: + +```bash +$EDITOR .env # SULTAN_IMAGE auf neue Version +docker compose pull && docker compose up -d +docker compose exec sultan version +``` + +--- + +## 2. Server (Ingress-Host, neben Traefik) + +Vorlage: [deploy/compose/server](../deploy/compose/server). + +### 2.1 Wie läuft Traefik bei dir? + +Davon hängt ab, wie Sultan neben Traefik läuft: + +| Traefik läuft mit … | Sultan-Server | Traefik erreicht Sultan unter | +|---|---|---| +| `network_mode: host` | **Variante A**: ebenfalls `network_mode: host` (Vorlage) | `http://127.0.0.1:8080` | +| eigenem Docker-Netz (z. B. `traefik-public`) | **Variante B**: im selben bzw. einem gemeinsamen internen Netz | `http://sultan-server:8080` | + +**Sicherheitsregel für beide Varianten:** Port 8080 (Traefik-Konfiguration, Checkmk) und 8081 (Wartungsseiten) sind **Klartext und ohne Anmeldung**. Sie dürfen nur für Traefik erreichbar sein, nie aus dem Internet und nicht für fremde Container. Öffentlich ist nur 8443. + +### 2.2 Variante A: `network_mode: host` (empfohlen, Vorlage) + +```bash +mkdir -p /docker/sultan-server && cd /docker/sultan-server +cp /deploy/compose/server/{compose.yaml,.env.example,traefik-provider.yml} . +cp .env.example .env && $EDITOR .env +install -d -m 0700 -o 65532 -g 65532 suldata # nonroot-User im Image +``` + +In der `.env` mindestens: + +```bash +SULTAN_IMAGE=zot.chmln.de/sultan:v0.1.1 +SULTAN_PUBLIC_URL=https://ingress.example.de:8443 # so erreichen Agents den Server; landet in jeder Einladung +``` + +**Ports prüfen:** Traefiks API/Dashboard belegt oft schon `8080`. + +```bash +ss -ltnp | grep -E ':(8443|8080|8081)\b' +# belegt → in .env z. B.: +# SULTAN_PROVIDER_ADDR=127.0.0.1:8085 +# SULTAN_LANDER_ADDR=127.0.0.1:8086 +``` + +Starten und prüfen: + +```bash +docker compose up -d +docker compose logs sultan-server # "Sultan-Server gestartet" + Fingerprint +docker compose exec sultan-server sultan version # Version + Zeitzone +curl -s http://127.0.0.1:8080/healthz # ok +``` + +### 2.3 Variante B: Traefik in einem Docker-Netz + +Sultan muss hier auf allen Interfaces *im Container* lauschen und Traefik den Lander unter dem Containernamen nennen. Am besten legt man ein **eigenes internes Netz** nur für Traefik und Sultan an, statt `traefik-public` mitzunutzen: Sonst könnte jeder Container in `traefik-public` die Konfiguration (Backend-Adressen) lesen. + +```yaml +# compose.yaml (Variante B) – statt network_mode: host +services: + sultan-server: + image: ${SULTAN_IMAGE} + container_name: sultan-server + restart: unless-stopped + command: ["sultan", "server", "--data-dir=/suldata"] + ports: + - "8443:8443" # nur der Agent-Port nach außen + environment: + SULTAN_AGENT_ADDR: ":8443" + SULTAN_PROVIDER_ADDR: ":8080" # nur im Netz sultan-internal erreichbar + SULTAN_LANDER_ADDR: ":8081" + SULTAN_INTERCEPTOR_URL: "http://sultan-server:8081" # so erreicht Traefik die Wartungsseiten + SULTAN_PUBLIC_URL: ${SULTAN_PUBLIC_URL} + TZ: Europe/Berlin + volumes: + - ./suldata:/suldata + networks: [sultan-internal] + +networks: + sultan-internal: + name: sultan-internal # Traefik zusätzlich an dieses Netz hängen +``` + +Im Traefik-Compose: `networks: [traefik-public, sultan-internal]` (plus `sultan-internal: {external: true}`). + +Unterschiede zu Variante A: +- `curl` vom Host aus geht nicht. Prüfen per `docker compose exec traefik wget -qO- http://sultan-server:8080/healthz`. +- Die Client-IP der Agents (Rate-Limit, `node list`) ist meist korrekt. Bei IPv6 oder aktivem `userland-proxy` kann dort aber die Docker-Gateway-IP stehen. + +### 2.4 Traefik anbinden + +In die **statische** Konfiguration (`traefik.yml`), zusätzlich zu den vorhandenen Providern: + +```yaml +providers: + http: + endpoint: "http://127.0.0.1:8080/v1/traefik-config" # Variante B: http://sultan-server:8080/… + pollInterval: "3s" +``` + +Danach Traefik **neu starten** (statische Konfiguration). Im Dashboard taucht der Provider `http` auf. Routen von Sultan heißen dort `____@http`. + +Wichtig: Die Labels an den Diensten verweisen auf **Namen aus deiner Traefik-Konfiguration**: Entrypoints (`web`, `websecure`) und Certresolver (`le`). Sultan prüft nicht, ob es sie gibt; Traefik meldet Tippfehler im eigenen Log. + +### 2.5 Firewall + +- TCP **8443** eingehend von den Agent-Hosts (bzw. aus dem Internet, wenn Agents von überall kommen). +- 8080/8081 **nicht** öffnen. +- Ausgehend vom Ingress zu den Backend-Ports (siehe 3.4). + +### 2.6 Mandant und Einladung + +Auch im Eigenbetrieb gehört jeder Knoten zu einem Mandanten. Der Mandant legt fest, welche Domains seine Dienste beanspruchen dürfen. + +```bash +docker compose exec sultan-server sultan token tenant add \ + --id homelab --name "Mein Homelab" --email ich@example.de \ + --domains "example.de,*.example.de" --max-routes -1 +docker compose exec sultan-server sultan token invite --tenant homelab # 24 h gültig, einmalig +``` + +- `--tier` ist im Eigenbetrieb `community` (Default): kein Mahnwesen, kein Ablauf. +- `--max-routes -1` = unbegrenzt (Default 5). +- Domains: `example.de` deckt **nur** genau diese Domain. `*.example.de` deckt alle Subdomains, aber **nicht** `example.de` selbst; für beides braucht es beide Einträge. + +Die Ausgabe (`sltn_enroll_…`) ist die Einladung. Sie enthält Server-Adresse und Zertifikats-Fingerprint, ist also alles, was der Agent braucht. Sie ist **geheim** bis zur Benutzung und danach wertlos. + +### 2.7 Sichern + +`suldata/` enthält Datenbank, TLS-Schlüssel und Master-Key. Ohne diese Dateien müssen sich alle Agents neu anmelden. + +```bash +tar -C /docker/sultan-server -czf sultan-backup-$(date +%F).tgz suldata # verschlüsselt ablegen! +``` + +--- + +## 3. Agent (Docker-Host) + +Vorlage: [deploy/compose/agent](../deploy/compose/agent). + +### 3.1 Warum `network_mode: host` + +Der Agent läuft **immer** mit `network_mode: host`. Ein Traefik-Netz braucht er nicht, denn er spricht nur ausgehend mit dem Server. Das host-Netz brauchen wir aus zwei Gründen: + +- Er meldet als Backend-Adresse die **IP des Hosts**, über die er den Server erreicht. In einem Bridge-Netz wäre das die nutzlose Container-IP `172.x`. +- Die Knoten-ID kommt aus dem **Hostnamen** des Hosts statt aus der zufälligen Container-ID. + +### 3.2 Einrichten + +```bash +mkdir -p /docker/sultan-agent && cd /docker/sultan-agent +cp /deploy/compose/agent/{compose.yaml,.env.example} . +cp .env.example .env && $EDITOR .env +install -d -m 0700 -o 65532 -g 65532 suldata +``` + +`.env`: + +```bash +SULTAN_IMAGE=zot.chmln.de/sultan:v0.1.1 +SULTAN_TOKEN=sltn_enroll_… # Einladung aus 2.6; nach dem ersten Start egal +DOCKER_GID=999 # getent group docker | cut -d: -f3 +# SULTAN_NODE_ID=dockweb11 # optional, sonst aus dem Hostnamen +# SULTAN_BACKEND_INTERFACE=wt0 # optional: Traefik erreicht den Host über NetBird +# SULTAN_BACKEND_HOST=192.168.1.20 # optional: Adresse fest vorgeben +``` + +```bash +docker compose up -d +docker compose logs -f sultan-agent +# Registriert node=… tenant=… +# Agent gestartet … backend= ← diese IP muss Traefik erreichen können! +# Mit Server verbunden / Lease erhalten +``` + +Danach `suldata/` behalten. Darin liegt der Schlüssel des Knotens (`node.key`); er verlässt den Host nie. Wer `suldata/` löscht, braucht eine neue Einladung. + +### 3.3 Dienste freigeben: Labels am Container + +Es sind dieselben Labels wie bei Traefiks Docker-Provider: + +```yaml +services: + web: + image: nginx + ports: + - "9080:80" # muss veröffentlicht sein – nicht nur 127.0.0.1:9080:80 + labels: + - "traefik.enable=true" + - "traefik.http.routers.meinweb.rule=Host(`www.example.de`) || Host(`example.de`)" + - "traefik.http.routers.meinweb.entrypoints=websecure" + - "traefik.http.routers.meinweb.tls.certresolver=le" + - "traefik.http.services.meinweb.loadbalancer.server.port=80" # Port IM Container +``` + +- Der Agent rechnet den Container-Port `80` selbst auf den veröffentlichten Port `9080` um. +- Fehlt das Port-Label und ist genau ein Port veröffentlicht, nimmt er diesen. +- Änderungen (Start, Stop, neue Labels) kommen innerhalb einer Sekunde bei Traefik an; Neustarts sind nicht nötig. +- Nicht übernommene Container stehen mit Grund im Agent-Log („Container nicht übernommen“). Fremde Domains landen im Server-Log („Policy-Verstoß“). + +### 3.4 Erreichbarkeit Traefik → Backend + +Der Agent meldet `http://:`. Prüfen vom **Ingress** aus: + +```bash +curl -sI http://:9080 -H 'Host: www.example.de' +``` + +Klappt das nicht: Firewall des Backend-Hosts (Port nur für die Ingress-IP freigeben), falsche Adresse (dann `SULTAN_BACKEND_HOST`/`SULTAN_BACKEND_INTERFACE`) oder IPv6. Der Agent nimmt die Adresse, über die er den Server erreicht, das kann eine IPv6-Adresse sein. Dann muss auch Traefik per IPv6 dorthin kommen. + +### 3.5 Ohne Docker (`/etc/sultan.d`) + +Für Bare-Metal-Dienste beschreibt man die Dienste als YAML in `/etc/sultan.d/*.yaml` (Format siehe README). Im Compose dafür `--config-dir=none` aus `command` entfernen und `/etc/sultan.d:/etc/sultan.d:ro` mounten. + +Als systemd-Dienst ohne Docker ist der Agent vorgesehen, aber **noch nicht getestet**. + +--- + +## 4. Kontrolle + +```bash +# Server: +docker compose exec sultan-server sultan token node list # online, Lease bis … +curl -s 'http://127.0.0.1:8080/v1/traefik-config?format=yaml' # was Traefik bekommt +curl -s http://127.0.0.1:8080/metrics/checkmk # Monitoring +# von außen: +curl -sI https://www.example.de +``` + +Typische Fehler: + +| Symptom | Ursache | +|---|---| +| Route fehlt in `traefik-config` | Container nicht `traefik.enable=true`, Port nicht veröffentlicht, Domain nicht in `--domains` (Server-Log „Policy-Verstoß“) | +| Route da, Traefik meldet 502/504 | Traefik erreicht `:` nicht (Firewall, IPv6) | +| Kein Zertifikat | DNS zeigt nicht auf den Ingress, Certresolver-Name falsch | +| 404 statt Wartungsseite | Container wurde *entfernt* statt gestoppt, siehe [Wartung](wartung.md) | + +Wartung, Neustarts und Updates: [docs/wartung.md](wartung.md). diff --git a/docs/hoster.md b/docs/hoster.md new file mode 100644 index 0000000..f999263 --- /dev/null +++ b/docs/hoster.md @@ -0,0 +1,166 @@ +# Sultan für Hoster: Kunden-Onboarding (pro / enterprise) + +Für Hoster, die Sultan als bezahlten Dienst anbieten: Du betreibst Ingress (Traefik + `sultan server`), der Kunde betreibt seine Backends mit `sultan agent`. Grundinstallation: siehe [community.md](community.md). Wartung: siehe [wartung.md](wartung.md). + +> **Stand:** Der kommerzielle Teil (Lizenzen, Mahnwesen, Vertragsende) ist durch Tests abgedeckt, im echten Betrieb aber **noch nicht erprobt**. Bekannte Lücken stehen am Ende. + +## Wer hat was? + +| | Hoster | Kunde | +|---|---|---| +| Läuft dort | Traefik, `sultan server` | Docker-Host(s) mit `sultan agent` | +| Geheimnisse | `suldata/` des Servers: TLS-Schlüssel, **Master-Key** (signiert Lizenzen, Leases, Befehle) | `node.key` je Host; verlässt den Host nie, auch du siehst ihn nicht | +| Entscheidet | welche Domains, wie viele Routen, bis wann | welche Dienste wie geroutet werden (Labels) | +| Sieht | alle Routen, Knoten, Verstöße, Fristen | nur sein eigenes Agent-Log | + +Der Kunde kann **nur Domains** aus seinem Mandanten beanspruchen und nur bis `max_routes`. Alles andere verwirft der Server mit Eintrag im Audit-Log. Kunden sehen sich gegenseitig nicht. Gleichnamige Router verschiedener Kunden kollidieren nicht, weil jeder Router in Traefik als `____` erscheint. + +## 1. Vor dem Onboarding klären + +Aus dem Vertrag gehen diese Werte in den Mandanten: + +| Wert | Flag | Hinweise | +|---|---|---| +| Kennung | `--id` | `a–z 0–9 - _`, dauerhaft (erscheint in Routernamen, Logs, Monitoring). Z. B. Kundennummer. | +| Name, Kontakt | `--name`, `--email` | für dich (Listen, Benachrichtigungen) | +| Domains | `--domains` | `kunde.de` = genau diese; `*.kunde.de` = alle Subdomains, **ohne** `kunde.de` selbst → meist beides eintragen. Wildcard auf TLD (`*.de`) ist verboten. Umlaut-Domains als Punycode (`xn--…`). | +| Paket | `--tier pro` oder `enterprise` | Beide verhalten sich technisch derzeit **gleich** (Mahnwesen, Vertragsende). Der Unterschied ist vertraglich. `community` = kein Mahnwesen, läuft nicht ab. | +| Routen | `--max-routes` | Anzahl Router (jeder `traefik.http.routers.` zählt, auch Redirect-Router). `-1` = unbegrenzt. Überzählige werden alphabetisch verworfen. | +| Vertragsende | `--valid-until JJJJ-MM-TT` | Default: heute + 1 Jahr. **Danach sehen Besucher die Landingpage „Hosting abgelaufen“.** | +| Zusatzfunktionen | `--features` | `custom_middleware`, `websockets`, `tcp_stream`. Ohne `custom_middleware` sind nur Standard-Middlewares erlaubt (Redirects, Pfad-Umschreibung, Header, Kompression). BasicAuth, Rate-Limit, IP-Allowlist usw. brauchen das Feature, sonst wird der Router mit Verstoß abgelehnt. `chain` und `errors` sind nie erlaubt. | + +**Achtung:** Vertragsende, Domains und Paket lassen sich bisher **nicht nachträglich ändern** (`tenant update` fehlt, siehe Lücken). Deshalb `--valid-until` und `--domains` beim Anlegen sorgfältig setzen. + +## 2. Netzwerk mit dem Kunden klären + +Das ist der häufigste Grund für ein holpriges Onboarding. Zwei Wege müssen frei sein: + +1. **Agent → Server:** ausgehend TCP 8443 vom Kunden-Host zu deiner `--public-url`. Meist ohnehin offen. +2. **Traefik → Kunden-Backend:** dein Ingress verbindet sich per HTTP auf den **veröffentlichten Port** des Containers. Dafür gibt es drei Möglichkeiten: + +| Situation beim Kunden | Lösung | +|---|---| +| öffentliche IP / eigenes Rechenzentrum | Kunde gibt die Ports **nur für deine Ingress-IP(s)** frei | +| hinter NAT/CGNAT/DS-Lite | VPN/Overlay (z. B. NetBird): Agent mit `SULTAN_BACKEND_INTERFACE=wt0`; eventuell Einladung mit `--server-url https://:8443` | +| gleiches LAN wie der Ingress | nichts zu tun | + +Vorgaben für den Kunden: +- Ports veröffentlichen (`"9080:80"`), **nicht** nur `127.0.0.1:9080:80`. +- Der Agent meldet als Backend die Adresse, über die er deinen Server erreicht. Das kann eine **IPv6-Adresse** sein. Dann muss dein Ingress IPv6 können, sonst setzt der Kunde `SULTAN_BACKEND_HOST`. +- Knoten-ID: Default ist der Hostname. Gleichnamige Hosts beim selben Kunden (z. B. `web01`) brauchen `SULTAN_NODE_ID`. + +## 3. Mandant anlegen und einladen + +```bash +cd /docker/sultan-server +docker compose exec sultan-server sultan token tenant add \ + --id k10042 --name "Novesia Bonn" --email it@novesia-bonn.de \ + --domains "novesia-bonn.de,www.novesia-bonn.de" \ + --tier pro --max-routes 10 --valid-until 2027-10-31 + +docker compose exec sultan-server sultan token invite --tenant k10042 --ttl 7d +``` + +**Die Einladung (`sltn_enroll_…`):** +- **Ein Knoten pro Einladung.** Drei Hosts = drei Einladungen. +- Bis zur Benutzung geheim: Wer sie hat, kann einen Knoten für diesen Kunden anmelden. Danach ist sie wertlos. Übergabe über einen sicheren Kanal (Kundenportal, verschlüsselte Mail, Passwortmanager), **nicht** ins Ticket kopieren. +- Gespeichert wird nur ein Hash; du kannst sie später also nicht noch einmal anzeigen. Verloren oder abgelaufen → einfach neu erzeugen. Aufräumen: `sultan token purge`. +- Sie enthält Server-Adresse und Zertifikats-Fingerprint. Der Kunde braucht nichts weiter, kein CA-Zertifikat, keine IP-Liste. Mit einer gefälschten Adresse landet der Agent nie bei einem fremden Server (Pinning). + +## 4. Was du dem Kunden schickst + +Ein Paket aus: + +1. Einladung (sicherer Kanal) und Gültigkeit. +2. Compose-Vorlage [deploy/compose/agent](../deploy/compose/agent) + Image `zot.chmln.de/sultan:`. +3. Die Labels, passend zu **deinen** Traefik-Namen (Entrypoint, Certresolver): + ```yaml + labels: + - "traefik.enable=true" + - "traefik.http.routers.shop.rule=Host(`www.novesia-bonn.de`) || Host(`novesia-bonn.de`)" + - "traefik.http.routers.shop.entrypoints=websecure" + - "traefik.http.routers.shop.tls.certresolver=le" + - "traefik.http.services.shop.loadbalancer.server.port=80" + ``` +4. DNS-Vorgabe: Domains per CNAME auf deinen Ingress (z. B. `web11.r25dc.de`). Erst dann klappt Let's Encrypt. +5. Freigabe aus Abschnitt 2 (Ports nur für deine Ingress-IP). +6. Die Wartungsregeln aus [wartung.md](wartung.md), vor allem: **`stop` statt `down`** und `sultan agent maintenance on`. + +## 5. Abnahme + +```bash +docker compose exec sultan-server sultan token node list --tenant k10042 # online, Lease bis … +curl -s 'http://127.0.0.1:8080/v1/traefik-config?format=yaml' | grep k10042 # Router + Backend-URL +curl -sI http://: # vom Ingress: Backend erreichbar? +curl -sI https://www.novesia-bonn.de # Zertifikat + Seite +docker compose logs sultan-server | grep -i verstoß # abgelehnte Domains/Routen? +``` + +Fehlt eine Route, steht der Grund im Server-Log („Policy-Verstoß“: fremde Domain, `HostRegexp`, Routenlimit, verbotene Middleware) oder im Agent-Log des Kunden („Container nicht übernommen“: Port nicht veröffentlicht). + +## 6. Laufender Betrieb + +### Zahlung und Mahnwesen + +Sultan kennt keine Rechnungen, nur das **Fälligkeitsdatum einer offenen Zahlung**. Das trägst du (oder später dein Rechnungssystem) ein: + +```bash +docker compose exec sultan-server sultan token tenant payment --id k10042 --overdue-since 2026-11-01 +docker compose exec sultan-server sultan token tenant payment --id k10042 --paid +``` + +Daraus ergibt sich automatisch: + +| Tage überfällig | Stufe | Besucher sehen | +|---|---|---| +| < 8 | – | normale Seite | +| ab 8 | watermark | Hinweisseite, nach 5 s weiter zur Seite (dann 60 min Ruhe) | +| ab 25 | firemark | wie oben, 10 s | +| ab 32 | paywall | **Seite offline**, Hinweisseite mit deinem Angebot (`--hoster-name`, `--hoster-url`) | + +Mit `--paid` ist sofort (nächster Refresh, ≤ 1 min) alles normal. Bei Stufe 1 und 2 meldet Checkmk bzw. Slack/ntfy. + +Sofort sperren, z. B. bei Zahlungsausfall oder Missbrauch: + +```bash +sultan token tenant set-status --id k10042 --status suspended_payment # Paywall-Seite +sultan token tenant set-status --id k10042 --status quarantined # Routen weg, Agent-Anfragen abgelehnt +sultan token tenant set-status --id k10042 --status active # zurück +``` + +### Vertragsende + +- Checkmk (`/metrics/checkmk`): **WARN 30**, **CRIT 14** Tage vor `valid_until`. Benachrichtigungen gehen einmal pro Frist raus. +- Ab `valid_until` wird der Mandant `expired`, Besucher sehen „Hosting abgelaufen“ (402). Die Leases enden spätestens zum Vertragsende. +- Verlängern: siehe Lücken. + +### Monitoring + +```bash +curl -s http://127.0.0.1:8080/metrics/checkmk # als Checkmk-Local-Check einbinden +``` + +Liefert pro kommerziellem Mandanten Vertragsfrist und Mahnstufe, pro Knoten online/offline/Wartung und Lease, dazu Policy-Verstöße der letzten 24 h. Zusätzlich Push per `SULTAN_NOTIFY_SLACK` / `SULTAN_NOTIFY_NTFY`: Knoten > 15 min offline, wieder online, Fristen, Mahnstufen. Policy-Verstöße erscheinen nur im Server-Log und als Zähler in Checkmk. + +### Sicherheit + +- Verdacht auf kompromittierten Kunden-Host: `sultan token node drain --id …` (Routen sofort weg), danach `node delete` und neue Einladung für einen sauberen Host. +- Schlüssel des Knotens tauschen, ohne Neuanmeldung: `sultan token node command --id … --cmd rotate_key`. +- Dein `suldata/` (Master-Key!) regelmäßig verschlüsselt sichern. Wer den Master-Key hat, kann Lizenzen und Befehle für alle Kunden ausstellen. + +### Kunde kündigt + +```bash +sultan token node drain --id # Routen sofort weg (für jeden Knoten) +sultan token tenant delete --id k10042 # löscht Mandant, Knoten und offene Einladungen +``` + +## Bekannte Lücken (Stand heute) + +| Lücke | Folge | Notlösung | +|---|---|---| +| **`tenant update` fehlt** | Vertragsende, Domains, Paket, Routenlimit nicht änderbar | Keine saubere: `tenant delete` + neu anlegen löscht auch die Knoten (neue Einladungen nötig). Erste Priorität für den kommerziellen Betrieb. | +| pro = enterprise | kein technischer Unterschied | vertraglich regeln | +| Keine Admin-API | Zahlungsstatus nur per CLI | Skript über `docker compose exec` | +| Kommerzieller Betrieb nicht im Feld getestet | – | mit einem Testmandanten und kurzen Fristen (`--valid-until` in wenigen Tagen, `payment --overdue-since` in der Vergangenheit) durchspielen | +| `public_reason` einsprachig | Wartungstext für de und en gleich | – | diff --git a/docs/wartung.md b/docs/wartung.md new file mode 100644 index 0000000..b1beca2 --- /dev/null +++ b/docs/wartung.md @@ -0,0 +1,145 @@ +# Wartung, drain und Container-Lebenszyklus + +Was Besucher sehen, wenn an einem Backend gearbeitet wird, und welcher Befehl wofür gedacht ist. + +## Das Grundprinzip + +Der Agent meldet dem Server immer den **vollständigen** Stand seines Hosts. Wie ein Dienst dabei erscheint, hängt davon ab, ob sein Container noch existiert: + +| Zustand des Containers | Was der Agent meldet | Besucher sehen | +|---|---|---| +| läuft | Dienst normal | die Seite | +| **gestoppt** (`docker stop`, `docker compose stop`, Absturz, `restarting`, `paused`) | Dienst vorhanden, aber *down* | **Wartungsseite** (503, „Gleich wieder da“) | +| **entfernt** (`docker rm`, `docker compose down`) | Dienst gibt es nicht mehr | **404**, die Route ist weg | +| Host/Agent weg (Reboot, Netzausfall) | nichts – Verbindung getrennt | **Wartungsseite**, solange die Lease gilt (Community 14 Tage, kommerziell 7) | + +**Das ist Absicht:** Ein verteilter Dienst bestimmt sein Routing selbst. `docker compose down` heißt „diesen Dienst gibt es hier nicht mehr“, also verschwindet seine Route, so wie bei Traefiks Docker-Provider. `stop` heißt „kommt wieder“, also bleibt die Route und zeigt die Wartungsseite. + +Merkregel: **Für Wartung `stop`, nie `down`.** + +Auch eine angemeldete Wartung (siehe unten) rettet einen *entfernten* Container nicht. Sie gilt für die Routen, die der Server vom Knoten kennt, und ein entfernter Container meldet keine mehr. + +## Die Werkzeuge + +| | **maintenance** – „bin gleich wieder da“ | **drain** – „gebe meine Arbeit ab“ | +|---|---|---| +| Routen bei Traefik | bleiben, zeigen die Wartungsseite (503) | werden entfernt (404, sofern kein anderer Knoten die Domain bedient) | +| Dauer | befristet (Default 4 h, `--for`), endet von selbst | bis `resume` | +| Anlass | Patchen, Reboot, Migration | Abbau, Umzug | +| Wer darf | Admin am Server **oder** der Knoten selbst | nur Admin | +| Geltungsbereich | ganzer Knoten **oder** einzelner Dienst | ganzer Knoten | + +Eine Knoten-Wartung gilt für **alle** Dienste, die der Agent dieses Hosts meldet, also für alle Container mit `traefik.enable=true` und alle Dienste aus `/etc/sultan.d`. + +### Knoten-Wartung am Server (Admin) + +```bash +cd /docker/sultan-server +docker compose exec sultan-server sultan token node maintenance --id dockweb11 \ + --for 2h --reason "Kernel-Update" --public-reason "Geplante Wartung, gleich wieder da" +docker compose exec sultan-server sultan token node resume --id dockweb11 +``` + +- `--for`: Dauer, z. B. `30m`, `2h`, `1d`; `0` = unbefristet. Das Ende steht auf der Wartungsseite und im `Retry-After`. +- `--reason`: intern (Log, `node list`). +- `--public-reason`: ersetzt den Standardtext der Wartungsseite (max. 300 Zeichen; noch nicht getrennt nach Sprache). +- `node resume` beendet Wartung **und** drain. + +**Verzögerung:** Der Server wertet einmal pro Minute neu aus (`--refresh`). Wartung und `resume` greifen also nach **bis zu einer Minute**. Vor Beginn der Arbeiten prüfen, ob die Wartung aktiv ist: + +```bash +docker compose exec sultan-server sultan token node list # Spalte Wartung: "bis …" +curl -sI https://www.example.de # HTTP 503 +``` + +### Knoten-Wartung vom Backend aus (Kunde/Betreiber des Hosts) + +Ohne Zugriff auf den Server, direkt auf dem Docker-Host: + +```bash +cd /docker/sultan-agent +docker compose exec sultan-agent sultan agent maintenance on --for 2h --public-reason "Update läuft" +docker compose exec sultan-agent sultan agent maintenance off +``` + +Die Anfrage ist mit dem Schlüssel des Knotens signiert. Ein Knoten kann so nur sich selbst in Wartung setzen. + +### Wartung für einen einzelnen Dienst + +Per Label am Container. Die anderen Dienste des Hosts laufen normal weiter: + +```yaml + labels: + - "sultan.maintenance=true" + - "sultan.maintenance.public_reason=Wir ziehen um – heute Abend wieder da" +``` + +```bash +docker compose up -d # Labels ändern = Container neu erzeugen; die Wartungsseite greift sofort +``` + +Zurück: Labels entfernen (oder `=false`), dann erneut `docker compose up -d`. + +Für `/etc/sultan.d`-Dienste: `maintenance: true` bzw. `maintenance_public_reason` in der YAML-Datei. + +### drain (Admin) + +```bash +docker compose exec sultan-server sultan token node drain --id dockweb11 +docker compose exec sultan-server sultan token node resume --id dockweb11 +``` + +Die Routen verschwinden **sofort** (404). Sinnvoll vor dem Abbau eines Hosts oder wenn ein anderer Knoten die Domains übernimmt. Für eine Seite mit nur einem Backend ist **maintenance** die richtige Wahl. + +## Abläufe + +### Container aktualisieren (neues Image) + +```bash +docker compose pull +docker compose up -d +``` + +`up -d` erzeugt den Container neu: alt entfernen, neu starten. Dazwischen kann für ein bis zwei Sekunden 404 kommen. Wer das nicht will, setzt vorher eine Wartung für den Knoten oder den Dienst und beendet sie danach. + +### Host patchen und neu starten + +```bash +# 1. Server: Wartung setzen und abwarten, bis sie aktiv ist (≤ 1 min) +docker compose exec sultan-server sultan token node maintenance --id dockweb11 --for 1h \ + --public-reason "Geplante Wartung, gleich wieder da" +curl -sI https://www.example.de # 503? + +# 2. Backend-Host +cd /docker/app && docker compose pull # Images vorab holen +docker compose stop # NICHT down +apt upgrade && reboot + +# 3. nach dem Reboot +cd /docker/app && docker compose up -d # gestoppte Dienste starten nach dem Reboot nicht von selbst +docker compose -f /docker/sultan-agent/compose.yaml logs --tail 5 # "Mit Server verbunden" +curl -sI http://127.0.0.1:9080 # Dienst lokal ok? + +# 4. Server: Wartung beenden (greift nach ≤ 1 min) +docker compose exec sultan-server sultan token node resume --id dockweb11 +``` + +Den Agent nie mit `docker compose stop` anhalten: `restart: unless-stopped` startet ihn sonst nach dem Reboot nicht wieder. Ein Reboot ohne vorheriges `stop` ist für den Agent in Ordnung. + +### Ungeplanter Ausfall + +Nichts zu tun. Stürzt ein Container ab, zeigt Sultan die Wartungsseite, bis er wieder läuft. Ist der ganze Host weg, gilt das, bis die Lease abläuft. Ist ein Knoten länger als 15 Minuten getrennt (`--offline-alert-after`), alarmiert der Server über Slack/ntfy, sofern konfiguriert, und meldet sich wieder, sobald der Knoten zurück ist. + +### Host abbauen + +```bash +docker compose exec sultan-server sultan token node drain --id dockweb11 # Routen sofort weg +# Agent und Dienste auf dem Host stoppen, suldata/ löschen +docker compose exec sultan-server sultan token node delete --id dockweb11 +``` + +## Grenzen (Stand heute) + +- `public_reason` ist ein Text für beide Sprachen der Wartungsseite (zweisprachig geplant). +- Wartung/`resume` per CLI greifen erst beim nächsten Refresh (≤ 1 min). +- Echte Lastverteilung einer Domain über mehrere Knoten gibt es noch nicht. drain verschiebt also keine Last, sondern nimmt die Seite vom Netz.