Files
sultan/CLAUDE.md
T
br c8df9d7651 erste module drin.
zwischenstand: Architektur aktualisiert, TOFU implementiert,
2026-10-01 22:22:13 +02:00

7.4 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Instruktionen

Lies die .ai/*.md Files. Diese liefern den Context für die App, die hier entstehen soll. Maßgeblich ist aktuell .ai/sultan-system-architecture.md (Netzwerk-, Sicherheits-, Daten- und Anwendungsmodell inkl. SQL-Schema, Event-Format und Referenz-Compose-Files). Bei Widersprüchen zwischen diesem File und der Spec gilt die Spec. Architekturentscheidungen werden in der Spec nachgepflegt (betroffene Abschnitte + Entscheidungslog in Abschnitt 8) – das gehört zu jeder Änderung, die Architektur festlegt.

Projektstatus

Frühe Phase: Go-Modul sultan (Go 1.25), noch kein main/Subcommand. Packages:

  • pkg/traefik – übersetzt flache traefik.*-Labels (FromLabels) in Traefiks dynamische Konfiguration, führt Knoten-Configs zusammen (Merge) und rendert JSON/YAML (Marshal) für /v1/traefik-config. Traefik pollt diesen Endpunkt über seinen HTTP-Provider; Sultan schreibt nie in Traefik und emuliert keinen Docker-Socket.
  • pkg/stream – Server→Agent-Kanal als SSE: Hub (Server, ein Stream pro Knoten, Keepalive :\n\n, Neuverbindung ersetzt alte mit ErrReplaced, OnConnect/OnDisconnect nur bei echtem Online/Offline), Client (Agent, Idle-Timeout 90s, Run mit exponentiellem Backoff, Prepare zum Signieren), Decoder. Authentifizierung macht der Aufrufer vor Hub.Serve. Agent→Server (service_state) läuft nicht über diesen Stream, sondern über eigene Requests.
  • pkg/store – SQLite-Persistenz (Schema Spec 4.3) über modernc.org/sqlite (reines Go, kein cgo → statisches Binary). Migrationen sind eine append-only Liste in store.go, Version in PRAGMA user_version; neuere DB-Version → ErrSchemaTooNew. Zeitstempel werden aus Go gesetzt (injizierbare Uhr Store.now) und als UTC-Text fester Breite gespeichert, damit SQL-Vergleiche funktionieren – keine CURRENT_TIMESTAMP-Defaults verwenden. Enrollment-Tokens nur als SHA-256; Enroll verbraucht Token und legt Knoten atomar an. Fehler: ErrNotFound/ErrExists/ErrInvalid/ErrTokenUsed/ErrTokenExpired.
  • pkg/auth – Ed25519-Request-Signatur, in jedem Tier Pflicht (kein Bypass, auch nicht für community/Homelab). Signer.Sign (passt als stream.Client.Prepare) signiert Methode, Pfad+Query, Zeitstempel, Nonce und Body-Hash (X-Sultan-*-Header). Verifier.Verify prüft in der Reihenfolge TLS → Signatur → Zeitfenster (±5min) → Nonce → Store, liefert Identity{Node, Tenant}; die Knoten-ID für Hub.Serve kommt nur daraus. VerifyUnregistered für Enrollment (Besitznachweis des Schlüssels). Gesperrt (ErrForbidden) wird nur bei Quarantäne; Zahlungsverzug/Ablauf/Drain regelt die Policy über das Routing. StatusCode(err) für HTTP-Antworten. Schlüsseldateien: PKCS#8-PEM, 0600, werden nie überschrieben.
  • Kein Klartext: Signer signiert nur https-URLs, Verifier lehnt Requests ohne r.TLS ab (ErrInsecureTransport, 426). Tests für Agent↔Server laufen daher über TLS-Testserver.
  • pkg/crypto – Transport: LoadOrCreateServerCert(dataDir) (selbstsigniert ECDSA P-256, keine CA, 20 Jahre, server.key 0600; fehlt eine der Dateien → Fehler statt Neuerzeugung), ServerTLSConfig (nur TLS 1.3), ClientTLSConfig(server, *KnownServers, expectedFP) – Pinning im Handshake: Pin gewinnt; ohne Pin nur mit Token-Fingerprint; ohne beides ErrUnknownServer (kein blindes TOFU). Enrollment-Token sltn_enroll_<secret>.<fingerprint> (NewEnrollmentToken/ParseEnrollmentToken); in den Store kommt nur Token.Secret. Paketname kollidiert mit Stdlib-crypto – bei Bedarf beim Import aliasen.

Tests werden zuerst als Vorgaben geschrieben, dann implementiert. Zeitabhängige Tests nutzen kurze Intervalle über die Konfigurationsfelder bzw. das injizierbare Client.sleep; mit -race -count=30 auf Stabilität prüfen.

go vet ./...
go test -race ./...                                   # alle Tests
go test -run TestFromLabels_Errors ./pkg/traefik      # einzelner Test
go test -cover ./...

Middleware-Optionen werden bewusst als unveränderte Label-Strings in verschachtelten Maps durchgereicht (Traefik parst sie beim Einlesen wie Labels); nur Router- und Service-Felder sind typisiert. Nicht unterstützte Labels (TCP/UDP, unbekannte Attribute) liefern ErrUnsupportedLabel statt still ignoriert zu werden.

Big Picture

Sultan ersetzt manuell gepflegtes Ingress-Routing (z. B. Nginx Proxy Manager): Backends deklarieren ihr Routing selbst (Traefik-Labels an Docker-Containern oder Drop-ins in /etc/sultan.d/*.conf für Bare-Metal/LAMP), ein Agent meldet das an einen zentralen Server, und Traefik auf dem Ingress-Knoten pollt daraus seine Konfiguration.

Single Binary in Go mit drei Subcommands:

  • sultan server – Registry, Stream-Hub (hält Agent-Verbindungen im RAM, Dead-Node-Timeout 90s), Policy-Engine, Traefik-HTTP-Provider
  • sultan agent – Watcher (Docker Events API auf traefik.*-Labels bzw. inotify auf /etc/sultan.d), State-Serializer (injiziert LAN- oder NetBird-IP 100.64.x.y als Backend-Ziel), Transport mit Reconnect + exponentiellem Backoff
  • sultan token – CLI für Schlüssel, Enrollment-Tokens, Lizenzausstellung

Ports / Endpunkte des Servers:

  • :8443 (TLS) – Control Plane, ausschließlich ausgehend vom Agent (NAT/CGNAT/DS-Lite-tauglich). Persistenter SSE-Stream (entschieden, siehe pkg/stream), 45s-Keepalive, Events werden sofort gepusht. Enrollment: POST /api/v1/enroll.
  • 127.0.0.1:8080 – GET /v1/traefik-config (Traefik pollt alle 2–5s; flache Labels aller aktiven Nodes → verschachteltes Traefik-JSON) und GET /metrics/checkmk (Checkmk Local-Check-Format).

Sicherheitsmodell (Kerninvarianten):

  • Server-TLS-Zertifikat self-signed (ECDSA P-256, TLS 1.3), Agent pinnt den SHA256-Fingerprint in known_servers; der Erstkontakt wird gegen den Fingerprint im Enrollment-Token geprüft (kein blindes TOFU). Kein Agent-Verkehr im Klartext.
  • Agent-Keys (Ed25519) werden nur lokal erzeugt und verlassen den Host nie; Enrollment tauscht Public Key + einmaligen Token gegen Mandanten-Zuordnung und signierten Capability-Lease. Danach ist jeder Heartbeat mit node.key signiert.
  • Jeder Agent-Payload wird serverseitig gegen die Mandanten-Policy geprüft: Hosts in Host(...)-Regeln müssen in allowed_domains stehen (sonst ablehnen + Audit-Log), Routen über max_routes werden verworfen.
  • Lizenzen (license.json) sind mit dem Ed25519-Server-Master-Key signiert.

Stream-Events (JSON: event_id, timestamp, type, node_id, payload): service_state (Agent→Server, vollständiger Label-Snapshot, nicht Delta), node_command (re_enroll/drain/maintenance), lease_renew, error.

Persistenz: SQLite unter <data-dir>/sultan.db (Default /var/lib/sultan), Tabellen tenants, nodes, enrollment_tokens (Tokens nur als Hash gespeichert).

Policy/Lizenzablauf: Mandanten-Status steuert das generierte Routing – Zahlungsverzug → Router auf Bezahlseite (402), Backend down/service_down → Statusseite 503 mit Retry-After: 60. Checkmk-Schwellen: WARN bei 30, CRIT bei 14 Tagen Restlaufzeit, bei Ablauf Umleitung auf Landingpage.

Unstimmigkeiten in der Spec

Bisherige Widersprüche sind aufgelöst und im Entscheidungslog der Spec (Abschnitt 8) dokumentiert. Neue Unklarheiten vor der Implementierung mit dem User klären statt raten.