From bed742c3d065dba3c9dda22a58c8b2b6c41bc55b Mon Sep 17 00:00:00 2001
From: Bjoern Runden
Date: Fri, 2 Oct 2026 22:42:56 +0200
Subject: [PATCH] =?UTF-8?q?pkg=20f=C3=BCr=20traefik=20ist=20drin?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.ai/sultan-system-architecture.md | 4 ++--
CLAUDE.md | 2 +-
README.md | 11 +++++++----
deploy/.env.example | 10 ++++++++++
deploy/compose/agent/.env.example | 2 +-
deploy/compose/server/.env.example | 2 +-
deploy/push-image.sh | 28 ++++++++++++++++++++++------
7 files changed, 44 insertions(+), 15 deletions(-)
create mode 100644 deploy/.env.example
diff --git a/.ai/sultan-system-architecture.md b/.ai/sultan-system-architecture.md
index bf12689..00c7bf2 100644
--- a/.ai/sultan-system-architecture.md
+++ b/.ai/sultan-system-architecture.md
@@ -787,7 +787,7 @@ services:
| 2026-10-02 | **Leases und Befehle umgesetzt:** Lease mit eigenem Signaturpräfix; ungeplant getrennter Knoten zeigt die Wartungsseite bis Lease-Ende; Befehle über eine DB-Warteschlange (CLI ist eigener Prozess); alle Server-Events signiert; `rotate_key` mit Besitznachweis und abbruchsicherer Wiederaufnahme. | Kein 404 bei kurzen Ausfällen; keine Fernsteuerung ohne Server-Signatur; Schlüsselwechsel ohne Aussperren. |
| 2026-10-02 | **Schritt 3 umgesetzt:** Checkmk-Ausgabe als reine Funktion (`pkg/checkmk`); Job bei jedem Refresh (Ablauf, Benachrichtigung, Aufräumen); Benachrichtigungen Slack-kompatibel + ntfy mit Wiederholung bis zur erfolgreichen Zustellung; Community läuft nicht über `valid_until` ab. | Testbar, ohne zusätzlichen Prozess; keine verlorenen Alarme. |
| 2026-10-02 | **Mehrere Server-Adressen:** `--public-url` mehrfach; die Einladung trägt nur die erste; die Liste kommt per signiertem `config`-Befehl bei jedem Verbinden; der Agent weicht der Reihe nach aus und merkt sich die funktionierende Adresse. | Kurze Einladung; Ausfall einer Adresse (DNS, Weiterleitung) ohne Eingriff überbrückt. |
-| 2026-10-02 | **Build & Auslieferung:** ein distroless-Image (nonroot) für Server und Agent, Multi-Arch amd64+arm64 per buildx mit Go-Cross-Compile (kein QEMU); Registry ist die Container-Registry von Gitea (`git.everdc.eu/chmln-app-dev/sultan`), ein Vanity-Name (`pods.chmln.de/`) entsteht später per Traefik-Proxy mit `replacePathRegex` auf eine feste Gitea-Organisation (kein Redirect: Docker verwirft den `Authorization`-Header bei Host-Wechsel). Binaries + `SHA256SUMS` statisch unter `sultan/releases//`, `releases/stable` als Zeiger. Agent-Container mit `network_mode: host`. Vorerst manuell über `deploy/*.sh`, später Gitea Actions bei Tags. | Kein Go-Stack beim Nutzer; ein Image für alle Architekturen; Agent meldet sonst die Container-IP als Backend; Registry ohne zusätzlichen Dienst. |
+| 2026-10-02 | **Build & Auslieferung:** ein distroless-Image (nonroot) für Server und Agent, Multi-Arch amd64+arm64 per buildx mit Go-Cross-Compile (kein QEMU); Registry: eigener **Zot**-Server `pod.chmln.de`, Image `pod.chmln.de/sultan` (ohne Owner-Ebene, daher kein Rewrite-Proxy nötig); Zugangsdaten für den Push nur in `deploy/.env` (gitignored), ohne Passwort bricht `push-image.sh` ab. (Ursprünglich geplant: Gitea-Registry hinter Rewrite-Proxy – verworfen.) Binaries + `SHA256SUMS` statisch unter `sultan/releases//`, `releases/stable` als Zeiger. Agent-Container mit `network_mode: host`. Vorerst manuell über `deploy/*.sh`, später Gitea Actions bei Tags. | Kein Go-Stack beim Nutzer; ein Image für alle Architekturen; Agent meldet sonst die Container-IP als Backend; eigene Registry mit kurzen Image-Namen ohne Proxy-Tricks. |
| 2026-10-02 | **Kleinere Punkte:** Rate-Limit `/enroll` 10/min/IP vor der Signaturprüfung (`-1` = aus); `SULTAN_` für alle Flags, Flags gewinnen; „wieder online“ nur nach Offline-Alarm; Checkmk-Zeiten in Server-Zeitzone. Ein Agent, dessen Enrollment mit 429 scheitert, beendet sich; der Neustart erfolgt über systemd bzw. Docker `restart`. | Schutz vor Durchprobieren; Container-/systemd-freundliche Konfiguration; vollständige Alarmkette. |
| geplant | Geordneter Zertifikatswechsel; optionale Root-CA. (Signierte Server-Events: umgesetzt.) | Zweite Sicherheitsschicht neben dem TLS-Pin; Betrieb ohne Aussperren. |
@@ -865,7 +865,7 @@ services:
**Schritt 5 – Betrieb und Installationsskript** (teilweise umgesetzt: `Dockerfile`, `deploy/build.sh`, `deploy/push-image.sh`, `deploy/publish-static.sh`, `deploy/compose/`)
* Strukturiertes Logging (`log/slog`, umgesetzt), Graceful Shutdown (umgesetzt), Versionsinfo
* ✅ Dockerfile (statisch, distroless, Multi-Arch), Build-/Deploy-Skripte, Compose-Vorlagen
-* offen: systemd-Unit, `install.sh`, Signatur von `SHA256SUMS` (Ed25519, Schlüssel nur im Runner), Gitea Actions (Tests bei Push, Release bei Tag `v*`), Image-Digest in der veröffentlichten Compose-Vorlage, Vanity-Proxy `pods.chmln.de`, optional Spiegel auf ghcr.io
+* offen: systemd-Unit, `install.sh`, Signatur von `SHA256SUMS` (Ed25519, Schlüssel nur im Runner), Gitea Actions (Tests bei Push, Release bei Tag `v*`), Image-Digest in der veröffentlichten Compose-Vorlage, optional Spiegel auf ghcr.io
* Installationsskript für `curl | sh`: Herkunft mit öffentlichem Zertifikat und/oder Prüfsumme bzw. Signatur. Die eigentliche Vertrauensfrage ist das Skript, nicht der Token.
**Weitere offene Bausteine (ohne feste Reihenfolge):**
diff --git a/CLAUDE.md b/CLAUDE.md
index a6fd126..978422c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -34,7 +34,7 @@ Go-Modul `sultan` (Go 1.26). `main.go` ist nur eine Hülle um `pkg/cli`; umgeset
**Mehrere Adressen (Schritt 4):** `Config.PublicURLs` (gemerkt als `settings.public_urls`, `server.LoadPublicURLs`), Versand per `config`-Befehl in `nodeOnline`; Agent: `Identity.ServerURLs`, `serverCandidates`/`nextServerURL` (Wechsel in `OnError` des Stream-Clients), `a.serverURL` nur über `currentServerURL()` lesen.
-- [deploy/](deploy) – Auslieferung: `Dockerfile` im Root (distroless, nonroot 65532, Multi-Arch, kein `ENTRYPOINT`, `WORKDIR /`), `build.sh` (Binaries amd64/arm64 + `SHA256SUMS` nach `dist//`), `push-image.sh` (buildx → Gitea-Registry `git.everdc.eu/chmln-app-dev/sultan`), `publish-static.sh` (rsync nach `sultan/releases//`), `compose/{server,agent}` (Agent mit `network_mode: host`). Version überall aus `git describe`; `-dirty` wird nicht veröffentlicht. `.dockerignore` hält `suldata/`, `.ai/`, `CLAUDE.md` aus dem Build-Kontext.
+- [deploy/](deploy) – Auslieferung: `Dockerfile` im Root (distroless, nonroot 65532, Multi-Arch, kein `ENTRYPOINT`, `WORKDIR /`), `build.sh` (Binaries amd64/arm64 + `SHA256SUMS` nach `dist//`), `push-image.sh` (liest `deploy/.env` mit `REGISTRY`/`REGISTRY_USER`/`REGISTRY_PASSWORD`, Vorlage `deploy/.env.example`; buildx → Zot-Registry `pod.chmln.de/sultan`; ohne Passwort Exit 1), `publish-static.sh` (rsync nach `sultan/releases//`), `compose/{server,agent}` (Agent mit `network_mode: host`). Version überall aus `git describe`; `-dirty` wird nicht veröffentlicht. `.dockerignore` hält `suldata/`, `.ai/`, `CLAUDE.md` aus dem Build-Kontext.
**CLI-Umgebung:** `pkg/cli/env.go` – `annotateEnv`/`applyEnv` machen jedes Flag als `SULTAN_` setzbar (nach `fs.Parse`, nur für nicht gesetzte Flags; Listen-Flags `stringList` kommagetrennt). Neue Flags bekommen das automatisch. Rate-Limit: `pkg/server/ratelimit.go` (`EnrollLimit`, `-1` = aus).
diff --git a/README.md b/README.md
index d76ee54..13fc6ef 100644
--- a/README.md
+++ b/README.md
@@ -67,15 +67,18 @@ docker run --rm sultan:latest sultan version
Das Image ist distroless (nur das statische Binary, ~17 MB) und läuft als nonroot-User `65532`. Die Rolle wählt der Befehl: `sultan server …` bzw. `sultan agent …`.
-### 4. Image in die Gitea-Registry schieben
+### 4. Image in die Registry schieben (Zot, `pod.chmln.de`)
```bash
-docker login git.everdc.eu # einmalig: Gitea-User + Token mit Scope write:package
-deploy/push-image.sh # → git.everdc.eu/chmln-app-dev/sultan:
+cp deploy/.env.example deploy/.env # einmalig; gitignored – nie einchecken!
+$EDITOR deploy/.env # REGISTRY_PASSWORD eintragen (Pflicht)
+deploy/push-image.sh # → pod.chmln.de/sultan:
LATEST=1 deploy/push-image.sh # zusätzlich :latest (nur für Releases)
```
-Baut per `docker buildx` gleichzeitig für `linux/amd64` und `linux/arm64` (Go kompiliert quer, kein QEMU nötig) und verweigert Versionen mit `-dirty`. Anpassbar über `REGISTRY`, `OWNER`, `PLATFORMS`.
+Das Skript liest `deploy/.env` (Defaults: `REGISTRY=pod.chmln.de`, `REGISTRY_USER=sultan`, `IMAGE_NAME=sultan`; ohne `REGISTRY_PASSWORD` Abbruch), meldet sich per `docker login --password-stdin` an und baut per `docker buildx` gleichzeitig für `linux/amd64` und `linux/arm64` (Go kompiliert quer, kein QEMU nötig). Versionen mit `-dirty` werden verweigert. Weitere Stellschrauben: `PLATFORMS`, `ENV_FILE`.
+
+Danach auf den Hosts: `SULTAN_IMAGE=pod.chmln.de/sultan:` in der Compose-`.env` und `docker compose pull && docker compose up -d`.
### 5. Binaries auf den statischen Webhost legen
diff --git a/deploy/.env.example b/deploy/.env.example
new file mode 100644
index 0000000..98cf050
--- /dev/null
+++ b/deploy/.env.example
@@ -0,0 +1,10 @@
+# Zugangsdaten für deploy/push-image.sh – als deploy/.env kopieren (gitignored, nie einchecken!)
+
+# Container-Registry (Zot)
+REGISTRY=pod.chmln.de
+REGISTRY_USER=sultan
+# Pflicht – ohne Passwort bricht push-image.sh ab
+REGISTRY_PASSWORD=
+
+# Image-Name in der Registry → ${REGISTRY}/${IMAGE_NAME}:
+IMAGE_NAME=sultan
diff --git a/deploy/compose/agent/.env.example b/deploy/compose/agent/.env.example
index 0130925..8bd5107 100644
--- a/deploy/compose/agent/.env.example
+++ b/deploy/compose/agent/.env.example
@@ -1,6 +1,6 @@
# Image: lokal gebaut (docker build -t sultan:latest .) oder aus der Registry
SULTAN_IMAGE=sultan:latest
-# SULTAN_IMAGE=git.everdc.eu/chmln-app-dev/sultan:v0.1.0
+# SULTAN_IMAGE=pod.chmln.de/sultan:v0.1.0
# Einladung vom Server: docker compose exec sultan-server sultan token invite --tenant=…
SULTAN_TOKEN=
diff --git a/deploy/compose/server/.env.example b/deploy/compose/server/.env.example
index 88e87aa..b82dde2 100644
--- a/deploy/compose/server/.env.example
+++ b/deploy/compose/server/.env.example
@@ -1,6 +1,6 @@
# Image: lokal gebaut (docker build -t sultan:latest .) oder aus der Registry
SULTAN_IMAGE=sultan:latest
-# SULTAN_IMAGE=git.everdc.eu/chmln-app-dev/sultan:v0.1.0
+# SULTAN_IMAGE=pod.chmln.de/sultan:v0.1.0
# Adresse, unter der Agents den Server erreichen – steckt danach in jeder Einladung
SULTAN_PUBLIC_URL=https://webproxy11.example.de:8443
diff --git a/deploy/push-image.sh b/deploy/push-image.sh
index 29893a1..c336f06 100755
--- a/deploy/push-image.sh
+++ b/deploy/push-image.sh
@@ -1,21 +1,35 @@
#!/usr/bin/env bash
# Baut das Multi-Arch-Image (amd64 + arm64) und schiebt es in die
-# Container-Registry von Gitea.
+# Container-Registry (Zot).
#
-# docker login git.everdc.eu # einmalig, Gitea-User + Token mit write:package
+# cp deploy/.env.example deploy/.env && $EDITOR deploy/.env # einmalig: REGISTRY_PASSWORD
# deploy/push-image.sh # Tag aus `git describe`
# LATEST=1 deploy/push-image.sh # zusätzlich :latest setzen (nur für Releases)
#
-# Ergebnis: ${REGISTRY}/${OWNER}/sultan: (und ggf. :latest)
+# Ergebnis: ${REGISTRY}/${IMAGE_NAME}: (und ggf. :latest)
set -euo pipefail
cd "$(dirname "$0")/.."
-REGISTRY="${REGISTRY:-git.everdc.eu}"
-OWNER="${OWNER:-chmln-app-dev}"
-IMAGE="${REGISTRY}/${OWNER}/sultan"
+ENV_FILE="${ENV_FILE:-deploy/.env}"
+if [ -f "$ENV_FILE" ]; then
+ set -a
+ # shellcheck disable=SC1090
+ . "$ENV_FILE"
+ set +a
+fi
+
+REGISTRY="${REGISTRY:-pod.chmln.de}"
+REGISTRY_USER="${REGISTRY_USER:-sultan}"
+IMAGE_NAME="${IMAGE_NAME:-sultan}"
+IMAGE="${REGISTRY}/${IMAGE_NAME}"
VERSION="${VERSION:-$(git describe --tags --always --dirty)}"
PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}"
+if [ -z "${REGISTRY_PASSWORD:-}" ]; then
+ echo "REGISTRY_PASSWORD fehlt – in ${ENV_FILE} setzen (Vorlage: deploy/.env.example)." >&2
+ exit 1
+fi
+
case "$VERSION" in
*-dirty)
echo "Arbeitsverzeichnis hat uncommittete Änderungen (${VERSION}) – erst committen." >&2
@@ -23,6 +37,8 @@ case "$VERSION" in
;;
esac
+printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
+
tags=(-t "${IMAGE}:${VERSION}")
if [ "${LATEST:-0}" = 1 ]; then
tags+=(-t "${IMAGE}:latest")