Files
sultan/.ai/sultan-system-architecture.md
T
2026-10-01 23:52:15 +02:00

46 KiB
Raw Blame History

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

[ Ö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.
  1. 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.
  1. 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 <name>@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 <data-dir>/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: <server> <fingerprint>, 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>.<fingerprint>
    • secret: 256 Bit Zufall (Base64url). Der Server speichert nur den SHA-256 von sltn_enroll_<secret>, 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_<secret> 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 <tenant>__<node>__<name>.
    • 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 <data-dir>/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)

{
  "$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):
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 (<title>, 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.

{
  "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):

-- 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:

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

# 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

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)

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.