Files
brandClaude Opus 5.5 c2d6dd694f sultan token tenant update: Mandanten nachträglich ändern
Weitere Compose-Stacks eines Kunden sind der Regelfall; bisher ließen sich
Domains und Routenlimit nur per delete + neu anlegen (und damit ohne Knoten)
ändern. Die Änderung liegt bewusst nur auf dem Server: Ein Agent darf sich
keine Domains selbst freigeben.

- Domains und Features werden hinzugefügt/entfernt statt ersetzt; unbekannter
  Eintrag beim Entfernen ist ein Fehler, die letzte Domain bleibt
- Verlängerung in die Zukunft setzt expired auf active (der Ablauf-Job setzt
  das nie zurück); Quarantäne und Zahlungsverzug bleiben
- Wechsel auf kommerziell mit vergangenem valid_until verlangt --valid-until

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 18:32:28 +01:00

308 lines
13 KiB
Markdown
Raw Permalink 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 im Eigenbetrieb (Community)
Anleitung für alle, die Sultan selbst betreiben: ein Ingress-Host mit Traefik und `sultan server`, dazu beliebig viele Docker-Hosts mit `sultan agent`. Das betrifft Homelab, kleine Firmen und einen Hoster in eigener Sache.
```text
Besucher ──▶ Traefik ──(pollt alle 3 s)──▶ sultan server ◀──(TLS, ausgehend)── sultan agent ── Docker
│ │
└──────────────── HTTP zum veröffentlichten Port ──────────────────┘
```
Zwei Verbindungen müssen funktionieren:
1. **Agent → Server**, TCP 8443 (TLS 1.3). Nur ausgehend vom Agent, funktioniert also auch hinter NAT/CGNAT.
2. **Traefik → Backend**, auf den *veröffentlichten* Port des Containers, z. B. `9080` bei `ports: "9080:80"`. Das ist normales HTTP. Der Ingress muss den Backend-Host also erreichen können: per LAN, öffentlicher IP oder VPN (z. B. NetBird).
---
## 1. Image holen
Server und Agent nutzen dasselbe Image. Die Rolle bestimmt der Befehl.
```bash
docker pull zot.chmln.de/sultan:<version> # z. B. v0.1.1; :latest = letztes Release
docker run --rm zot.chmln.de/sultan:<version> sultan version
```
Auf Servern eine **feste Version** in die `.env` schreiben (`SULTAN_IMAGE=…:v0.1.1`). `latest` wird bei `docker compose up` nicht neu geladen; für ein Update ist immer `docker compose pull` nötig.
Update später:
```bash
$EDITOR .env # SULTAN_IMAGE auf neue Version
docker compose pull && docker compose up -d
docker compose exec <dienst> sultan version
```
---
## 2. Server (Ingress-Host, neben Traefik)
Vorlage: [deploy/compose/server](../deploy/compose/server).
### 2.1 Wie läuft Traefik bei dir?
Davon hängt ab, wie Sultan neben Traefik läuft:
| Traefik läuft mit … | Sultan-Server | Traefik erreicht Sultan unter |
|---|---|---|
| `network_mode: host` | **Variante A**: ebenfalls `network_mode: host` (Vorlage) | `http://127.0.0.1:8080` |
| eigenem Docker-Netz (z. B. `traefik-public`) | **Variante B**: im selben bzw. einem gemeinsamen internen Netz | `http://sultan-server:8080` |
**Sicherheitsregel für beide Varianten:** Port 8080 (Traefik-Konfiguration, Checkmk) und 8081 (Wartungsseiten) sind **Klartext und ohne Anmeldung**. Sie dürfen nur für Traefik erreichbar sein, nie aus dem Internet und nicht für fremde Container. Öffentlich ist nur 8443.
### 2.2 Variante A: `network_mode: host` (empfohlen, Vorlage)
```bash
mkdir -p /docker/sultan-server && cd /docker/sultan-server
cp <repo>/deploy/compose/server/{compose.yaml,.env.example,traefik-provider.yml} .
cp .env.example .env && $EDITOR .env
install -d -m 0700 -o 65532 -g 65532 suldata # nonroot-User im Image
```
In der `.env` mindestens:
```bash
SULTAN_IMAGE=zot.chmln.de/sultan:v0.1.1
SULTAN_PUBLIC_URL=https://ingress.example.de:8443 # so erreichen Agents den Server; landet in jeder Einladung
```
**Ports prüfen:** Traefiks API/Dashboard belegt oft schon `8080`.
```bash
ss -ltnp | grep -E ':(8443|8080|8081)\b'
# belegt → in .env z. B.:
# SULTAN_PROVIDER_ADDR=127.0.0.1:8085
# SULTAN_LANDER_ADDR=127.0.0.1:8086
```
Starten und prüfen:
```bash
docker compose up -d
docker compose logs sultan-server # "Sultan-Server gestartet" + Fingerprint
docker compose exec sultan-server sultan version # Version + Zeitzone
curl -s http://127.0.0.1:8080/healthz # ok
```
### 2.3 Variante B: Traefik in einem Docker-Netz
Sultan muss hier auf allen Interfaces *im Container* lauschen und Traefik den Lander unter dem Containernamen nennen. Am besten legt man ein **eigenes internes Netz** nur für Traefik und Sultan an, statt `traefik-public` mitzunutzen: Sonst könnte jeder Container in `traefik-public` die Konfiguration (Backend-Adressen) lesen.
```yaml
# compose.yaml (Variante B) – statt network_mode: host
services:
sultan-server:
image: ${SULTAN_IMAGE}
container_name: sultan-server
restart: unless-stopped
command: ["sultan", "server", "--data-dir=/suldata"]
ports:
- "8443:8443" # nur der Agent-Port nach außen
environment:
SULTAN_AGENT_ADDR: ":8443"
SULTAN_PROVIDER_ADDR: ":8080" # nur im Netz sultan-internal erreichbar
SULTAN_LANDER_ADDR: ":8081"
SULTAN_INTERCEPTOR_URL: "http://sultan-server:8081" # so erreicht Traefik die Wartungsseiten
SULTAN_PUBLIC_URL: ${SULTAN_PUBLIC_URL}
TZ: Europe/Berlin
volumes:
- ./suldata:/suldata
networks: [sultan-internal]
networks:
sultan-internal:
name: sultan-internal # Traefik zusätzlich an dieses Netz hängen
```
Im Traefik-Compose: `networks: [traefik-public, sultan-internal]` (plus `sultan-internal: {external: true}`).
Unterschiede zu Variante A:
- `curl` vom Host aus geht nicht. Prüfen per `docker compose exec traefik wget -qO- http://sultan-server:8080/healthz`.
- Die Client-IP der Agents (Rate-Limit, `node list`) ist meist korrekt. Bei IPv6 oder aktivem `userland-proxy` kann dort aber die Docker-Gateway-IP stehen.
### 2.4 Traefik anbinden
In die **statische** Konfiguration (`traefik.yml`), zusätzlich zu den vorhandenen Providern:
```yaml
providers:
http:
endpoint: "http://127.0.0.1:8080/v1/traefik-config" # Variante B: http://sultan-server:8080/…
pollInterval: "3s"
```
Danach Traefik **neu starten** (statische Konfiguration). Im Dashboard taucht der Provider `http` auf. Routen von Sultan heißen dort `<mandant>__<knoten>__<router>@http`.
Wichtig: Die Labels an den Diensten verweisen auf **Namen aus deiner Traefik-Konfiguration**: Entrypoints (`web`, `websecure`) und Certresolver (`le`). Sultan prüft nicht, ob es sie gibt; Traefik meldet Tippfehler im eigenen Log.
### 2.5 Firewall
- TCP **8443** eingehend von den Agent-Hosts (bzw. aus dem Internet, wenn Agents von überall kommen).
- 8080/8081 **nicht** öffnen.
- Ausgehend vom Ingress zu den Backend-Ports (siehe 3.4).
### 2.6 Mandant und Einladung
Auch im Eigenbetrieb gehört jeder Knoten zu einem Mandanten. Der Mandant legt fest, welche Domains seine Dienste beanspruchen dürfen.
```bash
docker compose exec sultan-server sultan token tenant add \
--id homelab --name "Mein Homelab" --email ich@example.de \
--domains "example.de,*.example.de" --max-routes -1
docker compose exec sultan-server sultan token invite --tenant homelab # 24 h gültig, einmalig
```
- `--tier` ist im Eigenbetrieb `community` (Default): kein Mahnwesen, kein Ablauf.
- `--max-routes -1` = unbegrenzt (Default 5).
- Domains: `example.de` deckt **nur** genau diese Domain. `*.example.de` deckt alle Subdomains, aber **nicht** `example.de` selbst; für beides braucht es beide Einträge.
Die Ausgabe (`sltn_enroll_…`) ist die Einladung. Sie enthält Server-Adresse und Zertifikats-Fingerprint, ist also alles, was der Agent braucht. Sie ist **geheim** bis zur Benutzung und danach wertlos.
### 2.7 Sichern
`suldata/` enthält Datenbank, TLS-Schlüssel und Master-Key. Ohne diese Dateien müssen sich alle Agents neu anmelden.
```bash
tar -C /docker/sultan-server -czf sultan-backup-$(date +%F).tgz suldata # verschlüsselt ablegen!
```
---
## 3. Agent (Docker-Host)
Vorlage: [deploy/compose/agent](../deploy/compose/agent).
### 3.1 Warum `network_mode: host`
Der Agent läuft **immer** mit `network_mode: host`. Ein Traefik-Netz braucht er nicht, denn er spricht nur ausgehend mit dem Server. Das host-Netz brauchen wir aus zwei Gründen:
- Er meldet als Backend-Adresse die **IP des Hosts**, über die er den Server erreicht. In einem Bridge-Netz wäre das die nutzlose Container-IP `172.x`.
- Die Knoten-ID kommt aus dem **Hostnamen** des Hosts statt aus der zufälligen Container-ID.
### 3.2 Einrichten
```bash
mkdir -p /docker/sultan-agent && cd /docker/sultan-agent
cp <repo>/deploy/compose/agent/{compose.yaml,.env.example} .
cp .env.example .env && $EDITOR .env
install -d -m 0700 -o 65532 -g 65532 suldata
```
`.env`:
```bash
SULTAN_IMAGE=zot.chmln.de/sultan:v0.1.1
SULTAN_TOKEN=sltn_enroll_… # Einladung aus 2.6; nach dem ersten Start egal
DOCKER_GID=999 # getent group docker | cut -d: -f3
# SULTAN_NODE_ID=dockweb11 # optional, sonst aus dem Hostnamen
# SULTAN_BACKEND_INTERFACE=wt0 # optional: Traefik erreicht den Host über NetBird
# SULTAN_BACKEND_HOST=192.168.1.20 # optional: Adresse fest vorgeben
```
```bash
docker compose up -d
docker compose logs -f sultan-agent
# Registriert node=… tenant=…
# Agent gestartet … backend=<IP> ← diese IP muss Traefik erreichen können!
# Mit Server verbunden / Lease erhalten
```
Danach `suldata/` behalten. Darin liegt der Schlüssel des Knotens (`node.key`); er verlässt den Host nie. Wer `suldata/` löscht, braucht eine neue Einladung.
### 3.3 Dienste freigeben: Labels am Container
Es sind dieselben Labels wie bei Traefiks Docker-Provider:
```yaml
services:
web:
image: nginx
ports:
- "9080:80" # muss veröffentlicht sein – nicht nur 127.0.0.1:9080:80
labels:
- "traefik.enable=true"
- "traefik.http.routers.meinweb.rule=Host(`www.example.de`) || Host(`example.de`)"
- "traefik.http.routers.meinweb.entrypoints=websecure"
- "traefik.http.routers.meinweb.tls.certresolver=le"
- "traefik.http.services.meinweb.loadbalancer.server.port=80" # Port IM Container
```
- Der Agent rechnet den Container-Port `80` selbst auf den veröffentlichten Port `9080` um.
- Fehlt das Port-Label und ist genau ein Port veröffentlicht, nimmt er diesen.
- Änderungen (Start, Stop, neue Labels) kommen innerhalb einer Sekunde bei Traefik an; Neustarts sind nicht nötig.
- Nicht übernommene Container stehen mit Grund im Agent-Log („Container nicht übernommen“). Fremde Domains landen im Server-Log („Policy-Verstoß“).
#### TLS-Passthrough (eigene CA, z. B. tinyCA)
Soll TLS erst beim Backend enden, braucht der Mandant das Feature `tcp_stream` (`tenant add … --features tcp_stream`). Dann:
```yaml
ports:
- "9000:9000"
labels:
- "traefik.enable=true"
- "traefik.tcp.routers.ca.rule=HostSNI(`ca.lab.example.de`)"
- "traefik.tcp.routers.ca.entrypoints=websecure"
- "traefik.tcp.routers.ca.tls.passthrough=true"
- "traefik.tcp.services.ca.loadbalancer.server.port=9000"
```
- Nur einzelne Hosts in `HostSNI` (mehrere mit `||`). ``HostSNI(`*`)``, `*.lab.example.de` und `HostSNIRegexp` werden abgelehnt, auch wenn das DNS eine Wildcard hat. In den Mandanten-Domains darf `*.lab.example.de` stehen.
- Nur Entrypoint `websecure` (443). Andere Hosts auf 443 laufen weiter normal über HTTP.
- Jeder TCP-Router zählt als Route.
- Bei Wartung oder gestopptem Container gibt es keine Wartungsseite, die Route ist dann weg.
#### Weiterer Stack oder weitere URL für denselben Mandanten
Einen neuen Compose-Stack neben dem bestehenden startest du einfach mit seinen Labels wie oben. Der Agent findet ihn selbst; neue Einladung, Agent-Neustart und Änderung am Agent sind nicht nötig. Router-Namen müssen auf dem Knoten eindeutig sein, und jeder Router zählt gegen `--max-routes`.
Nur wenn die neue Domain noch nicht abgedeckt ist oder das Routenlimit nicht reicht, änderst du den Mandanten **auf dem Server**:
```bash
docker compose exec sultan-server sultan token tenant update --id homelab --add-domains "neu.example.org,*.neu.example.org"
docker compose exec sultan-server sultan token tenant update --id homelab --max-routes 20
```
Das wirkt ohne Neustart beim nächsten Refresh (bis zu 1 Minute). Eine zuvor abgelehnte Route erscheint dann von selbst. Domains und Limit legt bewusst nur der Server fest: Ein Agent darf sich keine Domains selbst freigeben.
### 3.4 Erreichbarkeit Traefik → Backend
Der Agent meldet `http://<backend-ip>:<veröffentlichter Port>`. Prüfen vom **Ingress** aus:
```bash
curl -sI http://<backend-ip>:9080 -H 'Host: www.example.de'
```
Klappt das nicht: Firewall des Backend-Hosts (Port nur für die Ingress-IP freigeben), falsche Adresse (dann `SULTAN_BACKEND_HOST`/`SULTAN_BACKEND_INTERFACE`) oder IPv6. Der Agent nimmt die Adresse, über die er den Server erreicht, das kann eine IPv6-Adresse sein. Dann muss auch Traefik per IPv6 dorthin kommen.
### 3.5 Ohne Docker (`/etc/sultan.d`)
Für Bare-Metal-Dienste beschreibt man die Dienste als YAML in `/etc/sultan.d/*.yaml` (Format siehe README). Im Compose dafür `--config-dir=none` aus `command` entfernen und `/etc/sultan.d:/etc/sultan.d:ro` mounten.
Als systemd-Dienst ohne Docker ist der Agent vorgesehen, aber **noch nicht getestet**.
---
## 4. Kontrolle
```bash
# Server:
docker compose exec sultan-server sultan token node list # online, Lease bis …
curl -s 'http://127.0.0.1:8080/v1/traefik-config?format=yaml' # was Traefik bekommt
curl -s http://127.0.0.1:8080/metrics/checkmk # Monitoring
# von außen:
curl -sI https://www.example.de
```
Typische Fehler:
| Symptom | Ursache |
|---|---|
| Route fehlt in `traefik-config` | Container nicht `traefik.enable=true`, Port nicht veröffentlicht, Domain nicht in `--domains` (Server-Log „Policy-Verstoß“) |
| Route da, Traefik meldet 502/504 | Traefik erreicht `<backend-ip>:<port>` nicht (Firewall, IPv6) |
| Kein Zertifikat | DNS zeigt nicht auf den Ingress, Certresolver-Name falsch |
| 404 statt Wartungsseite | Container wurde *entfernt* statt gestoppt, siehe [Wartung](wartung.md) |
Wartung, Neustarts und Updates: [docs/wartung.md](wartung.md).