#!/bin/bash # ============================================================================================== # ================================= Unraid API Key Renewal ==================================== # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # Creates/overwrites the Varaverk API key in the unraid-api service registry at # array start. The registry is ephemeral — OS updates and service restarts clear # it. This script re-registers the key every boot so Varaverk's enhanced # monitoring self-heals without manual intervention. # # Also updates HOST*_UNRAID_API_KEY in the local host conf so the partnership # page always reflects the live key value. # # ============================================================================================== # OPERATIONAL MODEL # ============================================================================================== # # 1. Check whether a Varaverk key already exists in the unraid-api registry # 2. Create or overwrite it — the registry is ephemeral, so re-registering is the norm # 3. Write the resulting key into this host's conf, replacing any previous value # 4. Push the key into each partner's OWN conf, at the path their varaverk.cfg reports # 5. Report whether the key was created, refreshed, or unchanged # # Runs at array start. The registry does not survive OS updates or an unraid-api restart, # which is why this re-registers unconditionally rather than only when the key is missing — # a key present in the conf but absent from the registry is the exact failure it repairs. # # ============================================================================================== # DESIGN PRINCIPLES # ============================================================================================== # # Self-Healing at Boot # The unraid-api registry is ephemeral — OS updates and service restarts clear # it without warning. Running at every array start means the key is always # present after boot without any manual intervention. # # Conf Stays Current # HOST*_UNRAID_API_KEY in the local host conf is updated after every renewal. # The partnership page reads the conf — it always reflects the live key value # without a separate sync step. # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # acquire_lock — prevents concurrent renewal attempts at boot # detect_hosts() — sets MY_ID to derive the correct conf var name # Conf file check — aborts before any writes if the host conf is missing # dry-run mode — shows what would happen without touching anything # # Remote path discovery # The partner's conf path comes from resolve_remote_scripts_dir(), which reads their # varaverk.cfg, so a partner in appdata storage mode is found. The path was hardcoded # to the flash plugin directory, which is wrong for any such partner. # # Remote target must exist # The pushed script refuses to create the conf and reports the path it looked at. # resolve_remote_scripts_dir() falls back to our own SCRIPTS_DIR when the probe fails, # and appending an API key to a merely plausible path is how the hardcoded version # failed without saying so. # # ============================================================================================== # CONFIGURATION # ============================================================================================== # # host*.conf # # HOST*_UNRAID_API_KEY # Written by this script every array start. Read by the plugin's PHP for enhanced # monitoring. Treated as output, not input — an existing value is always replaced, # because a conf value that no longer matches the registry is precisely the broken # state this exists to fix. # # Platform-owned: # # The unraid-api service registry — ephemeral, cleared by OS updates and service restarts. # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # unraid_api_key_renew.sh # Renew the key. Silent on success. # # unraid_api_key_renew.sh --dry-run # Show what would happen — no changes made. # # unraid_api_key_renew.sh --log # Verbose output. # # ============================================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../../../load_config.sh" parse_args "$@" # Rewrites the API key into host*.conf and registers it with the unraid-api service. [[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; } acquire_lock detect_hosts # ────────────────────────────────────────────────────────────────────────────── CONF_FILE="$SCRIPT_DIR/../../../Configurations/${MY_ID,,}.conf" VAR_NAME="${MY_ID}_UNRAID_API_KEY" # Key name: "Varaverk " stripping any unraid- prefix # Space separator — unRAID API only allows letters, numbers, and spaces HOSTNAME_SUFFIX=$(hostname -s 2>/dev/null | sed 's/^[Uu][Nn][Rr][Aa][Ii][Dd]-//' || hostname -s) KEY_NAME="Varaverk ${HOSTNAME_SUFFIX}" log "$ICON_GEAR Conf file: $CONF_FILE" log "$ICON_GEAR Key var: $VAR_NAME" log "$ICON_GEAR Key name: $KEY_NAME" if [[ ! -f "$CONF_FILE" ]]; then error "Conf file not found: $CONF_FILE" exit 1 fi if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would check registry for $KEY_NAME, renew only if missing" exit 0 fi # ────────────────────────────────────────────────────────────────────────────── # Check if key already exists in the unraid-api registry before creating. # --overwrite generates a new key value every time, invalidating the old one. # Only renew if the registry has lost it. # # Timeout was 5s — measured live 2026-07-19 at 2.2-2.7s baseline latency for this # exact command with the system idle, leaving almost no margin. Any load spike # (confirmed correlated with resource_watchdog "pressure escalating" events in the # same log) pushed it past 5s, killing the lookup — the script then couldn't tell # "timed out" from "genuinely not in the registry" and fell through to the create # path, logging a false "API key renewed (registry had lost it)" even though the # on-disk key file's timestamp never actually changed. 15s gives real headroom. log "Checking unraid-api registry for $KEY_NAME..." EXISTING=$(timeout 15 /usr/local/sbin/unraid-api apikey --name "$KEY_NAME" --json /dev/null) KEY=$(echo "$EXISTING" | jq -r '.key // empty' 2>/dev/null) if [[ -n "$KEY" ]]; then PREVIEW="${KEY:0:8}...${KEY: -4}" # Always sync registry key → conf, even if the key was already there. # Conf gets wiped on git pull / conf regeneration without touching the registry. # Sourced, not pattern-matched. This was a grep -oP with a variable-length lookbehind, which # Unraid's grep is ugrep and rejects outright — "length of lookbehind assertion is not # limited", rc 2, swallowed by the || true. CONF_HAS_KEY was therefore always empty, the # comparison below never matched, and this script logged "conf is stale — syncing" and # rewrote the same key into the conf every fifteen minutes since it was written. # Sourcing also means the value is read the way bash reads it, escapes and all. CONF_HAS_KEY=$(bash -c 'source "$1" >/dev/null 2>&1 || exit 0; printf "%s" "${!2-}"' \ _ "$CONF_FILE" "$VAR_NAME" 2>/dev/null || true) if [[ "$CONF_HAS_KEY" == "$KEY" ]]; then echo "API key valid ✅ — $VAR_NAME = $PREVIEW" log "Key in registry and conf — no action needed" exit 0 fi log "Key in registry but conf is stale — syncing..." if grep -q "^\s*${VAR_NAME}\s*=" "$CONF_FILE"; then sed -i "s|^\(\s*${VAR_NAME}\s*=\s*\)\"[^\"]*\"|\1\"${KEY}\"|" "$CONF_FILE" else echo " ${VAR_NAME}=\"${KEY}\"" >> "$CONF_FILE" fi echo "API key synced to conf ✅ — $VAR_NAME = $PREVIEW" exit 0 fi log "Key not found in registry — creating new key..." RAW=$(timeout 10 /usr/local/sbin/unraid-api apikey \ --name "$KEY_NAME" --create --overwrite \ --description "Varaverk plugin" --roles ADMIN --json &1) if [[ -z "$RAW" ]]; then error "unraid-api returned no output" exit 1 fi KEY=$(echo "$RAW" | jq -r '.key // empty' 2>/dev/null) if [[ -z "$KEY" ]]; then error "No key in unraid-api response: ${RAW:0:200}" exit 1 fi # ────────────────────────────────────────────────────────────────────────────── if grep -q "^\s*${VAR_NAME}\s*=" "$CONF_FILE"; then sed -i "s|^\(\s*${VAR_NAME}\s*=\s*\)\"[^\"]*\"|\1\"${KEY}\"|" "$CONF_FILE" else echo " ${VAR_NAME}=\"${KEY}\"" >> "$CONF_FILE" fi PREVIEW="${KEY:0:8}...${KEY: -4}" log "Writing new key to: $CONF_FILE" warn "API key renewed ✅ — $VAR_NAME = $PREVIEW (registry had lost it)" # ── Push renewed key into each partner's OWN conf ───────────────────────────── # Each host's conf is its complete keychest — no cross-host conf files needed. # SSH_KEY is set by detect_hosts() — this server's outbound private key. if [[ -z "$SSH_KEY" ]]; then log "No SSH key configured — skipping partner push" exit 0 fi # resolve_remote_scripts_dir() reads this; every inline timeout below already uses 10. SSH_TIMEOUT=10 for host_var in $(compgen -v | grep -E '^HOST[0-9]+$'); do partner_host="${!host_var}" [[ -z "$partner_host" ]] && continue [[ "${host_var,,}" == "${MY_ID,,}" ]] && continue partner_slot="${host_var,,}" # e.g. host2 partner_ip=$(resolve_tailscale_ip "$partner_host" 2>/dev/null || true) [[ -z "$partner_ip" ]] && { log "Cannot resolve IP for $partner_host — skipping"; continue; } # Target is the partner's OWN conf on their machine. Their SCRIPTS_DIR is read from their # varaverk.cfg rather than assumed — this was hardcoded to the flash plugin path, so a # partner in appdata storage mode had its key appended under a directory that does not # exist there. HOST2 has run in appdata mode since it was installed. partner_sd=$(resolve_remote_scripts_dir "$partner_ip" "$SSH_KEY" "no") partner_conf="${partner_sd}/Configurations/${partner_slot}.conf" tmp=$(mktemp /tmp/vv_kp_XXXXXX.sh) remote="/tmp/vv_kp_${RANDOM}.sh" chmod 700 "$tmp" # Key stays in the temp file — never appears in SSH command args. # The conf must already exist: resolve_remote_scripts_dir falls back to our own SCRIPTS_DIR # when the probe fails, and appending a key to a path that is merely plausible is how the # hardcoded version failed silently. Report the path instead of guessing. cat > "$tmp" </dev/null; then sed -i 's|^\(\\s*${VAR_NAME}\\s*=\\s*\)"[^"]*"|\1"${KEY}"|' "\$target" else printf ' ${VAR_NAME}="%s"\n' '${KEY}' >> "\$target" fi echo ok PUSHSCRIPT if timeout 10 scp -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes \ -o StrictHostKeyChecking=no "$tmp" "root@${partner_ip}:${remote}" 2>/dev/null; then push_out=$(timeout 10 ssh -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes \ -o StrictHostKeyChecking=no "root@${partner_ip}" \ "bash '${remote}'; rc=\$?; rm -f '${remote}'; exit \$rc" 2>/dev/null) case "$push_out" in *ok*) echo "Key pushed to $partner_host ✅" ;; missing:*) warn "Key push to $partner_host failed — no conf at ${push_out#missing:}" ;; *) warn "Key push to $partner_host failed — they can create their own copy" ;; esac else warn "SCP to $partner_host failed — skipping" fi rm -f "$tmp" done