# 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 "=" 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 }