erste module drin.
zwischenstand: Architektur aktualisiert, TOFU implementiert,
This commit is contained in:
1 parent
9d5789c5d7
commit
c8df9d7651
41 files changed
+6455
-36
No files matched your search
@@ -1,6 +1,3 @@
|
||||
# CLAUDE.md
|
||||
|
||||
|
||||
# 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).
|
||||
@@ -74,7 +71,8 @@ Klassische Ingress-Lösungen wie Nginx Proxy Manager (NPM) erfordern eine manuel
|
||||
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 HTTP/2- oder SSE-Stream. Alle 45 Sekunden wird ein minimales Keepalive-Byte (`:\n\n`) ausgetauscht. Verhindert das Schließen von State-Tables in NAT-Routern (Conntrack Timeout).
|
||||
* **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.
|
||||
|
||||
|
||||
@@ -86,6 +84,8 @@ Klassische Ingress-Lösungen wie Nginx Proxy Manager (NPM) erfordern eine manuel
|
||||
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 `<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.
|
||||
|
||||
|
||||
|
||||
@@ -95,18 +95,51 @@ Klassische Ingress-Lösungen wie Nginx Proxy Manager (NPM) erfordern eine manuel
|
||||
|
||||
### 3.1 Transport-Verschlüsselung (TLS ohne CA-Zirkus)
|
||||
|
||||
* **Server-Zertifikat:** Generiert sich beim ersten Start selbst (Self-Signed / Snakeoil ECDSA P-256).
|
||||
* **Vertrauensmodell:** Trust-on-First-Use (TOFU) analog zu SSH. Der Agent verifiziert beim ersten Connect den SHA256-Fingerprint des Servers und speichert ihn in `known_servers`.
|
||||
* **Optional:** Unterstützung für explizit gemountete Root-CA-Zertifikate.
|
||||
* **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.
|
||||
* 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 kurzlebigen Einladungs-Token auf dem Server (`sltn_token_...`).
|
||||
2. Agent generiert lokalen Key (`node.key`) und sendet seinen Public Key (`node.pub`) zusammen mit dem Einladungs-Token an den Server (`POST /api/v1/enroll`).
|
||||
3. Server speichert den Public Key des Agenten, weist ihm eine Mandanten-ID zu und signiert das initiale Berechtigungszertifikat (Capability Lease).
|
||||
4. Der Einladungs-Token wird entwertet; alle künftigen Heartbeats werden kryptografisch vom Agenten mit `node.key` signiert.
|
||||
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`).
|
||||
|
||||
|
||||
|
||||
@@ -117,6 +150,16 @@ Jeder Payload vom Agenten an den Server muss vom Server gegen die zugewiesene Po
|
||||
* **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.
|
||||
|
||||
### 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)
|
||||
@@ -125,7 +168,7 @@ Jeder Payload vom Agenten an den Server muss vom Server gegen die zugewiesene Po
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "[http://json-schema.org/draft-07/schema#](http://json-schema.org/draft-07/schema#)",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "SultanCapabilities",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -150,7 +193,7 @@ Jeder Payload vom Agenten an den Server muss vom Server gegen die zugewiesene Po
|
||||
|
||||
### 4.2 Stream Event Protocol (Agent <-> Server)
|
||||
|
||||
Das bidirektionale Streaming-Format nutzt standardisierte JSON-Events:
|
||||
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
|
||||
{
|
||||
@@ -172,7 +215,15 @@ 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 (`/var/lib/sultan/sultan.db`):
|
||||
Der Server persistiert Mandanten, Tokens und Richtlinien lokal in SQLite (`/var/lib/sultan/sultan.db`).
|
||||
|
||||
* **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
|
||||
@@ -184,9 +235,9 @@ CREATE TABLE tenants (
|
||||
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
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
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
|
||||
@@ -197,9 +248,9 @@ CREATE TABLE nodes (
|
||||
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
|
||||
FOREIGN KEY(tenant_id) REFERENCES tenants(id)
|
||||
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 (
|
||||
@@ -208,6 +259,7 @@ CREATE TABLE enrollment_tokens (
|
||||
expires_at DATETIME NOT NULL,
|
||||
used INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX enrollment_tokens_tenant_id ON enrollment_tokens(tenant_id);
|
||||
|
||||
```
|
||||
|
||||
@@ -309,7 +361,7 @@ services:
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
command:
|
||||
- "--providers.http.endpoint=[http://127.0.0.1:8080/v1/traefik-config](http://127.0.0.1:8080/v1/traefik-config)"
|
||||
- "--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"
|
||||
@@ -329,7 +381,7 @@ services:
|
||||
- "8443:8443" # Agent Inbound (TLS Stream)
|
||||
- "127.0.0.1:8080:8080" # Traefik Config Provider & Checkmk Metrics
|
||||
volumes:
|
||||
- ./sultan-data:/var/lib/sultan
|
||||
- ./sultan-data:/var/lib/sultan # sultan.db, server.key (0600), server.crt – server.key sichern!
|
||||
|
||||
```
|
||||
|
||||
@@ -344,11 +396,11 @@ services:
|
||||
command:
|
||||
- "sultan"
|
||||
- "agent"
|
||||
- "--server=[https://ingress.dein-netz.de:8443](https://ingress.dein-netz.de:8443)"
|
||||
- "--token=sltn_enroll_01J8Z9..."
|
||||
- "--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
|
||||
- ./agent-data:/var/lib/sultan
|
||||
- ./agent-data:/var/lib/sultan # node.key (0600) und known_servers – persistent halten!
|
||||
|
||||
kunde-wordpress:
|
||||
image: wordpress:latest
|
||||
@@ -360,9 +412,26 @@ services:
|
||||
- "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/$$](https://www.kunde.de/$$){1}"
|
||||
- "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_`. |
|
||||
| geplant | Signierte Server-Events; geordneter Zertifikatswechsel; optionale Root-CA. | Zweite Sicherheitsschicht neben dem TLS-Pin; Betrieb ohne Aussperren. |
|
||||
Reference in new issue
Block a user