275 lines
12 KiB
Markdown
275 lines
12 KiB
Markdown
# 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ß“).
|
||
|
||
### 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).
|