# 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: # z. B. v0.1.1; :latest = letztes Release docker run --rm zot.chmln.de/sultan: 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 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 /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 `____@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 /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= ← 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. ### 3.4 Erreichbarkeit Traefik → Backend Der Agent meldet `http://:`. Prüfen vom **Ingress** aus: ```bash curl -sI http://: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 `:` 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).