diff --git a/scripts/farm/respaldo-subcuenta.sh b/scripts/farm/respaldo-subcuenta.sh new file mode 100755 index 00000000..ee879468 --- /dev/null +++ b/scripts/farm/respaldo-subcuenta.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# respaldo-subcuenta.sh — crea (o verifica) la subcuenta de SÓLO LECTURA del Storage Box que usa la +# granja para tirar del respaldo por la red interna de Hetzner. +# +# ── POR QUÉ EXISTE: 440 kB/s ──────────────────────────────────────────────────────────────────── +# Medido el 2026-08-09 contra el propio Storage Box: el uplink de la oficina da **440 kB/s**. Con eso +# los 127 G del store son **82 horas** — subirlo desde el laptop no es lento, es imposible. Pero el +# Storage Box está en hel1 y el worker está en hel1: entre ellos la copia va por dentro de Hetzner. +# ⇒ El laptop deja de ser el camino de los datos. Sólo manda las RECETAS (26 M). +# +# ── POR QUÉ UNA SUBCUENTA Y NO LA CUENTA PRINCIPAL ───────────────────────────────────────────── +# El worker es EFÍMERO y se borra solo; darle la credencial principal sería darle permiso de borrado +# sobre el ÚNICO respaldo que existe. Un respaldo al que puede escribir la máquina de la que hay que +# protegerse no es un respaldo. La subcuenta acota el daño posible a cero por construcción: +# --readonly el sistema de ficheros se monta de sólo lectura del lado del box +# --reachable-externally=false sólo desde la red de Hetzner: la credencial no sirve fuera +# --home-directory hammer no ve el resto del box +# +# ── LA VERIFICACIÓN ES NEGATIVA, Y ESO NO ES OPCIONAL ────────────────────────────────────────── +# «Sólo lectura» es una afirmación sobre lo que el sistema **impide**, así que no se comprueba +# leyendo (eso sólo prueba que lee). Hay que INTENTAR ESCRIBIR Y BORRAR y exigir que fallen. Medido +# el 2026-08-09: `rm` → «Read-only file system», `scp` → «dest open: Failure», respaldo intacto. +# Sin ese paso lo único que sabríamos es que la subcuenta funciona, no que está acotada. +# +# ── LA LLAVE VIVE EN EL WORKER Y MUERE CON ÉL ────────────────────────────────────────────────── +# Se genera en el worker (nunca viaja una privada desde el laptop) y su pública se autoriza en +# `hammer/.ssh/authorized_keys` del box, escrita CON LA CUENTA PRINCIPAL — la subcuenta no puede +# escribir ni su propio authorized_keys, que es justo lo que se quería. +# El home es `hammer` y no `hammer/store` a propósito: el authorized_keys tiene que estar en el home, +# y no queremos un `.ssh` dentro del store, que es CAS y se cuenta por entradas. +# +# ⚠ La shell del Storage Box es RESTRINGIDA: acepta `ls`/`mkdir`/`rm` pero **no redirección**. +# `echo k > authorized_keys` falla EN SILENCIO (crea el directorio y no el fichero). Va con `scp`. +# +# Uso: scripts/farm/respaldo-subcuenta.sh crear # crea y guarda la clave en ~/.config/hammer +# scripts/farm/respaldo-subcuenta.sh autorizar +# scripts/farm/respaldo-subcuenta.sh verificar # la prueba NEGATIVA +set -uo pipefail +ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT" +BOX="${BOX:-hammer-respaldo}" +SUB_HOST="${SUB_HOST:-u647150-sub1.your-storagebox.de}" +SUB_USER="${SUB_USER:-u647150-sub1}" +MAIN="${MAIN:-u647150@u647150.your-storagebox.de}" +KEY="${KEY:-$HOME/.ssh/github5}" +CRED="$HOME/.config/hammer/storagebox-worker.env" + +case "${1:-}" in +crear) + # La contraseña la exige la API aunque después se use llave. Debe traer mayúscula, minúscula, + # número y símbolo, o la API la rechaza con `invalid_input`. + PW="$(openssl rand -base64 18 | tr -d '/+=' | cut -c1-18)Aa1_%" + hcloud storage-box subaccount create "$BOX" \ + --home-directory hammer --password "$PW" --readonly --enable-ssh \ + --reachable-externally=false \ + --description "worker de la granja: lectura del respaldo, red interna" || exit 1 + umask 077; printf 'SUB_PASSWORD=%s\n' "$PW" > "$CRED"; chmod 600 "$CRED" + echo "==> subcuenta creada · credencial en $CRED (600, FUERA del repo, nunca a git)" + ;; +autorizar) + IP="${2:?falta la IP del worker}" + PUB=$(ssh -i "$KEY" "root@$IP" 'mkdir -p /root/.ssh + [ -f /root/.ssh/sb_sub ] || ssh-keygen -q -t ed25519 -N "" -C "hworker→respaldo(ro)" -f /root/.ssh/sb_sub + cat /root/.ssh/sb_sub.pub' 2>/dev/null | tail -1) + [ -z "$PUB" ] && { echo "!! no obtuve la pública del worker"; exit 1; } + T=$(mktemp); printf '%s\n' "$PUB" > "$T" + ssh -4 -p 23 -i "$KEY" "$MAIN" 'mkdir -p hammer/.ssh' >/dev/null 2>&1 + # scp y NO `echo >`: la shell restringida del box no hace redirección y falla sin decirlo. + scp -4 -P 23 -i "$KEY" "$T" "$MAIN:hammer/.ssh/authorized_keys" || { rm -f "$T"; exit 1; } + rm -f "$T"; echo "==> pública del worker autorizada en el box" + ;; +verificar) + IP="${2:?falta la IP del worker}" + ssh -i "$KEY" "root@$IP" "SB=\"ssh -4 -p 23 -i /root/.ssh/sb_sub -o StrictHostKeyChecking=no $SUB_USER@$SUB_HOST\" + lee=\$(\$SB 'ls' 2>/dev/null | tr '\n' ' ') + borra=\$(\$SB 'rm -rf estado' 2>&1 | head -1) + echo hola > /tmp/.wtest; esc=\$(scp -4 -P 23 -i /root/.ssh/sb_sub -o StrictHostKeyChecking=no /tmp/.wtest $SUB_USER@$SUB_HOST:.wtest 2>&1 | head -1) + sigue=\$(\$SB 'ls' 2>/dev/null | tr '\n' ' ') + echo \"lee: \$lee\" + echo \"borrar: \$borra\" + echo \"escribir: \$esc\" + echo \"intacto: \$sigue\" + case \"\$borra\$esc\" in *[Rr]ead-only*|*Failure*) echo 'VEREDICTO: acotada ✓' ;; + *) echo 'VEREDICTO: ⚠ ESCRIBE — no usar'; exit 1 ;; esac" + ;; +*) sed -n '/^# Uso:/,/verificar/p' "$0"; exit 2 ;; +esac