Hand over the token and let the console take it from here

The last section carries three things, and none of them could sit anywhere
else: the registration and key swap, Traefik's token plus the proof that a route
table actually arrives, and the nftables lockdown that ran last in the old
pipeline for a reason.

`|| true` is gone from the pveum calls, and not by deleting it. `role add ||
true` applied the privilege list only on the run that first created the role, so
a privilege added later reached new hosts and no existing one — which is exactly
how Sys.Modify was missing everywhere, surfacing much later and somewhere else
as a 403 on POST /cluster/backup that failed a paying customer at
register_backup. So: add, and on failure modify, as two separate commands so a
failure can be attributed. For the user and the ACL the tolerated `|| true` is
replaced by looking: try, and if it fails, check whether the desired state holds
anyway. A `|| true` hides "already there" and "went wrong" equally well.

The privilege list is copied verbatim from config/provisioning.php and checked
against it — 18 privileges, byte-identical, Sys.Modify included. It is granted on
/ because both endpoints that need it check / and nothing narrower satisfies
them.

The key swap follows the four steps this plan was amended to require. Register
first and hold the answer, switch wg0 to the new private key, PROVE a fresh
handshake, and only then let the old key go. If the handshake does not come, it
puts the old key back and fails loudly. Ordering alone says when to discard, not
whether the new key carries — and a host that locks itself out on the final step
is the one failure nobody fixes remotely.

Traefik's token is written here because here is where it exists, and the route
table is then fetched directly with that token and that URL. Section 7 could not
give this proof; it is not skipped, it is given where it can be.

The lockdown comes dead last, with the full ruleset from SecureHostFirewall
including the ICMP correction — ICMPv6 neighbour discovery and packet-too-big,
IPv4 fragmentation-needed — because a bare policy drop takes IPv6 down within
minutes and black-holes large transfers instead of failing them. The ruleset is
validated with `nft -c` before it is applied, since a partially loaded ruleset
means policy drop is in place and the exceptions are not. There is no automatic
reopening: a firewall that reopens itself under failure is not a firewall, so the
emergency release is a script for the provider's console.

Verified without hardware: all seven files dash-clean; json_field reads
full-tokenid, value and host_token without jq; the privilege list is identical to
the config's; and the rendered ruleset carries policy drop, both ICMP families,
80/443, the DHCP rebind rule and the tunnel-only 22/8006. Step 2 unticked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feature/host-bootstrap
nexxo 2026-07-30 20:20:20 +02:00
parent 5d25018cc6
commit f45f290f00
4 changed files with 433 additions and 4 deletions

View File

