# shellcheck shell=sh # # Traefik: Binary plus systemd-Unit. **Kein Docker auf dem Hypervisor** (Handoff # §1) — nftables bleibt einziger Eigentümer der Host-Firewallregeln, und ein # Docker-Dienst brächte seine eigene Kette mit. # # --------------------------------------------------------------------------- # Die Namen, die passen müssen # --------------------------------------------------------------------------- # # `SshTraefikWriter::render()` gibt aus, was der Routen-Endpunkt später liefert, # und die statische Konfiguration hier muss dazu passen. Nachgelesen, nicht # geraten: # # entryPoints: ["websecure"] # tls.certResolver: "letsencrypt" # service -> loadBalancer.servers[].url: "http://:80" # # Heißt der Entrypoint hier anders, laufen die Router ins Leere und Traefik # meldet trotzdem einen sauberen Start. # # --------------------------------------------------------------------------- # Reihenfolge: der Token kommt erst später # --------------------------------------------------------------------------- # # Der `http`-Provider braucht den dauerhaften Host-Token, und den gibt es erst # mit `POST /host/register` in Abschnitt `registered`. Die statische # Konfiguration wird deshalb ZWEIMAL geschrieben: hier mit leerem Token, damit # der Dienst steht und 80/443 belegt sind, und in Task 9 noch einmal mit dem # echten. Erst dort lässt sich beweisen, dass eine Routentabelle wirklich # abgerufen wurde. CLUPILOT_TRAEFIK_BIN='/usr/local/bin/traefik' CLUPILOT_TRAEFIK_DIR='/etc/traefik' CLUPILOT_TRAEFIK_TOKEN_FILE='/etc/traefik/host-token' CLUPILOT_TRAEFIK_API='http://127.0.0.1:8080' # Neueste stabile v3 aus der Release-Liste. # # Nicht fest verdrahtet, aus demselben Grund wie bei der ISO: eine feste Nummer # ist in ein paar Monaten eine 404 mitten in einer Übernahme. discover_traefik_version() { _json="$(http_get 'https://api.github.com/repos/traefik/traefik/releases/latest' || true)" [ -n "$_json" ] || return 1 printf '%s' "$_json" \ | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\(v3[0-9.]*\)".*/\1/p' \ | head -1 } # Lädt das Binary und prüft es gegen die Prüfsummendatei der Ausgabe. # # Dieselbe Begründung wie bei der ISO: was auf 80 und 443 lauscht und die # Zertifikate aller Kunden hält, wird nicht ungeprüft aus dem Netz übernommen. install_traefik_binary() { _version="$1" _plain="${_version#v}" _base="https://github.com/traefik/traefik/releases/download/${_version}" _archive="traefik_${_version}_linux_amd64.tar.gz" _sums="traefik_${_version}_checksums.txt" _tmp="${CLUPILOT_WORK_DIR}/traefik" rm -rf "$_tmp" mkdir -p "$_tmp" if ! http_download "${_base}/${_archive}" "${_tmp}/${_archive}"; then log "Traefik ${_version} nicht ladbar" return 1 fi _sumfile="$(http_get "${_base}/${_sums}" || true)" if [ -z "$_sumfile" ]; then log "Prüfsummendatei ${_sums} nicht abrufbar" return 1 fi _expected="$(printf '%s\n' "$_sumfile" | awk -v n="$_archive" '$2 == n || $2 == "*" n { print $1; exit }')" if [ -z "$_expected" ]; then log "keine Prüfsumme für ${_archive}" return 1 fi _actual="$(sha256sum "${_tmp}/${_archive}" | awk '{ print $1 }')" if [ "$_actual" != "$_expected" ]; then log "Traefik-Prüfsumme stimmt nicht: erwartet ${_expected}, bekommen ${_actual}" return 1 fi tar xzf "${_tmp}/${_archive}" -C "$_tmp" traefik install -m 755 "${_tmp}/traefik" "$CLUPILOT_TRAEFIK_BIN" rm -rf "$_tmp" log "Traefik ${_plain} installiert, Prüfsumme stimmt" return 0 } # Die statische Konfiguration. # # `token` darf leer sein — dann steht der Dienst, holt aber keine Tabelle. Das # ist der Zustand zwischen Abschnitt 7 und Abschnitt 9. write_traefik_static_config() { _token="${1:-}" _endpoint="${CLUPILOT_API}/host/routes" mkdir -p "$CLUPILOT_TRAEFIK_DIR" chmod 755 "$CLUPILOT_TRAEFIK_DIR" # acme.json hält die privaten Schlüssel aller Kundenzertifikate. Traefik # weigert sich, sie zu benutzen, wenn die Rechte weiter sind als 600 — und # das ist gut so. if [ ! -f "${CLUPILOT_TRAEFIK_DIR}/acme.json" ]; then : > "${CLUPILOT_TRAEFIK_DIR}/acme.json" fi chmod 600 "${CLUPILOT_TRAEFIK_DIR}/acme.json" cat > "${CLUPILOT_TRAEFIK_DIR}/traefik.yml" < "$CLUPILOT_TRAEFIK_TOKEN_FILE" chmod 600 "$CLUPILOT_TRAEFIK_TOKEN_FILE" fi } write_traefik_unit() { cat > /etc/systemd/system/traefik.service </dev/null 2>&1; then return 0 fi _tries=$((_tries - 1)) sleep 2 done return 1 } # Lauschen 80 und 443 wirklich? # # „Der Dienst läuft" und „der Port ist belegt" sind zwei Aussagen. Traefik # startet auch dann sauber, wenn ein Entrypoint wegen eines Tippfehlers gar # nicht angelegt wurde. traefik_ports_bound() { for _port in 80 443; do if ! ss -ltn "sport = :${_port}" 2>/dev/null | grep -q LISTEN; then log "Port ${_port} ist nicht belegt" return 1 fi done return 0 } # Holt der Host seine Routentabelle wirklich? # # Direkt gefragt, mit demselben Token und derselben Adresse, die in der # statischen Konfiguration stehen. Über Traefiks eigene Router zu gehen wäre # hier kein Beweis: ein frischer Host hat noch keine Kunden, also ist die # Tabelle mit Recht leer, und „null Router" hieße dann sowohl „alles gut" als # auch „nichts abgerufen". host_routes_reachable() { _token="$1" [ -n "$_token" ] || return 1 if command -v curl >/dev/null 2>&1; then curl -sS -f --connect-timeout 5 --max-time 15 \ -H "Authorization: Bearer ${_token}" \ "${CLUPILOT_API}/host/routes" >/dev/null 2>&1 return $? fi wget -q -O /dev/null --timeout=15 \ --header="Authorization: Bearer ${_token}" \ "${CLUPILOT_API}/host/routes" }