Files
sultan/docs/hoster.md
T

184 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sultan für Hoster: Kunden-Onboarding (pro / enterprise)
Für Hoster, die Sultan als bezahlten Dienst anbieten: Du betreibst Ingress (Traefik + `sultan server`), der Kunde betreibt seine Backends mit `sultan agent`. Grundinstallation: siehe [community.md](community.md). Wartung: siehe [wartung.md](wartung.md).
> **Stand:** Der kommerzielle Teil (Lizenzen, Mahnwesen, Vertragsende) ist durch Tests abgedeckt, im echten Betrieb aber **noch nicht erprobt**. Bekannte Lücken stehen am Ende.
## Wer hat was?
| | Hoster | Kunde |
|---|---|---|
| Läuft dort | Traefik, `sultan server` | Docker-Host(s) mit `sultan agent` |
| Geheimnisse | `suldata/` des Servers: TLS-Schlüssel, **Master-Key** (signiert Lizenzen, Leases, Befehle) | `node.key` je Host; verlässt den Host nie, auch du siehst ihn nicht |
| Entscheidet | welche Domains, wie viele Routen, bis wann | welche Dienste wie geroutet werden (Labels) |
| Sieht | alle Routen, Knoten, Verstöße, Fristen; je Container Compose-Projekt und -Verzeichnis, Startzeit, Health; je Host FQDN, Betriebssystem, Docker-Version, NetBird-IP | nur sein eigenes Agent-Log |
Die Host- und Container-Angaben sammelt der Agent automatisch für Inventar und Störfall (wo läuft welche URL, in welchem Compose-Verzeichnis). Sag das dem Kunden; enthält ein Verzeichnisname Vertrauliches, sieht der Hoster es.
Der Kunde kann **nur Domains** aus seinem Mandanten beanspruchen und nur bis `max_routes`. Alles andere verwirft der Server mit Eintrag im Audit-Log. Kunden sehen sich gegenseitig nicht. Gleichnamige Router verschiedener Kunden kollidieren nicht, weil jeder Router in Traefik als `<mandant>__<knoten>__<router>` erscheint.
## 1. Vor dem Onboarding klären
Aus dem Vertrag gehen diese Werte in den Mandanten:
| Wert | Flag | Hinweise |
|---|---|---|
| Kennung | `--id` | `a–z 0–9 - _`, dauerhaft (erscheint in Routernamen, Logs, Monitoring). Z. B. Kundennummer. |
| Name, Kontakt | `--name`, `--email` | für dich (Listen, Benachrichtigungen) |
| Domains | `--domains` | `kunde.de` = genau diese; `*.kunde.de` = alle Subdomains, **ohne** `kunde.de` selbst → meist beides eintragen. Wildcard auf TLD (`*.de`) ist verboten. Umlaut-Domains als Punycode (`xn--…`). |
| Paket | `--tier pro` oder `enterprise` | Beide verhalten sich technisch derzeit **gleich** (Mahnwesen, Vertragsende). Der Unterschied ist vertraglich. `community` = kein Mahnwesen, läuft nicht ab. |
| Routen | `--max-routes` | Anzahl Router (jeder `traefik.http.routers.<name>` und `traefik.tcp.routers.<name>` zählt, auch Redirect-Router). `-1` = unbegrenzt. Überzählige werden alphabetisch verworfen. |
| Vertragsende | `--valid-until JJJJ-MM-TT` | Default: heute + 1 Jahr. **Danach sehen Besucher die Landingpage „Hosting abgelaufen“.** |
| Zusatzfunktionen | `--features` | `custom_middleware`, `websockets`, `tcp_stream`. Ohne `custom_middleware` sind nur Standard-Middlewares erlaubt (Redirects, Pfad-Umschreibung, Header, Kompression). BasicAuth, Rate-Limit, IP-Allowlist usw. brauchen das Feature, sonst wird der Router mit Verstoß abgelehnt. `chain` und `errors` sind nie erlaubt. `tcp_stream` erlaubt TLS-Passthrough (Kunde mit eigener CA/TLS, z. B. tinyCA): nur `HostSNI` mit einzelnen Hosts aus den Domains, nur Port 443; ab Paywall oder bei Wartung ist die Route ganz weg (keine Hinweisseite möglich). |
**Achtung:** Vertragsende, Domains und Paket lassen sich bisher **nicht nachträglich ändern** (`tenant update` fehlt, siehe Lücken). Deshalb `--valid-until` und `--domains` beim Anlegen sorgfältig setzen.
## 2. Netzwerk mit dem Kunden klären
Das ist der häufigste Grund für ein holpriges Onboarding. Zwei Wege müssen frei sein:
1. **Agent → Server:** ausgehend TCP 8443 vom Kunden-Host zu deiner `--public-url`. Meist ohnehin offen.
2. **Traefik → Kunden-Backend:** dein Ingress verbindet sich per HTTP auf den **veröffentlichten Port** des Containers. Dafür gibt es drei Möglichkeiten:
| Situation beim Kunden | Lösung |
|---|---|
| öffentliche IP / eigenes Rechenzentrum | Kunde gibt die Ports **nur für deine Ingress-IP(s)** frei |
| hinter NAT/CGNAT/DS-Lite | VPN/Overlay (z. B. NetBird): Agent mit `SULTAN_BACKEND_INTERFACE=wt0`; eventuell Einladung mit `--server-url https://<netbird-ip>:8443` |
| gleiches LAN wie der Ingress | nichts zu tun |
Vorgaben für den Kunden:
- Ports veröffentlichen (`"9080:80"`), **nicht** nur `127.0.0.1:9080:80`.
- Der Agent meldet als Backend die Adresse, über die er deinen Server erreicht. Das kann eine **IPv6-Adresse** sein. Dann muss dein Ingress IPv6 können, sonst setzt der Kunde `SULTAN_BACKEND_HOST`.
- Knoten-ID: Default ist der Hostname. Gleichnamige Hosts beim selben Kunden (z. B. `web01`) brauchen `SULTAN_NODE_ID`.
## 3. Mandant anlegen und einladen
```bash
cd /docker/sultan-server
docker compose exec sultan-server sultan token tenant add \
--id k10042 --name "Novesia Bonn" --email it@novesia-bonn.de \
--domains "novesia-bonn.de,www.novesia-bonn.de" \
--tier pro --max-routes 10 --valid-until 2027-10-31
docker compose exec sultan-server sultan token invite --tenant k10042 --ttl 7d
```
**Die Einladung (`sltn_enroll_…`):**
- **Ein Knoten pro Einladung.** Drei Hosts = drei Einladungen.
- Bis zur Benutzung geheim: Wer sie hat, kann einen Knoten für diesen Kunden anmelden. Danach ist sie wertlos. Übergabe über einen sicheren Kanal (Kundenportal, verschlüsselte Mail, Passwortmanager), **nicht** ins Ticket kopieren.
- Gespeichert wird nur ein Hash; du kannst sie später also nicht noch einmal anzeigen. Verloren oder abgelaufen → einfach neu erzeugen. Aufräumen: `sultan token purge`.
- Sie enthält Server-Adresse und Zertifikats-Fingerprint. Der Kunde braucht nichts weiter, kein CA-Zertifikat, keine IP-Liste. Mit einer gefälschten Adresse landet der Agent nie bei einem fremden Server (Pinning).
## 4. Was du dem Kunden schickst
Ein Paket aus:
1. Einladung (sicherer Kanal) und Gültigkeit.
2. Compose-Vorlage [deploy/compose/agent](../deploy/compose/agent) + Image `zot.chmln.de/sultan:<version>`.
3. Die Labels, passend zu **deinen** Traefik-Namen (Entrypoint, Certresolver):
```yaml
labels:
- "traefik.enable=true"
- "traefik.http.routers.shop.rule=Host(`www.novesia-bonn.de`) || Host(`novesia-bonn.de`)"
- "traefik.http.routers.shop.entrypoints=websecure"
- "traefik.http.routers.shop.tls.certresolver=le"
- "traefik.http.services.shop.loadbalancer.server.port=80"
```
4. DNS-Vorgabe: Domains per CNAME auf deinen Ingress (z. B. `web11.r25dc.de`). Erst dann klappt Let's Encrypt.
5. Freigabe aus Abschnitt 2 (Ports nur für deine Ingress-IP).
6. Die Wartungsregeln aus [wartung.md](wartung.md), vor allem: **`stop` statt `down`** und `sultan agent maintenance on`.
## 5. Abnahme
```bash
docker compose exec sultan-server sultan token node list --tenant k10042 # online, Lease bis …
curl -s 'http://127.0.0.1:8080/v1/traefik-config?format=yaml' | grep k10042 # Router + Backend-URL
curl -sI http://<backend-ip>:<port> # vom Ingress: Backend erreichbar?
curl -sI https://www.novesia-bonn.de # Zertifikat + Seite
docker compose logs sultan-server | grep -i verstoß # abgelehnte Domains/Routen?
```
Fehlt eine Route, steht der Grund im Server-Log („Policy-Verstoß“: fremde Domain, `HostRegexp`, Routenlimit, verbotene Middleware) oder im Agent-Log des Kunden („Container nicht übernommen“: Port nicht veröffentlicht).
## 6. Laufender Betrieb
### Zahlung und Mahnwesen
Sultan kennt keine Rechnungen, nur das **Fälligkeitsdatum einer offenen Zahlung**. Das trägst du (oder später dein Rechnungssystem) ein:
```bash
docker compose exec sultan-server sultan token tenant payment --id k10042 --overdue-since 2026-11-01
docker compose exec sultan-server sultan token tenant payment --id k10042 --paid
```
Daraus ergibt sich automatisch:
| Tage überfällig | Stufe | Besucher sehen |
|---|---|---|
| < 8 | – | normale Seite |
| ab 8 | watermark | Hinweisseite, nach 5 s weiter zur Seite (dann 60 min Ruhe) |
| ab 25 | firemark | wie oben, 10 s |
| ab 32 | paywall | **Seite offline**, Hinweisseite mit deinem Angebot (`--hoster-name`, `--hoster-url`) |
Mit `--paid` ist sofort (nächster Refresh, ≤ 1 min) alles normal. Bei Stufe 1 und 2 meldet Checkmk bzw. Slack/ntfy.
Sofort sperren, z. B. bei Zahlungsausfall oder Missbrauch:
```bash
sultan token tenant set-status --id k10042 --status suspended_payment # Paywall-Seite
sultan token tenant set-status --id k10042 --status quarantined # Routen weg, Agent-Anfragen abgelehnt
sultan token tenant set-status --id k10042 --status active # zurück
```
### Vertragsende
- Checkmk (`/metrics/checkmk`): **WARN 30**, **CRIT 14** Tage vor `valid_until`. Benachrichtigungen gehen einmal pro Frist raus.
- Ab `valid_until` wird der Mandant `expired`, Besucher sehen „Hosting abgelaufen“ (402). Die Leases enden spätestens zum Vertragsende.
- Verlängern: siehe Lücken.
### Monitoring
```bash
curl -s http://127.0.0.1:8080/metrics/checkmk # als Checkmk-Local-Check einbinden
```
Liefert pro kommerziellem Mandanten Vertragsfrist und Mahnstufe, pro Knoten online/offline/Wartung und Lease, dazu Policy-Verstöße der letzten 24 h. Zusätzlich Push per `SULTAN_NOTIFY_SLACK` / `SULTAN_NOTIFY_NTFY`: Knoten > 15 min offline, wieder online, Fristen, Mahnstufen. Policy-Verstöße erscheinen nur im Server-Log und als Zähler in Checkmk.
### Inventar (Accountant)
Betreibst du mehrere Ingress-Server, melden sie ihr Inventar an deinen `sultan accountant`: Mandanten, Knoten und je URL das Ziel (`<fqdn>:<port>`), Container, Compose-Verzeichnis, Health und das Produkt aus `sultan.accountant.billing`. Im Störfall:
```bash
sultan accountant where kunde-a.de # Backend <fqdn>:<port>, Container, Compose-Verzeichnis, seit wann, wo vorher
sultan accountant routes --tenant=k10042
```
Einrichtung: `sultan accountant invite --payee=<server>` auf dem Accountant, die Einladung als `SULTAN_ACCOUNTANT_TOKEN` in die `.env` des Ingress-Servers, Neustart. Danach liegen `payee.key` und `accountant.json` in `suldata/` (mitsichern).
Abrechnung: Kunden legt der Accountant aus den Mandanten automatisch an; du ergänzt Zahlweise, Pauschale, Gruppe und Notiz (`customer set`), pflegst den Produktkatalog (`product add|set|price`, Preise optional) und ordnest Posten zu (Label `sultan.accountant.billing` am Container oder `assign --host`). `sultan accountant billing --month=JJJJ-MM --out=…csv` liefert die Monatsliste; die Spalte „Hinweis“ zeigt Lücken (kein Kunde, nicht zugeordnet, Zahlweise offen) und Kündigungen mit tagesgenauer Gegenrechnung.
Checkmk auf dem Accountant-Host: `curl -s http://127.0.0.1:8082/metrics/checkmk` als Local Check einbinden. Je URL ein Service `Sultan URL <host>` (WARN bei gestoppt, Policy-Verstoß, unhealthy, Knoten getrennt; Wartung bleibt OK) mit Ziel `http://<fqdn>:<port>` und Compose-Verzeichnis im Text, je Ingress-Server `Sultan Payee <id>` (WARN ab 2 h ohne Bericht, CRIT ab 6 h). Den Text passt du mit `sultan accountant dump-checkmk` an.
### Sicherheit
- Verdacht auf kompromittierten Kunden-Host: `sultan token node drain --id …` (Routen sofort weg), danach `node delete` und neue Einladung für einen sauberen Host.
- Schlüssel des Knotens tauschen, ohne Neuanmeldung: `sultan token node command --id … --cmd rotate_key`.
- Dein `suldata/` (Master-Key!) regelmäßig verschlüsselt sichern. Wer den Master-Key hat, kann Lizenzen und Befehle für alle Kunden ausstellen.
### Kunde kündigt
```bash
sultan token node drain --id <knoten> # Routen sofort weg (für jeden Knoten)
sultan token tenant delete --id k10042 # löscht Mandant, Knoten und offene Einladungen
```
## Bekannte Lücken (Stand heute)
| Lücke | Folge | Notlösung |
|---|---|---|
| **`tenant update` fehlt** | Vertragsende, Domains, Paket, Routenlimit nicht änderbar | Keine saubere: `tenant delete` + neu anlegen löscht auch die Knoten (neue Einladungen nötig). Erste Priorität für den kommerziellen Betrieb. |
| pro = enterprise | kein technischer Unterschied | vertraglich regeln |
| Keine Admin-API | Zahlungsstatus nur per CLI | Skript über `docker compose exec` |
| Kommerzieller Betrieb nicht im Feld getestet | – | mit einem Testmandanten und kurzen Fristen (`--valid-until` in wenigen Tagen, `payment --overdue-since` in der Vergangenheit) durchspielen |
| `public_reason` einsprachig | Wartungstext für de und en gleich | – |