@ -851,6 +851,90 @@ section_template_built() {
return 0 return 0
} }
# ---------------------------------------------------------------------------
# Abschnitt 9: registered — Token übergeben, und erst dann zusperren
# ---------------------------------------------------------------------------
#
# Der letzte Abschnitt trägt drei Dinge, die alle hierher gehören und nirgendwo
# sonst hin konnten:
#
# - die Registrierung samt Schlüsseltausch,
# - Traefiks Token und der Beweis, dass eine Routentabelle wirklich kommt
# (Abschnitt 7 konnte ihn nicht führen, es gab den Token noch nicht),
# - die nftables-Sperre, die im alten Ablauf aus gutem Grund ZULETZT lief.
section_registered() {
export DEBIAN_FRONTEND=noninteractive
apt-get install -y jq >> "${CLUPILOT_WORK_DIR}/apt.log" 2>&1 || true
if ! converge_proxmox_role; then
CLUPILOT_SECTION_NOTE='die Proxmox-Rolle ließ sich nicht angleichen — ohne Sys.Modify stirbt jede Kundenbereitstellung am Backup-Schritt'
return 1
fi
if ! ensure_proxmox_user; then
CLUPILOT_SECTION_NOTE='Proxmox-Benutzer oder Rollenzuweisung ließen sich nicht herstellen'
return 1
fi
_api_token="$(mint_proxmox_token)"
if [ -z "$_api_token" ]; then
CLUPILOT_SECTION_NOTE='Proxmox-API-Token ließ sich nicht erzeugen'
return 1
fi
# Der frische Schlüssel. Der aus der Befehlszeile stand in einer
# Zwischenablage und wird damit wertlos (Spec §5).
umask 077
_new_private="$(wg genkey)"
_new_public="$(printf '%s' "$_new_private" | wg pubkey)"
if [ -z "$_new_public" ]; then
CLUPILOT_SECTION_NOTE='neues WireGuard-Schlüsselpaar ließ sich nicht erzeugen'
return 1
fi
_response="$(post_registration "$(render_register_payload "$_api_token" "$_new_public")")"
if [ -z "$_response" ]; then
CLUPILOT_SECTION_NOTE="POST ${CLUPILOT_API}/host/register hat nicht geantwortet"
return 1
fi
_host_token="$(json_field host_token "$_response")"
if [ -z "$_host_token" ]; then
CLUPILOT_SECTION_NOTE='die Registrierung kam ohne host_token zurück — ohne den holt Traefik nie eine Routentabelle'
return 1
fi
# Erst registrieren und die Antwort haben, DANN tauschen — und den Tausch
# beweisen, bevor der alte Schlüssel verworfen ist.
if ! swap_wireguard_key "$_new_private"; then
CLUPILOT_SECTION_NOTE='der Schlüsseltausch hat keinen Handshake ergeben; es wurde auf den alten Schlüssel zurückgestellt'
return 1
fi
# Jetzt kann Traefik seine Tabelle holen.
write_traefik_static_config "$_host_token"
systemctl restart traefik >/dev/null 2>&1 || true
if ! traefik_answers_ping 15; then
CLUPILOT_SECTION_NOTE='Traefik antwortet nach dem Einsetzen des Tokens nicht mehr'
return 1
fi
if ! host_routes_reachable "$_host_token"; then
CLUPILOT_SECTION_NOTE="die Routentabelle unter ${CLUPILOT_API}/host/routes ist mit diesem Token nicht abrufbar — ein Host, der Traefik hat, aber nie eine Tabelle bekam, ist nicht fertig"
return 1
fi
# Ganz zuletzt zusperren. Ab hier ist Port 22 nur noch über den Tunnel da.
if ! apply_host_firewall; then
CLUPILOT_SECTION_NOTE='die Host-Firewall ließ sich nicht anwenden; der Host bleibt offen, statt halb gesperrt zu sein'
return 1
fi
CLUPILOT_SECTION_NOTE='Token übergeben, Schlüssel getauscht und Handshake erneut bewiesen, Routentabelle abgerufen, Host abgesperrt'
return 0
}
main() { main() {
parse_arguments "$@" parse_arguments "$@"
@ -865,6 +949,8 @@ main() {
. "${CLUPILOT_BOOTSTRAP_DIR}/lib/traefik.sh" . "${CLUPILOT_BOOTSTRAP_DIR}/lib/traefik.sh"
# shellcheck source=lib/template.sh # shellcheck source=lib/template.sh
. "${CLUPILOT_BOOTSTRAP_DIR}/lib/template.sh" . "${CLUPILOT_BOOTSTRAP_DIR}/lib/template.sh"
# shellcheck source=lib/register.sh
. "${CLUPILOT_BOOTSTRAP_DIR}/lib/register.sh"
init_reporting init_reporting
if [ "$CLUPILOT_RESUMING" = '1' ]; then if [ "$CLUPILOT_RESUMING" = '1' ]; then
@ -915,9 +1001,12 @@ main() {
die 'Abbruch: ohne bewiesene Vorlage stirbt der erste bezahlte Auftrag nach der Zahlung.' die 'Abbruch: ohne bewiesene Vorlage stirbt der erste bezahlte Auftrag nach der Zahlung.'
fi fi
# run_section registered section_registered # Task 9 if ! run_section registered section_registered; then
die 'Abbruch bei der Übergabe. Der Host ist NICHT abgesperrt und NICHT fertig.'
fi
log 'Proxmox abgenommen, Quellen in Ordnung. Die Abschnitte ab Task 5 sind noch nicht eingehängt.' flush_reports
log 'Fertig. Der Host hat sich gemeldet; die Konsole übernimmt ab hier.'
} }
main "$@" main "$@"

View File

@ -372,6 +372,134 @@ wireguard_handshake_proven() {
return 1 return 1
} }
# ---------------------------------------------------------------------------
# Die Host-Firewall (nftables)
# ---------------------------------------------------------------------------
#
# Läuft ZULETZT, in Abschnitt `registered`. Der Grund steht in
# `SecureHostFirewall.php` und ist eine Reihenfolgefrage: erst wenn der Tunnel
# jeden vorigen Schritt getragen hat, darf SSH nach außen zugehen. Früher
# geschlossen, und ein Host hinge mitten in der Übernahme fest.
#
# nftables statt pve-firewall, und nur die INPUT-Kette: FORWARD und OUTPUT
# bleiben auf der Kernel-Vorgabe, damit der Verkehr der Kunden-VMs über die
# Brücken nie von einer Host-Regel berührt wird. Zwei Filter, die beide den
# Eingang besitzen wollen, sind der Weg zu einem unerreichbaren Host — deshalb
# schaltet `enable_datacenter_firewall` die Node-Firewall ausdrücklich ab.
#
# **Keine automatische Wiederöffnung.** Kein Zeitgeber, kein „nach N Minuten ohne
# Handshake wieder auf". Eine Firewall, die sich unter Störung selbst öffnet, ist
# keine Firewall. Für den Notfall liegt ein Skript bereit, das von der
# Anbieterkonsole aus gestartet wird.
CLUPILOT_EMERGENCY_SCRIPT='/usr/local/sbin/clupilot-emergency-open-firewall.sh'
render_nftables_config() {
_wg_subnet="$1"
cat <<NFT
#!/usr/sbin/nft -f
# Von CluPilots Bootstrap geschrieben.
# Nicht von Hand ändern — der nächste Lauf überschreibt die Datei.
# Um den Host im Notfall vollständig zu öffnen, von der Anbieterkonsole aus
# ${CLUPILOT_EMERGENCY_SCRIPT} starten.
flush ruleset
table inet clupilot_filter {
chain input {
type filter hook input priority 0; policy drop;
iif "lo" accept
ct state invalid drop
ct state established,related accept
# ICMPv6 ist nicht optional. Neighbour Discovery (135/136) und Router
# Advertisements kommen in der INPUT-Kette an, ein blankes policy drop
# nimmt IPv6 also mit, sobald der Neighbour-Cache abläuft — Minuten, nicht
# Tage. packet-too-big ist die andere Hälfte: daran lernt die Gegenstelle
# einer TLS-Verbindung die Pfad-MTU, und es zu verwerfen lässt große
# Übertragungen ins Leere laufen, statt sie scheitern zu lassen. Eine
# Regel pro Zeile, absichtlich: ein Regelwerk, das man zeilenweise lesen
# kann, kann man auch zeilenweise prüfen.
meta nfproto ipv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request, echo-reply, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, mld-listener-query, mld-listener-report, mld-listener-done } accept
# IPv4-ICMP, aus demselben Grund: destination-unreachable trägt
# fragmentation-needed (Code 4), worauf die IPv4-Pfad-MTU-Erkennung
# beruht. echo ist dabei, damit der Host auf ein ping antwortet — auch auf
# das des Hubs.
meta nfproto ipv4 icmp type { destination-unreachable, time-exceeded, parameter-problem, echo-request, echo-reply } accept
# Öffentliches Web — Traefik bedient hier den Kundenverkehr.
tcp dport { 80, 443 } accept
# DHCP-Antworten. Manche Anbieter vergeben die IPv4-Adresse per DHCP, und
# ein REBIND kommt als frisches eingehendes Paket an, für das conntrack
# keinen Eintrag hat — verworfen, die Lease läuft ab, und der Host
# verliert die Adresse, über die er erreicht wird. Bewusst eng: nur eine
# Antwort vom Server-Port an den Client-Port.
udp sport 67 udp dport 68 accept
# Verwaltungszugänge: nur über den Tunnel.
ip saddr ${_wg_subnet} tcp dport { 22, 8006 } accept
}
}
NFT
}
render_emergency_script() {
cat <<'SH'
#!/bin/sh
# CluPilots Notöffnung der Firewall.
#
# NUR von der Anbieterkonsole (KVM/seriell) starten, wenn dieser Host über das
# Netz nicht mehr erreichbar ist. Es tut genau eine Sache: nftables abschalten
# und alle Regeln verwerfen, sodass der Host auf jedem Port wieder annimmt.
#
# Bewusst von Hand. Nichts hier öffnet die Firewall selbsttätig, auf einen
# Zeitgeber hin oder weil ein Handshake eine Weile alt aussah — eine Firewall,
# die sich unter Störung selbst öffnet, ist keine Firewall. Die Regeln kommen
# zurück mit:
# nft -f /etc/nftables.conf && systemctl enable --now nftables
set -e
systemctl disable --now nftables 2>/dev/null || true
nft flush ruleset 2>/dev/null || true
echo "Firewallregeln verworfen: dieser Host nimmt jetzt auf jedem Port an."
SH
}
# Sperrt den Host ab. Verlangt VORHER einen bewiesenen Tunnel und fasst sonst
# nichts an.
apply_host_firewall() {
if ! wireguard_handshake_proven 3; then
log 'Tunnel nicht bewiesen — die Host-Firewall wird nicht angewendet'
return 1
fi
export DEBIAN_FRONTEND=noninteractive
apt-get install -y nftables >/dev/null 2>&1 || { log 'nftables ließ sich nicht installieren'; return 1; }
render_nftables_config "$(ipv4_network "$CLUPILOT_WG_IP")" > /etc/nftables.conf
render_emergency_script > "$CLUPILOT_EMERGENCY_SCRIPT"
chmod 700 "$CLUPILOT_EMERGENCY_SCRIPT"
# Erst prüfen, dann anwenden. Ein Regelwerk mit einem Tippfehler wird von
# `nft -f` teilweise geladen — und „teilweise" heißt hier: policy drop steht,
# die Ausnahmen nicht.
if ! nft -c -f /etc/nftables.conf >/dev/null 2>&1; then
log 'nftables-Regelwerk ist nicht gültig — es wird nichts angewendet'
return 1
fi
systemctl enable --now nftables >/dev/null 2>&1 || { log 'nftables ließ sich nicht aktivieren'; return 1; }
systemctl reload-or-restart nftables >/dev/null 2>&1 || nft -f /etc/nftables.conf
return 0
}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Die Datacenter-Firewall # Die Datacenter-Firewall
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@ -0,0 +1,211 @@
# shellcheck shell=sh
#
# Die Übergabe: Proxmox-Rolle und Token erzeugen, bei CluPilot registrieren, den
# WireGuard-Schlüssel tauschen.
#
# ---------------------------------------------------------------------------
# Die Rolle, und warum `|| true` hier NICHT steht
# ---------------------------------------------------------------------------
#
# `pveum role add … || true` wendet die Rechteliste nur bei dem Lauf an, der die
# Rolle zum ersten Mal anlegt. Ein Recht, das später dazukam, erreichte damit
# neue Hosts und keinen einzigen bestehenden. Genau so fehlte `Sys.Modify`
# überall — mit einem Symptom, das viel später und ganz woanders auftrat: ein
# 403 auf `POST /cluster/backup`, und die Bereitstellung eines zahlenden Kunden
# scheiterte bei `register_backup`.
#
# Deshalb: `role add`, und wenn das scheitert (die Rolle gibt es schon, der
# gewöhnliche Fall), `role modify` — zwei getrennte Befehle, damit ein
# Fehlschlag zuzuordnen ist. `modify` ohne `-append` ERSETZT die Liste, der Host
# passt sich also der Konfiguration an, statt anzusammeln, was irgendeine
# frühere Fassung einmal gewährt hat.
#
# Für Benutzer und ACL galt `|| true` bisher als geduldet, weil „gibt es schon"
# dort der Normalfall ist. Auch das entfällt: statt den Fehler zu verwerfen,
# wird nachgesehen, ob der gewünschte Zustand steht. Ein `|| true` verdeckt
# nicht nur „gibt es schon", sondern auch „ging schief".
CLUPILOT_PVE_ROLE='CluPilotAutomation'
CLUPILOT_PVE_USER='automation@pve'
CLUPILOT_PVE_TOKEN_NAME='clupilot'
# Wörtlich aus `config/provisioning.php`. Sys.Modify ist nicht optional: es steht
# in `PVE/API2/Backup.pm` als `check => ['perm', '/', ['Sys.Modify']]` — an der
# Quelle geprüft, nicht aus einem Forenbeitrag geschlossen — und deckt zugleich
# `PUT /cluster/firewall/options` ab. Gewährt auf `/`, weil beide Endpunkte
# gegen `/` prüfen und nichts Engeres sie zufriedenstellt.
CLUPILOT_PVE_PRIVS='VM.Allocate,VM.Clone,VM.Config.Disk,VM.Config.CPU,VM.Config.Memory,VM.Config.Network,VM.Config.Options,VM.Config.Cloudinit,VM.PowerMgmt,VM.Monitor,VM.Audit,VM.Backup,VM.GuestAgent.Audit,VM.GuestAgent.Unrestricted,Datastore.AllocateSpace,Datastore.Audit,Sys.Audit,Sys.Modify'
# Ein Feld aus flachem JSON. Nimmt `jq`, wenn es da ist, und sonst einen
# eng gefassten Ausdruck.
json_field() {
_key="$1"
_json="$2"
if command -v jq >/dev/null 2>&1; then
printf '%s' "$_json" | jq -r --arg k "$_key" '.[$k] // empty' 2>/dev/null
return 0
fi
printf '%s' "$_json" \
| sed -n "s/.*\"${_key}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" \
| head -1
}
converge_proxmox_role() {
if pveum role add "$CLUPILOT_PVE_ROLE" -privs "$CLUPILOT_PVE_PRIVS" >/dev/null 2>&1; then
log "Proxmox-Rolle ${CLUPILOT_PVE_ROLE} angelegt"
return 0
fi
if pveum role modify "$CLUPILOT_PVE_ROLE" -privs "$CLUPILOT_PVE_PRIVS" >/dev/null 2>&1; then
log "Proxmox-Rolle ${CLUPILOT_PVE_ROLE} auf die aktuelle Rechteliste gebracht"
return 0
fi
log 'Die Rechteliste der Proxmox-Rolle ließ sich nicht angleichen'
return 1
}
# Benutzer und ACL — ohne `|| true`, aber mit Nachsehen.
ensure_proxmox_user() {
if ! pveum user list --output-format json 2>/dev/null | grep -q "\"${CLUPILOT_PVE_USER}\""; then
if ! pveum user add "$CLUPILOT_PVE_USER" >/dev/null 2>&1; then
# Nicht blind schlucken: nachsehen, ob er jetzt da ist. Wenn ja, war
# es ein Wettlauf und kein Fehler.
if ! pveum user list --output-format json 2>/dev/null | grep -q "\"${CLUPILOT_PVE_USER}\""; then
log "Proxmox-Benutzer ${CLUPILOT_PVE_USER} ließ sich nicht anlegen"
return 1
fi
fi
fi
if ! pveum acl modify / -user "$CLUPILOT_PVE_USER" -role "$CLUPILOT_PVE_ROLE" >/dev/null 2>&1; then
# Auch hier: gilt die Zuweisung inzwischen, war es keiner.
if ! pveum acl list --output-format json 2>/dev/null | grep -q "$CLUPILOT_PVE_ROLE"; then
log 'Die Rollenzuweisung auf / ließ sich nicht setzen'
return 1
fi
fi
return 0
}
# Erzeugt den API-Token und gibt "<tokenid>=<secret>" aus.
#
# Das Geheimnis wird genau einmal gezeigt. Ein halb angelegter Token aus einem
# abgebrochenen früheren Lauf wird vorher entfernt, damit ein zweiter Lauf
# gleich ausgeht.
mint_proxmox_token() {
pveum user token remove "$CLUPILOT_PVE_USER" "$CLUPILOT_PVE_TOKEN_NAME" >/dev/null 2>&1 || true
_out="$(pveum user token add "$CLUPILOT_PVE_USER" "$CLUPILOT_PVE_TOKEN_NAME" -privsep 0 --output-format json 2>/dev/null)"
[ -n "$_out" ] || return 1
_secret="$(json_field value "$_out")"
[ -n "$_secret" ] || return 1
_tokenid="$(json_field 'full-tokenid' "$_out")"
[ -n "$_tokenid" ] || _tokenid="${CLUPILOT_PVE_USER}!${CLUPILOT_PVE_TOKEN_NAME}"
printf '%s=%s' "$_tokenid" "$_secret"
}
# Der Fingerabdruck des SSH-Wirtsschlüssels. Nur zur Anzeige — die Plattform
# meldet sich nicht mehr per SSH an (Spec §6).
ssh_host_fingerprint() {
for _key in /etc/ssh/ssh_host_ed25519_key.pub /etc/ssh/ssh_host_rsa_key.pub; do
if [ -f "$_key" ]; then
ssh-keygen -lf "$_key" 2>/dev/null | awk '{ print $2; exit }'
return 0
fi
done
printf ''
}
# Die Eckdaten dieses Knotens, so wie `RegisterCapacity` sie liest.
#
# Der größte Speicher, der Platten aufnimmt — NICHT die Summe. Proxmox führt
# überlappende Pools (local und local-lvm auf derselben Platte), und zu
# summieren hieße, denselben Platz zweimal zu verkaufen.
node_total_gb() {
pvesm status --content images 2>/dev/null \
| awk 'NR > 1 { if ($4 + 0 > max) max = $4 + 0 } END { printf "%d", max / 1048576 }'
}
# Baut den Rumpf für POST /host/register.
render_register_payload() {
_api_token="$1"
_wg_pubkey="$2"
printf '{"code":"%s","api_token":"%s","wg_pubkey":"%s","node":"%s","pve_version":"%s","total_gb":%s,"total_ram_mb":%s,"cpu_cores":%s,"ssh_host_key":"%s"}' \
"$(json_escape "$CLUPILOT_CODE")" \
"$(json_escape "$_api_token")" \
"$(json_escape "$_wg_pubkey")" \
"$(json_escape "$(hostname)")" \
"$(json_escape "$(pveversion 2>/dev/null | head -1)")" \
"$(node_total_gb)" \
"$(awk '/^MemTotal:/ { printf "%d", $2 / 1024; exit }' /proc/meminfo)" \
"$(nproc 2>/dev/null || echo 1)" \
"$(json_escape "$(ssh_host_fingerprint)")"
}
# POST /host/register — gibt die Antwort aus, oder 1.
post_registration() {
_payload="$1"
if command -v curl >/dev/null 2>&1; then
curl -sS -f --connect-timeout 10 --max-time 60 \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-X POST --data "$_payload" \
"${CLUPILOT_API}/host/register" 2>/dev/null
return $?
fi
wget -q -O - --timeout=60 \
--header='Content-Type: application/json' --header='Accept: application/json' \
--post-data="$_payload" \
"${CLUPILOT_API}/host/register" 2>/dev/null
}
# ---------------------------------------------------------------------------
# Der Schlüsseltausch
# ---------------------------------------------------------------------------
#
# Task 6 lässt den Tunnel erst mit bewiesenem Handshake gelten. Hier wird genau
# dieser Tunnel im laufenden Betrieb auf einen neuen Schlüssel umgestellt — also
# gilt dieselbe Regel, und die Reihenfolge allein genügt nicht: sie sagt, WANN
# verworfen wird, nicht OB das Neue trägt.
#
# 1. neues Paar erzeugen, registrieren, Antwort haben
# 2. wg0 auf den neuen privaten Schlüssel umstellen
# 3. erneuten Handshake BEWEISEN
# 4. erst dann den alten verwerfen
#
# Kommt der Handshake nicht, wird zurückgestellt und laut gescheitert. Ein Host,
# der sich im letzten Schritt selbst aussperrt, ist der eine Fall, den niemand
# aus der Ferne repariert — und er passiert am Ende eines Laufs, der bis dahin
# alles richtig gemacht hat.
swap_wireguard_key() {
_new_private="$1"
_old_private="$CLUPILOT_WG_PRIVATE"
CLUPILOT_WG_PRIVATE="$_new_private"
printf '%s\n' "$(render_wireguard_config)" > /etc/wireguard/wg0.conf
chmod 600 /etc/wireguard/wg0.conf
systemctl restart wg-quick@wg0 >/dev/null 2>&1 || true
if wireguard_handshake_proven 10; then
log 'Handshake mit dem neuen Schlüssel bewiesen'
return 0
fi
log 'Kein Handshake mit dem neuen Schlüssel — es wird auf den alten zurückgestellt'
CLUPILOT_WG_PRIVATE="$_old_private"
printf '%s\n' "$(render_wireguard_config)" > /etc/wireguard/wg0.conf
chmod 600 /etc/wireguard/wg0.conf
systemctl restart wg-quick@wg0 >/dev/null 2>&1 || true
return 1
}

