# Sultan: High-Performance Distributed Ingress & ZeroTrust Service Discovery Dieses Dokument beschreibt das vollständige Architektur-, Daten-, Netzwerk- und Anwendungsmodell des Projekts **Sultan**. Es dient als Kontext und Referenz für Software-Architekten und AI-gestützte Coding-Agenten (z. B. via Continue in VSCodium). --- ## 1. Executive Summary & Problemstellung Klassische Ingress-Lösungen wie Nginx Proxy Manager (NPM) erfordern eine manuelle, GUI-basierte Konfiguration des Routings. In verteilten Umgebungen (Mehrere Proxmox-Cluster, Docker-Hosts, Bare-Metal-LAMP/Apache-Server, Kunden-Setups hinter DS-Lite/CGNAT) führt dies zu Dokumentationsbrüchen, toten Routen und hohem Wartungsaufwand. **Sultan** löst dies durch eine entkoppelte, zweigeteilte Architektur: 1. **In-Service-Deklaration:** Backends deklarieren ihr Routing direkt am Dienst (native Traefik-Labels in Docker oder Drop-in-Konfigurationen unter `/etc/sultan.d/`). 2. **Dynamische Registrierung:** Ein leichtgewichtiger Agent überträgt diese Konfiguration kryptografisch gesichert über einen langlebigen, ressourcenschonenden Keepalive-Stream an den zentralen Ingress. 3. **ZeroTrust & Public Reachability:** Optionale Anbindung über ein WireGuard-Mesh (z. B. NetBird), um auch Hosts ohne statische IPv4/IPv6 oder hinter NAT/Firewalls öffentlich mit TLS bereitzustellen. 4. **Integrierte Autorisierung & Monetarisierung:** Mandantensichere Capability-Lizenzen via Ed25519-Kryptografie, Lease-System und automatisierte Überwachung von Lizenzfristen. --- ## 2. Netzwerkmodell (Network Model) ### 2.1 Topologie ```text [ Öffentliches Internet ] │ │ HTTP (80) / HTTPS (443) / TCP-Streams ▼ ┌────────────────────────────────────────────────────────┐ │ Ingress-Knoten (Zentrales Rechenzentrum / Public IP) │ │ │ │ ┌────────────────────────────────────────────────┐ │ │ │ Traefik v3 (Ingress / ACME DNS-01 Validierung) │ │ │ └───────────────────────▲────────────────────────┘ │ │ │ HTTP Poll (:8080) │ │ │ (/v1/traefik-config) │ │ ┌───────────────────────┴────────────────────────┐ │ │ │ Sultan-Server (Registry / Policy Engine) │ │ │ └───────────────────────▲────────────────────────┘ │ │ │ │ │ │ Control Plane: │ │ │ TLS-Stream (:8443) │ │ │ (45s Heartbeat, │ │ │ Instant Event Push) │ │ │ │ │ ┌───────────────────────┴────────────────────────┐ │ │ │ NetBird / WireGuard Gateway (100.64.0.1) │ │ │ └───────────────────────▲────────────────────────┘ │ └───────────────────────────┼────────────────────────────┘ │ ZeroTrust WireGuard Tunnel (Traffic) │ ┌──────────────────┴──────────────────┐ │ │ ┌────────┴──────────────┐ ┌──────────┴─────────────┐ │ Client-Host A │ │ Client-Host B │ │ (Bare-Metal / LAMP) │ │ (Docker-Compose Stack) │ │ │ │ │ │ ┌───────────────────┐ │ │ ┌────────────────────┐ │ │ │ sultan-agent │ │ │ │ sultan-agent │ │ │ │ (File-Watcher) │ │ │ │ (Docker-Socket) │ │ │ └───────────────────┘ │ │ └────────────────────┘ │ │ ┌───────────────────┐ │ │ ┌────────────────────┐ │ │ │ Apache / VHost │ │ │ │ Web-Container │ │ │ └───────────────────┘ │ │ └────────────────────┘ │ └───────────────────────┘ └────────────────────────┘ ``` ### 2.2 Datenfluss & Verbindungstypen 1. **Control Plane (Agent -> Server):** * **Transport:** Ausgehende Verbindung vom Agenten zum Server über TCP `:8443` (TLS). * **Firewall-Freundlichkeit:** Reine Outbound-Verbindung; funktioniert hinter DS-Lite, Mobilfunk, Carrier-Grade-NAT und Unternehmens-Firewalls ohne Portweiterleitung. * **Keepalive-Mechanik:** Persistenter **SSE-Stream** (Server-Sent Events, `text/event-stream`) vom Server zum Agenten. Alle 45 Sekunden ohne Event wird die SSE-Kommentarzeile `:\n\n` gesendet. Verhindert das Schließen von State-Tables in NAT-Routern (Conntrack Timeout). Der Agent gilt als getrennt, wenn 90 s lang nichts ankommt (auch kein Keepalive), und verbindet sich mit exponentiellem Backoff (1 s … 60 s) neu. * **Rückkanal (Agent -> Server):** Nicht über den Stream, sondern als eigene, signierte HTTPS-Requests (z. B. `service_state`). * **Event-Driven Push:** Änderungen an Containern (Start/Stop) oder Notfall-Sperren werden verzögerungsfrei (Latenz < 5 ms) über den bestehenden Stream übertragen. 2. **Data Plane (Traffic: Besucher -> Backend):** * **Direkt:** Liegen Ingress und Backend im selben VLAN/LAN, routet Traefik direkt auf die interne LAN-IP. * **ZeroTrust Overlay (NetBird):** Sitzt das Backend im Fremdnetz/Homelab, routet Traefik auf die WireGuard-Overlay-IP (`100.64.x.y`). End-to-End verschlüsselt. 3. **Ingress zu Traefik:** * Traefik pollt den lokalen Sultan-Server via HTTP-Provider: `GET http://127.0.0.1:8080/v1/traefik-config`. * Intervall: 2–5 Sekunden. Traefik verarbeitet die In-Memory-Konfiguration ohne Neustart und ohne Verbindungsabbrüche. * **Pull, nicht Push:** Traefik holt die Konfiguration ab; Sultan schreibt nie in Traefik und emuliert keinen Docker-Socket. Freigeschaltet wird das einmalig in Traefiks statischer Konfiguration (`--providers.http.endpoint`). Router aus Sultan heißen in Traefik `@http`; Mischbetrieb mit dem Docker-Provider für lokale Container ist möglich (Querverweise mit Provider-Suffix, z. B. `auth@docker`). * Port 8080 ist Klartext und deshalb nur an Loopback bzw. ein internes Docker-Netz zu binden. Achtung: Traefiks eigenes API/Dashboard nutzt per Default ebenfalls 8080. --- ## 3. Sicherheits- und Vertrauensmodell ### 3.1 Transport-Verschlüsselung (TLS ohne CA-Zirkus) * **Grundsatz: Kein Agent-Verkehr im Klartext.** Durchgesetzt auf mehreren Ebenen: * Der Agent-Port `:8443` wird ausschließlich per TLS bedient, **nur TLS 1.3**. * Der Agent signiert nur Requests an `https`-URLs; an eine `http`-URL geht weder Signatur noch Body. * Der Server authentifiziert nur Requests, die über TLS angekommen sind (sonst `426 Upgrade Required`). * **Server-Zertifikat:** Wird beim ersten Start selbst erzeugt (selbstsigniert, ECDSA P-256, **keine CA**) und liegt dauerhaft unter `/server.key` (0600) und `server.crt`. Gültigkeit 20 Jahre, weil Agents den Fingerprint pinnen. Fehlt nur eine der beiden Dateien, startet der Server nicht, statt still ein neues Zertifikat zu erzeugen (das würde alle Pins brechen). * **Fingerprint:** SHA-256 über das DER-Zertifikat, Base64url ohne Padding (43 Zeichen). * **Vertrauensmodell: Pinning mit geprüftem Erstkontakt** (statt reinem TOFU, siehe 3.4): * Der Agent speichert den Fingerprint je Server (`host:port`) in `known_servers` (Format wie SSH `known_hosts`: ` `, 0600; Einträge werden nur angehängt, nie still geändert). * **Erstkontakt:** Der Fingerprint muss zu dem im Enrollment-Token eingebetteten Wert passen (3.2), erst dann wird gepinnt. * **Folgekontakte:** Das Zertifikat muss zum Pin passen. Der Pin gewinnt immer, auch gegen einen später gelieferten Token-Fingerprint. * **Ohne Pin und ohne Token-Fingerprint** wird nicht verbunden (kein blindes TOFU). * Die Prüfung erfolgt im TLS-Handshake, also bevor Token, Signatur oder Body gesendet werden. * **Optional (geplant):** Unterstützung für explizit gemountete Root-CA-Zertifikate. ### 3.2 Authentifizierung & Enrollment (CSR-Handshake) * Private Keys werden **ausschließlich lokal auf dem Ziel-Host** generiert (Ed25519) und verlassen diesen niemals. `node.key` liegt als PKCS#8-PEM mit Rechten 0600 vor, wird nie überschrieben und bei offeneren Rechten nicht geladen. * **Authentifizierung ist in jedem Tier Pflicht** – auch im `community`-/Homelab-Betrieb gibt es keinen Zugang ohne Ed25519-Signatur und keinen Schalter, der das abstellt. * **Enrollment-Token:** `sltn_enroll_.` * `secret`: 256 Bit Zufall (Base64url). Der Server speichert nur den SHA-256 von `sltn_enroll_`, einmalig verwendbar, mit Ablaufzeit. * `fingerprint`: Fingerprint des Server-Zertifikats (3.1). Nicht geheim; er sichert den Erstkontakt des Agenten ab. * Der Admin überträgt den Token auf einem separaten Weg (z. B. Copy-Paste aus dem Server-Terminal) an den Agenten. * **Enrollment:** 1. Admin erzeugt den Einladungs-Token auf dem Server (`sultan token`). 2. Agent zerlegt den Token, verbindet sich per TLS und prüft dabei den Server-Fingerprint gegen den Token (3.1). Erst danach pinnt er den Server. 3. Agent generiert lokalen Key (`node.key`) und sendet `sltn_enroll_` an den Server (`POST /api/v1/enroll`). Der Request ist bereits mit dem neuen Schlüssel signiert (Besitznachweis); der Public Key steht im Header `X-Sultan-Key`. 4. Server prüft die Signatur, löst den Token ein und legt den Knoten **in einer Transaktion** an. Der Mandant ergibt sich ausschließlich aus dem Token. Schlägt das Anlegen fehl, bleibt der Token gültig. Paralleles Einlösen desselben Tokens gelingt genau einmal. 5. *(Geplant)* Server signiert das initiale Berechtigungszertifikat (Capability Lease). 6. Alle künftigen Requests werden vom Agenten mit `node.key` signiert. * **Request-Signatur** (jeder Agent-Request inkl. Stream-Aufbau): * Header: `X-Sultan-Key` (Public Key, Base64), `X-Sultan-Timestamp` (Unix-Sekunden), `X-Sultan-Nonce` (16–64 Zeichen, pro Request neu), `X-Sultan-Signature` (Base64). * Signiert wird (zeilengetrennt): `SULTAN-ED25519-V1`, Methode, Pfad+Query, Timestamp, Nonce, SHA-256-Hex des Bodys. * **Server-Prüfreihenfolge:** 1. TLS 2. Signatur (ohne Datenbankzugriff) 3. Zeitfenster ±5 Minuten 4. Nonce (Replay-Schutz) 5. erst dann Nachschlagen des Knotens per Public Key Gefälschte Requests kosten so keinen DB-Zugriff und können keine Nonces verbrauchen. * Die **Knoten-ID** wird ausschließlich aus dem geprüften Schlüssel abgeleitet, nie aus einer Angabe des Clients. * Body-Limit 1 MiB. * Fehler-Mapping: 401 (nicht authentifiziert, Replay, Zeitabweichung), 403 (gesperrt), 413 (Body zu groß), 426 (kein TLS); Infrastrukturfehler 500. * **Sperren:** Die Authentifizierung sperrt nur Mandanten im Status `quarantined`. Zahlungsverzug, Ablauf und gedrainte Knoten regelt die Policy-Engine über das Routing (5.2). Der Agent behält den Kanal, z. B. für `lease_renew` oder eine Reaktivierung. Für Entzug zur Laufzeit trennt der Server bestehende Streams aktiv (`Hub.Disconnect`). ### 3.3 Autorisierung & Domain-Ownership-Schutz Jeder Payload vom Agenten an den Server muss vom Server gegen die zugewiesene Policy geprüft werden: * **Domain Spoofing Protection:** Beansprucht ein Agent ein Label `Host('kunde.de')`, prüft der Server, ob `kunde.de` im Feld `allowed_domains` der Mandanten-Lizenz hinterlegt ist. Falls nicht: Ablehnung mit Sicherheits-Audit-Log. * **Route Limits:** Übersteigt die Anzahl der deklarierten Routen das Limit (`max_routes`), werden überzählige Routen verworfen (deterministisch: nach Router-Namen sortiert, die hinteren fallen weg). Umsetzung in `pkg/policy`. Jeder Verstoß entfernt nur den betroffenen Router und wird als Violation (Audit-Log) gemeldet: * **Regel-Analyse statt Textsuche:** Traefik-Regeln werden geparst (Matcher, `&&`, `||`, `!`, Klammern). * Jeder `||`-Zweig muss durch `Host(...)` eingeschränkt sein. ``Host(`kunde.de`) || PathPrefix(`/`)`` würde sonst den Verkehr **aller** Mandanten abgreifen. * Negierte Hosts schränken nicht ein. * `HostRegexp` ist nicht erlaubt, weil sich ein Regex nicht gegen `allowed_domains` prüfen lässt. * **Namensraum:** Router, Services und Middlewares heißen in der ausgelieferten Konfiguration `____`. * Mandanten können sich damit weder überschreiben noch gegenseitig referenzieren. * IDs: alphanumerisch mit `-`/`_`, nicht am Rand, kein `__`. * Das Präfix `sultan__` und die Mandanten-ID `sultan` sind reserviert. * **Fremde Provider:** Referenzen mit `@` (z. B. `api@internal` = Traefik-Dashboard) sind verboten, außer sie stehen in einer Freigabeliste des Admins (z. B. `auth@file`). * **Middleware-Ziele begrenzen (geplant):** Middlewares mit eigenem Netzwerkziel (z. B. `forwardAuth`, Redirects auf andere Hosts) sollen auf das eigene Backend des Dienstes beschränkt werden können. * Grenze: das Tupel `ip:port`, `[ipv6]:port` bzw. `fqdn:port`, eventuell plus höchstens ein Pfadsegment. * Redirect/Auth auf einen Host derselben Zone (z. B. `acronym.meine.home.lab` → `acronym-service.meine.home.lab`) soll möglich bleiben, um Zertifikate zu erleichtern. * Aktivierung **per Label**; zunächst im **Beobachtungsmodus** (nur Audit-Log, keine Sperre). * Zunächst werden nur die mitgelieferten Redirects (to-www/to-non-www) angeboten; alles andere wird **geduldet**. * **Umgesetzt ist die Andockstelle** `policy.Engine.MiddlewareTargetCheck(mwName, def, backend)`: * nil (Default): alles dulden * mit Prüffunktion und `EnforceMiddlewareTargets = false`: Verstoß nur melden (`Observed`) * mit `true`: Router entfernen * Regeln und Label-Name folgen später. * **Middleware-Gates:** * Ohne Feature `custom_middleware` sind nur Basistypen erlaubt: redirectScheme, redirectRegex, stripPrefix(Regex), addPrefix, replacePath(Regex), compress, headers. * `chain` und `errors` sind **immer** verboten, weil sie per Namen auf andere Middlewares/Services verweisen und den Namensraum umgehen könnten. * Nicht referenzierte Services/Middlewares werden nicht ausgeliefert. ### 3.4 Bedrohungsmodell: Erstkontakt (TOFU) und Server-Events * **Schwäche von reinem TOFU:** Beim ersten Kontakt wird ungeprüft vertraut. Ein Angreifer im Netzwerkweg (ARP-Spoofing im LAN, DNS-Manipulation, kompromittierter Router, offenes WLAN) würde dauerhaft gepinnt. Er könnte damit: * den Enrollment-Token abgreifen und mit eigenem Schlüssel zuerst einlösen, * sich dauerhaft dazwischenschalten (der echte Server gilt danach als „Fälschung“), * unsignierte Server-Events (`node_command`, `lease_renew`) einschleusen. * **Gegenmaßnahme (umgesetzt):** Der Fingerprint ist im Enrollment-Token eingebettet (3.1/3.2). Damit gibt es keinen ungeprüften Erstkontakt mehr. Ein Angreifer scheitert im TLS-Handshake und sieht weder Token noch Signatur; der Token bleibt beim echten Server einlösbar. * **Betriebsrisiko:** Ein Verlust von `/server.key` erzeugt ein neues Zertifikat und sperrt alle Agents aus. Deshalb muss `server.key` gesichert werden. *(Geplant:)* geordneter Zertifikatswechsel, bei dem der Server das neue Zertifikat vorab über den gepinnten Kanal ankündigt. * **Zweite Schicht (geplant):** Server→Agent-Events werden mit einem Server-Signaturschlüssel signiert, den der Agent beim Enrollment lernt. Die Echtheit von `node_command` hängt dann nicht mehr allein am TLS-Pin. --- ## 4. Datenmodell (Data Specifications) ### 4.1 Lizenz & Capabilities (`license.json`) ```json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "SultanCapabilities", "type": "object", "properties": { "tenant_id": { "type": "string" }, "tier": { "type": "string", "enum": ["community", "pro", "enterprise"] }, "max_routes": { "type": "integer", "description": "-1 für unbegrenzt" }, "allowed_domains": { "type": "array", "items": { "type": "string" } }, "features": { "type": "array", "items": { "type": "string", "enum": ["websockets", "tcp_stream", "custom_middleware"] } }, "valid_until": { "type": "string", "format": "date-time" }, "signature": { "type": "string", "description": "Ed25519 Signatur des Server-Master-Keys über den Payload" } }, "required": ["tenant_id", "tier", "max_routes", "allowed_domains", "valid_until", "signature"] } ``` * **Signatur:** Ed25519 mit dem Server-Master-Key. * Signiert wird `SULTAN-LICENSE-V1\n` + JSON der Felder in fester Reihenfolge (`tenant_id`, `tier`, `max_routes`, `allowed_domains`, `features`, `valid_until`). * `valid_until` geht als RFC 3339 in UTC mit ganzen Sekunden ein. * Jede Änderung eines Felds, auch die Reihenfolge der Domains, macht die Signatur ungültig. * Das Präfix trennt Lizenz-Signaturen von künftigen anderen Nutzungen desselben Master-Keys (z. B. signierte Server-Events, 3.4). * **Einlesen:** Unbekannte Felder werden abgelehnt; `features` fehlt → leere Liste. * **`allowed_domains`:** Kleinbuchstaben, ohne Punkt am Ende, IDN als Punycode (`xn--…`). * `shop.de` deckt **genau** `shop.de`. * `*.firma.de` deckt **jede Subdomain beliebiger Tiefe**, aber **nicht** `firma.de` selbst. * Wildcards nur ganz vorne; `*.de` (Wildcard auf TLD) und `*` sind unzulässig. * **Ablauf:** Der Zeitpunkt `valid_until` selbst gilt als abgelaufen. „Verbleibende Tage“ (Checkmk) = abgerundete volle Tage, nach Ablauf negativ. * **Routen:** `max_routes = -1` bedeutet unbegrenzt. ### 4.1a Leases (Entscheidung 2026-10-01) * Eine **Lease ist eine kurzlaufende, signierte Kopie der Lizenz** (gleiches Format, eigenes `valid_until`). Sie ist der „Ausweis“, mit dem ein Knoten Routen bekommt. * **Kommerziell:** Laufzeit **7 Tage**, Erneuerung nach rund 2/3 (ab Tag 4–5); bei Wartung/Baustelle 10 Tage, Erneuerung ab Tag 5. Angelehnt an ACME (90 Tage, Erneuerung nach ~60). Die Lease endet spätestens mit dem bezahlten Abo-Zyklus (`valid_until` der Lizenz). * **Community:** Laufzeit **max. 14 Tage**, Erneuerung nach **ca. 7 Tagen mit zufälliger Streuung** (damit nicht alle Agents gleichzeitig erneuern). Der Server stellt Lizenz und Lease selbständig neu aus (re-license). Es gibt kein Mahnwesen. * Eine Wartung des Servers darf damit bis zu 7 Tage dauern: Bis die Lease abläuft, wird nichts invalidiert, keine Authentifizierung, keine Policy, keine Route. * Ist das Backend in dieser Zeit nicht erreichbar, zeigt der Lander eine 50x-Seite („geht gerade nicht …“) mit Hinweis auf den Pro-Service. * **Unterbrechung jederzeit serverseitig möglich:** Agent und Traefik pollen bzw. halten den Stream, also wirkt eine Unterbrechung sofort (`Hub.Disconnect`, keine Erneuerung, Policy-Stufe). * Gründe: Security-Breach, DDoS, Domain-Verstoß. * Nur kommerziell zusätzlich: Verstoß gegen Nutzungsrichtlinien (z. B. strafbare Inhalte), Zahlungsausfall, unterbrochenes Abo. * **Eskalation (nur kommerziell), analog zu Mahnstufen:** Karenzzeit mit Wasserzeichen → Firemark → Paywall-Landingpage („Kauf mich!“), siehe Stufen in 5.2. ### 4.1b Agent-Konfiguration `/etc/sultan.d/*.yaml` (Entscheidung 2026-10-01) * **Format: YAML.** Eine Datei beschreibt einen oder mehrere Dienste mit allem, was Traefik-Labels hergeben würden, plus Komfortschaltern. Beispiel (Schema noch auszuarbeiten): ```yaml services: - name: kunde-a url: http://127.0.0.1:8080 # lokaler Dienst (oder: interface + port, s. u.) hosts: [kunde-a.de, www.kunde-a.de] https: true redirect_http_to_https: true redirect_www: to_non_www # none | to_www | to_non_www healthcheck: interval: 30s expect_status: 200 expect_title: "KundeA - Willkommen in KundeA's Online-Welt" # optional expect_contains: { tag: h2, text: "Impressum" } # optional labels: # optional: rohe traefik.*-Labels für alles Weitere traefik.http.middlewares.x.headers.stsseconds: "31536000" ``` * **Healthcheck → `service_down`:** Der Agent ruft den lokalen Dienst ab und erwartet `200 OK`, optional auch einfache Inhaltsprüfungen (``, Tag mit Text). Ein Fehlschlag meldet `service_down`, die Policy setzt `maintenance`. * **Backend-Adresse/NetBird:** Interface (z. B. `wt0`) und Port werden **konfiguriert**; die IP wird über das Interface automatisch ermittelt. Ports lassen sich nicht zuverlässig erraten. Als Vorschlag dienen typische Ports: tcp/8080, tcp/80, tcp/443, tcp/3000, tcp/8000, tcp/8443. ### 4.2 Stream Event Protocol (Agent <-> Server) Beide Richtungen nutzen dasselbe JSON-Event-Format. Server→Agent läuft über den SSE-Stream; jedes Event ist ein SSE-Rahmen `id: <event_id>`, `event: <type>`, `data: <JSON in einer Zeile>`, gefolgt von einer Leerzeile (max. 1 MiB). Agent→Server läuft über signierte HTTPS-Requests. `node_id` setzt der Server immer selbst aus der geprüften Identität. ```json { "event_id": "evt_01J8Z9X2Y3...", "timestamp": 1727618531, "type": "service_state | lease_renew | node_command | error", "node_id": "node-proxmox-vm-42", "payload": {} } ``` * **Typ `service_state` (Agent -> Server):** Enthält die vollständige Liste der aktuell aktiven Traefik-Labels des Knotens. * **Typ `node_command` (Server -> Agent):** Enthält Aktionen wie `re_enroll`, `drain`, `maintenance`. * **Typ `lease_renew` (Server -> Agent):** Liefert ein verlängertes Krypto-Lease an den Agenten zurück. ### 4.3 SQLite Datenbankschema (Server State) Der Server persistiert Mandanten, Tokens und Richtlinien lokal in SQLite (`<data-dir>/sultan.db`, Default-Datenverzeichnis `./suldata`). * **Treiber:** `modernc.org/sqlite` (reines Go, kein cgo → statisches Single Binary). * **Pragmas:** `foreign_keys=ON` (sonst greift `ON DELETE CASCADE` nicht), WAL, `busy_timeout`, Schreib-Transaktionen `IMMEDIATE`. * **Migrationen:** Versioniert über `PRAGMA user_version`, nur anhängen. Eine Datenbank mit neuerer Version als das Programm wird abgelehnt. * **Zeitstempel:** Die Anwendung setzt sie selbst (keine `CURRENT_TIMESTAMP`-Defaults), als UTC-Text fester Breite (`2006-01-02T15:04:05.000000000Z`). Dadurch funktionieren Vergleiche direkt in SQL. * **Tokens:** Gespeichert wird nur der SHA-256-Hash. Schema (Stand Migration v1): ```sql -- Mandanten & Abonnements CREATE TABLE tenants ( id TEXT PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL, tier TEXT NOT NULL DEFAULT 'community', max_routes INTEGER NOT NULL DEFAULT 5, allowed_domains TEXT NOT NULL, -- JSON-Array: ["*.firma.de", "shop.de"] valid_until DATETIME NOT NULL, -- Ablaufdatum des Vertrags / Abos status TEXT NOT NULL DEFAULT 'active', -- active, suspended_payment, quarantined, expired created_at DATETIME NOT NULL, -- von der Anwendung gesetzt (UTC-Text) updated_at DATETIME NOT NULL ); -- Registrierte Agenten / Knoten CREATE TABLE nodes ( id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, hostname TEXT NOT NULL, public_key TEXT NOT NULL UNIQUE, -- Ed25519 Public Key des Agenten last_seen DATETIME NOT NULL, ip_address TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'online' -- online, offline, drained ); CREATE INDEX nodes_tenant_id ON nodes(tenant_id); -- Einmalige Enrollment-Tokens CREATE TABLE enrollment_tokens ( token_hash TEXT PRIMARY KEY, tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, expires_at DATETIME NOT NULL, used INTEGER NOT NULL DEFAULT 0 ); CREATE INDEX enrollment_tokens_tenant_id ON enrollment_tokens(tenant_id); ``` --- ## 5. Anwendungsmodell (Application Model) Das gesamte System ist als **Single-Binary-Architektur** in Go implementiert: ```text sultan ├── server # Führt Registry, Stream-Hub, Policy-Engine & Traefik-Provider aus ├── agent # Führt Docker-/File-Watcher aus und verbindet sich zum Server └── token # CLI-Tools zur Schlüsselverwaltung, Enrollment- & Lizenzausstellung ``` **Umsetzung:** `main.go` ist eine dünne Hülle (Signale → `pkg/cli.Run`); `pkg/cli` parst Befehle und Flags; `pkg/server` verdrahtet Store, Zertifikat, Master-Key, Hub, Verifier, Registry und Policy. **Datenverzeichnis:** Default **`./suldata`** (relativ zum Arbeitsverzeichnis), für Server **und** Agent; per `--data-dir` änderbar. Es wird mit Rechten 0700 angelegt und enthält Schlüssel. Es gehört gesichert und nie ins Git (steht in `.gitignore`). **`sultan server` – Flags und Defaults:** | Flag | Default | Bedeutung | |---|---|---| | `--data-dir` | `./suldata` | `sultan.db`, `server.key`/`server.crt`, `master.key` | | `--agent-addr` | `:8443` | Agent-Port, nur TLS 1.3 | | `--provider-addr` | `127.0.0.1:8080` | Traefik-Provider, Klartext. Bewusst Loopback, weil die Konfiguration Backend-IPs aller Mandanten enthält. Im Container `:8080` setzen und nur auf `127.0.0.1` veröffentlichen. | | `--interceptor-url` | `http://127.0.0.1:8081` | Sultan-Interceptor/Lander | | `--refresh` | `1m` | Neubewertung aller Knoten (zeitabhängige Stufen, Mandantenänderungen) | **HTTP-Routen des Servers:** | Port | Route | Zweck | |---|---|---| | Agent (TLS) | `POST /api/v1/enroll` | Body `{token, node_id, hostname}`, signiert mit neuem Schlüssel → 201 `{node_id, tenant_id, master_public_key}`; Token ungültig/verbraucht/abgelaufen → 403 ohne Details; ID/Schlüssel vergeben → 409 | | Agent (TLS) | `GET /api/v1/stream` | SSE-Stream (signiert); Stream offen = Knoten `online` | | Agent (TLS) | `POST /api/v1/state` | vollständiger Schnappschuss `{workloads: [{name, host, labels}], down_services}`; nur bei offenem Stream (sonst 409) → 204 | | Provider | `GET /v1/traefik-config` | `?format=json\|yaml` | | Provider | `GET /healthz` | Lebenszeichen | **Lebenszyklus eines Knotens:** * Enrollment legt ihn `offline` an. * Stream verbunden → `online`. * Stream getrennt → Routen sofort entfernt, `offline`. * `drained` bleibt bei beidem unverändert. Der Agent muss deshalb nach jedem (Wieder-)Verbinden seinen Zustand erneut senden. ### 5.1 Agenten-Subsysteme 1. **Watcher Engine:** * **Docker Engine:** Lauscht am `/var/run/docker.sock` via Docker Events API. Filtert nach Labels mit Präfix `traefik.`. * **File Provider:** Überwacht Verzeichnis `/etc/sultan.d/*.conf` via Inotify auf Konfigurationsdateien im INI/YAML-Format. 2. **State Serializer:** * Extrahiert Services, Host-Ports und Routen. * Injiziert automatisch die Ziel-IP (LAN-IP oder NetBird-IP) in die Server-URL-Definitionen für Traefik. 3. **Transport Engine:** * Verwaltet den permanenten TLS-Stream mit 45s-Keepalive. * Führt automatischen Reconnect mit exponentiellem Backoff durch. ### 5.2 Server-Subsysteme 1. **Stream Hub:** * Hält offene Sockets aller Agents im RAM. * Verarbeitet State-Pushes und führt Dead-Node-Detection durch (Timeout: 90s). 2. **Dynamic Config Aggregator:** * Führt die flachen Traefik-Labels aller aktiven Knoten zusammen. * Wandelt die Labels in das verschachtelte Traefik-JSON-Format um. * Stellt `GET /v1/traefik-config` bereit. 3. **Policy & Interception Engine:** * **Sperrung bei Zahlungsverzug:** Modifiziert den Traefik-Router auf einen internen Redirect zu einer Bezahlseite (`HTTP 402 / Payment Required`). * **Notfall-Wartung:** Schlägt das Backend fehl oder meldet der Agent `service_down`, liefert Traefik eine statische Statusseite mit `Retry-After: 60` (`HTTP 503`). * **Stufen (`pkg/policy.StageFor`)**, Reihenfolge der Prüfung: | Bedingung | Stufe | Wirkung | |---|---|---| | Status `quarantined` | `blocked` | keine Routen | | Status `expired` oder Lizenz abgelaufen | `landing` | Informations-Landingpage | | Tier `community` | `normal` | **kein Mahnwesen** | | Status `suspended_payment` oder Mahnstufe ≥ 3 | `paywall` | Kunde ist offline: freundliche Einladung des Hosters, den Sultan-Service und weitere Angebote kennenzulernen (402), **kein Durchlass** | | Mahnstufe 2 | `firemark` | Hinweisseite, dann Weiterleitung zum Original (längere Wartezeit) | | Mahnstufe 1 | `watermark` | Hinweisseite, dann Weiterleitung zum Original (kurze Wartezeit) | | Healthcheck meldet Dienst down | `maintenance` (je Router) | 503, `Retry-After: 60` | * **Sultan-Interceptor / Web-Lander** (eigener Dienst, nur Loopback, z. B. `127.0.0.1:8081`): Bei jeder Stufe außer `normal` leitet die Policy den Router auf den Service `sultan__interceptor` um. * Regel, TLS und die Middlewares des Mandanten bleiben erhalten. * Ein `headers`-Middleware setzt `X-Sultan-Stage`, `X-Sultan-Tenant` und `X-Sultan-Tier`. Traefik überschreibt damit vom Besucher gesendete Werte. * Der Lander liefert nur eine eigene HTML-Seite je Stufe. Er verändert keine Originalseiten und braucht deshalb kein Upstream. * **Mahnhinweis mit Durchlass (watermark/firemark):** 1. Der Lander zeigt den Hinweis und leitet per `<meta http-equiv="refresh" content="<x>;url=<ursprüngliche URL>">` zurück. Wartezeit konfigurierbar, Default **5 s (Stufe 1)** und **10 s (Stufe 2)**. 2. Dabei setzt er das Cookie `sultan_ack=<stufe>` auf der Mandanten-Domain, gültig **60 Minuten** (durchschnittlicher Besuch). Der Hinweis darf also mehrmals am Tag erscheinen. 3. Die Policy erzeugt dafür einen zweiten Router `sultan__<tenant>__<node>__<router>` mit der Regel ``(<Originalregel>) && HeaderRegexp(`Cookie`, `(^|;\s*)sultan_ack=<stufe>(;|$)`)``. Er führt direkt zum Original, sonst würde die Weiterleitung wieder beim Lander landen (Schleife). 4. Priorität: Ohne gesetzte Priorität gewinnt der Durchlass über die längere Regel, sonst über Priorität+1. 5. Eine bestätigte niedrigere Stufe gilt nicht für eine höhere. 6. Das Cookie ist bewusst kein Sicherheitsmerkmal: Wer es selbst setzt, überspringt nur den Hinweis. `paywall`, `landing` und `maintenance` haben keinen Durchlass. * **Eskalation in der Praxis:** Bei Stufe 1 und 2 alarmiert Checkmk. Zwei Alarme genügen; spätestens mit Stufe 3 ist der Kunde offline. * **Lander-Inhalte:** * **Eingebaut:** je Stufe eine minimale statische HTML-Seite, ohne CSS. Kurzer, nicht technischer Text in **de_DE und en_US**, was gerade funktioniert und was nicht. * **Eigene Vorlagen (optional):** Liegen in einem vereinbarten, per Docker gemounteten Verzeichnis Vorlagen, werden diese gerendert und ausgeliefert. * Format: Jinja2-artig; in Go umgesetzt über eine kompatible Template-Engine oder `html/template`, noch festzulegen. * Pro Lander-Seite höchstens **1× HTML-Vorlage, 1× CSS, 1× Grafik (WebP oder SVG)**. * Bewusst flach: kein allgemeiner Webserver. * **Mahnstufen-Zeitplan (`policy.DunningSchedule`):** Der Server berechnet die Stufe aus dem Fälligkeitsdatum der offenen Zahlung, angelehnt an Bank-Zahlungsfristen: **Stufe 1 ab 8 Tagen, Stufe 2 ab 25, Stufe 3 ab 32 Tagen** Verzug. Die Werte sind konfigurierbar. --- ## 6. Überwachung, Lizenzfristen & Alerting (Checkmk-Integration) Um zu verhindern, dass Kunden-Instanzen bei Ablauf von Quartals- oder Jahresabonnements unerwartet in den Wartungsmodus fallen, implementiert der Sultan-Server Schnittstellen für **Checkmk** und externe Alerting-Pipelines. ### 6.1 Checkmk Local Check Protocol Der Server stellt einen internen Management-Endpunkt bereit: `GET /metrics/checkmk` (oder auslesbar über ein lokales Checkmk-Agent-Plugin-Script `/usr/lib/check_mk_agent/local/sultan_licenses`). Das Ausgabeformat entspricht der nativen Checkmk Local-Check-Syntax: ```text # Syntax: <Status> <ServiceName> <Metriken> <Nachrichtentext> # 0 = OK, 1 = WARN, 2 = CRIT, 3 = UNKNOWN 0 "Sultan_Tenant_kunde_schmidt" days_left=74;30;14 OK - Abo aktiv bis 2026-12-15 (74 Tage verbleibend) 1 "Sultan_Tenant_meier_gmbh" days_left=12;30;14 WARN - Abo laeuft in 12 Tagen ab! Verlaengerung erforderlich. 2 "Sultan_Tenant_alt_ag" days_left=0;30;14 CRITICAL - Abo seit 1 Tagen abgelaufen! Status: suspended_payment ``` ### 6.2 Schwellenwerte & Alarmierungskaskade * **30 Tage vor Ablauf:** Warnung im Checkmk-Dashboard (Monitoring-Status: `WARN`). * **14 Tage vor Ablauf:** Kritischer Alert (`CRIT`). Automatischer E-Mail-Dispatch / Ticket-Erstellung an Kunden und Admin: *„Abo-Verlängerung erforderlich – bitte Rechnung begleichen“*. * **Tag 0 (Ablaufzeitpunkt):** * Status wechselt in SQLite auf `expired`. * Traefik leitet Domain auf Informations-Landingpage um. * Checkmk triggert Alarm: `CRITICAL - Service offline geschaltet`. --- ## 7. Referenzkonfigurationen ### 7.1 Compose-Setup: Ingress-Knoten ```yaml services: traefik: image: traefik:v3.1 container_name: traefik-ingress restart: unless-stopped network_mode: host command: - "--providers.http.endpoint=http://127.0.0.1:8080/v1/traefik-config" - "--providers.http.pollInterval=3s" - "--entrypoints.web.address=:80" - "--entrypoints.web.http.redirections.entryPoint.to=websecure" - "--entrypoints.web.http.redirections.entryPoint.scheme=https" - "--entrypoints.websecure.address=:443" - "--certificatesresolvers.dns-resolver.acme.dnschallenge=true" - "--certificatesresolvers.dns-resolver.acme.dnschallenge.provider=rfc2136" volumes: - ./acme.json:/etc/traefik/acme.json sultan-server: image: sultan:latest container_name: sultan-server restart: unless-stopped # Im Container auf allen Interfaces lauschen; nach außen nur 127.0.0.1 veröffentlichen. command: ["sultan", "server", "--data-dir=/suldata", "--provider-addr=:8080"] ports: - "8443:8443" # Agent Inbound (TLS Stream) - "127.0.0.1:8080:8080" # Traefik Config Provider & Checkmk Metrics – Klartext, nie öffentlich volumes: - ./suldata:/suldata # sultan.db, server.key/.crt, master.key – server.key und master.key sichern! ``` ### 7.2 Compose-Setup: Kunden-Host (Backend mit Docker) ```yaml services: sultan-agent: image: sultan:latest container_name: sultan-agent restart: unless-stopped command: - "sultan" - "agent" - "--server=https://ingress.dein-netz.de:8443" - "--token=sltn_enroll_<secret>.<server-fingerprint>" # Ausgabe von `sultan token` volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./suldata:/suldata # node.key (0600) und known_servers – persistent halten! (Agent mit --data-dir=/suldata) kunde-wordpress: image: wordpress:latest restart: unless-stopped labels: - "traefik.enable=true" - "traefik.http.routers.wp.rule=Host(`kunde.de`) || Host(`www.kunde.de`)" - "traefik.http.routers.wp.entrypoints=websecure" - "traefik.http.routers.wp.tls=true" - "traefik.http.routers.wp.tls.certresolver=dns-resolver" - "traefik.http.middlewares.wp-www.redirectregex.regex=^https?://kunde\\.de/(.*)" - "traefik.http.middlewares.wp-www.redirectregex.replacement=https://www.kunde.de/$${1}" - "traefik.http.middlewares.wp-www.redirectregex.permanent=true" - "traefik.http.routers.wp.middlewares=wp-www" - "traefik.http.services.wp.loadbalancer.server.port=80" ``` --- ## 8. Architekturentscheidungen (Entscheidungslog) | Datum | Entscheidung | Begründung | |---|---|---| | 2026-09-30 | Traefik bezieht die Konfiguration per **HTTP-Provider (Pull)** von `/v1/traefik-config`; kein Docker-Socket-Nachbau. | Gleichwertiger Traefik-Provider; ein Socket-Nachbau wäre fragil. Remote-Docker-Provider scheidet aus (Root-Zugriff auf Kunden-Hosts, eingehende Ports, nur ein Endpunkt). | | 2026-09-30 | Labels → Traefik-Konfiguration: Router/Services typisiert, **Middleware-Optionen als unveränderte Label-Strings** durchgereicht; nicht unterstützte Labels (TCP/UDP, unbekannte Attribute) werden abgelehnt statt ignoriert. Nur mit `traefik.enable=true`. | Traefik parst Middleware-Werte beim Einlesen wie Labels; Sultan muss nicht jeden Middleware-Typ kennen. | | 2026-09-30 | Server→Agent-Stream als **SSE**; Agent→Server über eigene signierte Requests. | Das Keepalive `:\n\n` ist SSE-Syntax; SSE ist proxy- und HTTP/1.1-tauglich. | | 2026-10-01 | Tenant-Status **`expired`** als vierter gültiger Wert. | Abschnitt 6.2 setzt ihn bei Ablauf. | | 2026-10-01 | SQLite über **`modernc.org/sqlite`**. | Kein cgo, statisches Single Binary. | | 2026-10-01 | **Ed25519-Request-Signatur in jedem Tier**, auch `community`. | Kein ungeschützter Homelab-Modus; einheitliches Sicherheitsniveau. | | 2026-10-01 | Authentifizierung sperrt nur `quarantined`; übrige Sperren über Routing-Policy. | Agent braucht den Kanal weiterhin (Lease, Reaktivierung). | | 2026-10-01 | **Kein Klartext für Agent-Verkehr**: TLS 1.3 only, Signer nur für `https`, Verifier nur mit TLS. | Signaturen und Labels dürfen nie unverschlüsselt übertragen werden. | | 2026-10-01 | **Token-Format `sltn_enroll_<secret>.<fingerprint>`**, Pinning mit geprüftem Erstkontakt statt blindem TOFU. | Schließt die Man-in-the-Middle-Lücke beim Erstkontakt (3.4). Entscheidet zugleich den früheren Widerspruch `sltn_token_` vs. `sltn_enroll_`. | | 2026-10-01 | Lizenz-Signatur mit Versionspräfix `SULTAN-LICENSE-V1`; Wildcard `*.domain` deckt alle Subdomain-Ebenen, nicht die Apex-Domain; `*.<tld>` verboten. | Eindeutige, manipulationssichere Kodierung; Wildcard-Semantik entspricht der Erwartung „Mandant besitzt die Zone“, ohne versehentlich ganze TLDs freizugeben. | | 2026-10-01 | **Leases**: signierte Lizenz-Kopie, 7 Tage (Wartung 10), Erneuerung nach ~2/3; kommerziell an Abo-Zyklus gebunden; Community automatisch neu ausgestellt; jederzeit serverseitig unterbrechbar. | Kurze Laufzeit begrenzt Schaden bei Missbrauch; Sperren wirken über Polling ohnehin sofort (ACME-Analogie). | | 2026-10-01 | **Eskalatives Mahnwesen nur kommerziell**: Wasserzeichen → Firemark → Paywall (402); umgesetzt über einen Sultan-Interceptor, zu dem die Policy umleitet. | Mehrstufige Dringlichkeit analog zu Mahnstufen; Traefik selbst kann Seiteninhalte nicht verändern. | | 2026-10-01 | **Policy parst Regeln** und verlangt Host-Einschränkung in jedem `\|\|`-Zweig; `HostRegexp` verboten; Namensraum `<tenant>__<node>__<name>`; `@`-Referenzen nur per Freigabeliste; `chain`/`errors` verboten. | Verhindert Catch-all-Übernahmen, Zugriff auf `api@internal` und Querverweise zwischen Mandanten. | | 2026-10-01 | `/etc/sultan.d` als **YAML** mit Komfortschaltern + rohen Labels; Healthcheck (Status + einfache Inhaltsprüfung) im YAML; Interface/Port konfiguriert. | Für Bare-Metal-Admins lesbarer als Labels; Ports sind nicht sicher erratbar. | | 2026-10-01 | **Benachrichtigung:** Community per Webhook; kommerziell SMTP an Mandanten + Ticketsystem (Zammad per Webhook/API). | Homelab braucht keinen Mailserver; kommerziell nachvollziehbare Kommunikation. | | 2026-10-01 | **Admin-Zugriff:** Community nur lokale CLI; kommerziell zusätzlich Admin-API (Rechnungssystem, Ansible). | Automatisierte Lizenzpflege im kommerziellen Betrieb. | | 2026-10-01 | **Community-Lease 14 Tage**, Erneuerung ~7 Tage mit Streuung; Wartung bis 7 Tage ohne Invalidierung, solange 50x-Lander mit Pro-Hinweis. | Homelab-Betrieb robust gegen Server-Wartung; Upsell statt Ausfall. | | 2026-10-01 | **Mahnstufen 8/25/32 Tage** (Bank-Zahlungsfristen), vom Server berechnet. **Watermark/Firemark als Hinweisseite mit `meta refresh`** statt Einblendung ins Original; Durchlass per Cookie `sultan_ack=<stufe>` über einen zweiten Router. | Deutlich einfacher als Seiten umzuschreiben; Cookie verhindert die Weiterleitungsschleife. | | 2026-10-01 | **Middleware-Zielbegrenzung** vorgesehen (Backend-Tupel `host:port`, per Label, erst Beobachtungsmodus). | SSRF-Risiko durch `forwardAuth` u. ä.; Regeln erst nach Beobachtung festlegen. | | 2026-10-01 | **Admin-API mit API-Token**; TLS des Admin-Interfaces über ACME (öffentliches Zertifikat via Traefik), kein Pinning nötig. | Gängige Praxis für Rechnungssystem/Ansible; ACME ist auf dem Ingress ohnehin vorhanden. | | 2026-10-01 | Mahnhinweis-Weiterleitung **5 s / 10 s**, Cookie **60 min**; Stufe 3 ohne Durchlass = Kunde offline mit Hoster-Einladung. | Hinweis pro Besuch statt pro Request; Eskalation endet mit Abschaltung nach zwei Checkmk-Alarmen. | | 2026-10-01 | **Lander:** eingebaute minimale HTML-Seiten (de_DE/en_US, ohne CSS); optional eigene Vorlagen aus gemountetem Verzeichnis, max. 1 HTML-Vorlage + 1 CSS + 1 Grafik pro Seite. | Funktioniert ohne Konfiguration; Anpassung möglich ohne komplexen Webserver. | | 2026-10-01 | Middleware-Zielbegrenzung als **Andockstelle mit Beobachtungs-/Durchsetzungsmodus**, Default dulden. | Erst beobachten, dann Regeln festlegen. | | 2026-10-01 | **Registry**: `service_state` = vollständiger Schnappschuss je Knoten (Workloads mit Host + Labels, Liste ausgefallener Services); Workloads einzeln übersetzt, bei Namenskonflikt innerhalb eines Knotens gewinnt der alphabetisch erste; fail-static bei Directory-Ausfall; Verstöße nur beim ersten Auftreten melden. | Robust gegen Teilausfälle; Audit-Log ohne Wiederholungsflut. | | 2026-10-01 | **Lander-Vorlagen mit Go `html/template`**; die eingebauten Seiten lassen sich als Beispiel exportieren (`dump-landing`). | Eingebaut, ohne Abhängigkeit, schützt automatisch gegen eingeschleustes HTML; Syntax erlernbar. | | 2026-10-01 | **Datenverzeichnis `./suldata`** für Server und Agent; Ports `:8443`/`8080` als konfigurierbare Defaults, Provider-Port auf Loopback. | Einheitlich, einfach zu mounten; Klartext-Port nicht versehentlich öffentlich. | | 2026-10-01 | **`sultan server`**: Knoten-Lebendigkeit = offener Stream; Routen beim Trennen sofort entfernt; `POST /api/v1/state` nur bei offenem Stream. | Keine verwaisten Routen; einfaches, eindeutiges Modell. | | geplant | Signierte Server-Events; geordneter Zertifikatswechsel; optionale Root-CA. | Zweite Sicherheitsschicht neben dem TLS-Pin; Betrieb ohne Aussperren. | --- ## 9. Umsetzungsstand & Roadmap **Umgesetzt (Go-Packages, testgetrieben):** | Package | Inhalt | |---|---| | `pkg/traefik` | Labels → Traefik-Konfiguration, Merge, JSON/YAML | | `pkg/stream` | SSE-Hub (Server) und Client (Agent) inkl. Keepalive, Backoff, Disconnect | | `pkg/store` | SQLite: Mandanten, Knoten, Enrollment | | `pkg/auth` | Ed25519-Request-Signatur, Verifier, Schlüsseldateien | | `pkg/crypto` | Server-Zertifikat, TLS 1.3, Pinning, Enrollment-Token | | `pkg/license` | Lizenz ausstellen, prüfen, Domain/Routen/Feature-Checks | | `pkg/policy` | Regel-Parser, Domain-/Routen-/Feature-/Referenz-Prüfung, Namensraum, Stufen & Interceptor-Umleitung, Mahnstufen-Zeitplan, Andockstelle Middleware-Ziele | | `pkg/registry` | Aggregator: Zustand je Knoten → Labels → Policy → Merge; `/v1/traefik-config`-Handler; Audit-Callback | | `pkg/server` | `sultan server`: Datenverzeichnis, Master-Key, HTTP-Routen beider Ports, Knotenstatus, Refresh-Schleife, Graceful Shutdown, Audit-Log über `slog` | | `pkg/cli`, `main.go` | Befehle `server`, `version`, `help` (`agent`/`token` als Platzhalter) | **Offen – Bausteine:** 1. **Sultan-Interceptor / Web-Lander** (5.2): eigener Dienst auf Loopback; je `X-Sultan-Stage` eine HTML-Seite (eingebaut de_DE/en_US oder eigene `html/template`-Vorlage aus dem Datenverzeichnis; Befehl **`dump-landing`** exportiert die eingebauten Seiten als Vorlage zum Anpassen): Mahnhinweis mit `meta refresh` (5/10 s) + Cookie `sultan_ack` (60 min), Paywall/Hoster-Einladung (402), Landing, Wartung (503, `Retry-After: 60`; Community mit Pro-Hinweis). Audit-Log für Policy-Violations (Callback der Registry). 2. **Server-Ergänzungen:** `GET /metrics/checkmk`; Rate-Limit für `/enroll`; Gnadenfrist vor dem Entfernen von Routen beim Trennen (derzeit sofort); Konfiguration auch per Umgebungsvariablen. 3. *(entfällt – umgesetzt in `pkg/server`)* 4. **Leases:** * Lease bei Enrollment ausstellen und per `lease_renew` erneuern (Master-Key und Übergabe des Public Keys beim Enrollment sind umgesetzt). * Persistenz → Migration v2: `features` und offene Zahlung (`payment_overdue_since`, daraus die Mahnstufe per `DunningSchedule`) fehlen im Schema; Leases (aktuelle Lease je Knoten). * Lease-Laufzeiten (kommerziell 7/10 Tage, Community 14 Tage mit Erneuerung ~7 Tage + Streuung), Bindung an Abo-Zyklus (4.1a). 5. **Agent:** * Docker-Watcher (Events API über `/var/run/docker.sock`) * File-Provider `/etc/sultan.d/*.yaml` per inotify (Schema 4.1b ausarbeiten, Übersetzung in Labels) * Healthchecks (Status, `<title>`, Tag mit Text) → `service_down` * State-Serializer: Ziel-IP LAN oder NetBird `100.64.x.y` * Senden von `service_state`; Behandlung von `node_command` 6. **Lizenzfristen & Benachrichtigung:** periodischer Job (Status → `expired` an Tag 0, Mahnstufen), Checkmk-Ausgabe (6.1); Community: Webhook; kommerziell: SMTP an Mandanten + Zammad (Webhook/API). 7. **Admin-API (nur kommerziell):** Mandanten, Lizenzen, Zahlungsstatus für Rechnungssystem und Ansible; Authentifizierung per API-Token, TLS über ACME. 8. **CLI:** * `sultan agent` (Default `--data-dir=./suldata`) * `sultan token`: Mandant anlegen, Token erzeugen, Lizenz ausstellen, Knoten auflisten/drainen/löschen. Arbeitet direkt auf der SQLite-DB des Servers (WAL erlaubt parallelen Zugriff). * `sultan dump-landing`: eingebaute Lander-Seiten als Vorlage exportieren. 9. **Betrieb:** * Strukturiertes Logging (`log/slog`), Graceful Shutdown, Versionsinfo * Dockerfile (statisch, distroless), systemd-Unit * CI (`go vet`, `go test -race`, Linter), Releases 10. **Sicherheit (geplant, 3.4):** signierte Server-Events, geordneter Zertifikatswechsel, optionale Root-CA. **Offene Fachfragen (vor Umsetzung klären):** * Lander-Vorlagen: genauer Unterordner im Datenverzeichnis (z. B. `./suldata/landing/`) und Namensschema der Dateien. * Middleware-Zielbegrenzung: genaue Grenze (Pfadsegment? Zonen-Redirects?), Label-Name, Wechsel vom Beobachtungs- in den Sperrmodus.