View File

@ -52,6 +52,7 @@
| `deploy/bootstrap/lib/network.sh` | Brücke mit Selbstrücknahme, nftables | | `deploy/bootstrap/lib/network.sh` | Brücke mit Selbstrücknahme, nftables |
| `deploy/bootstrap/lib/traefik.sh` | Traefik: Binary, statische Konfiguration, Unit | | `deploy/bootstrap/lib/traefik.sh` | Traefik: Binary, statische Konfiguration, Unit |
| `deploy/bootstrap/lib/template.sh` | Die goldene Vorlage | | `deploy/bootstrap/lib/template.sh` | Die goldene Vorlage |
| `deploy/bootstrap/lib/register.sh` | Rolle, Token, Registrierung, Schlüsseltausch |
| `deploy/bootstrap/assets/docker-compose.yml` | Die Compose-Datei, die in die Vorlage kommt | | `deploy/bootstrap/assets/docker-compose.yml` | Die Compose-Datei, die in die Vorlage kommt |
| `docs/runbooks/host-bootstrap.md` | Was zu tun ist, wenn das Skript stehenbleibt | | `docs/runbooks/host-bootstrap.md` | Was zu tun ist, wenn das Skript stehenbleibt |
@ -439,7 +440,7 @@ fest, dass `VerifyVmTemplate` heute nur die **Existenz** prüft, nicht das Merkm
### Task 9: `registered` — Token übergeben ### Task 9: `registered` — Token übergeben
- [ ] **Step 1: Schreiben** - [x] **Step 1: Schreiben**
Proxmox-Rolle und Token erzeugen — **`Sys.Modify` nicht vergessen**, siehe Proxmox-Rolle und Token erzeugen — **`Sys.Modify` nicht vergessen**, siehe
`CreateAutomationToken.php`; ohne das stirbt jede Kundenbereitstellung am `CreateAutomationToken.php`; ohne das stirbt jede Kundenbereitstellung am
@ -488,7 +489,7 @@ bedeutet** — und wo eines steht, steht der Grund daneben.
meldet `provisioning.usable_host` als erfüllt. **Und der Tunnel steht danach meldet `provisioning.usable_host` als erfüllt. **Und der Tunnel steht danach
noch** — nach dem Schlüsseltausch einmal `wg show` ansehen und einen frischen noch** — nach dem Schlüsseltausch einmal `wg show` ansehen und einen frischen
Handshake mit dem *neuen* Schlüssel sehen, nicht den alten Zählerstand. Handshake mit dem *neuen* Schlüssel sehen, nicht den alten Zählerstand.
- [ ] **Step 3: Committen.** `Hand over the token and let the console take it from here` - [x] **Step 3: Committen.** `Hand over the token and let the console take it from here`
--- ---