#!/bin/sh # publicar-repo.sh — deja el repo FIRMADO servido en `repo.gioser.net`. Se corre EN LA CAJA, como # root (la clave privada de release vive en `/root/.config/takana/keys/`, SDD 28 §2116). # # ── QUÉ HACE ──────────────────────────────────────────────────────────────────────────────────── # 1. publica la clausura del perfil en `/srv/repo` con `repo-perfil.sh` (que ABORTA si no está la # clave: un índice firmado con una efímera que nadie conoce no lo verifica nadie), # 2. comprueba que el índice verifica contra `trust/`, # 3. dice si Caddy ya sirve el nombre, y **si no, imprime el bloque y para**. # # ── EL CADDYFILE: SÓLO CON `--install-vhost`, Y CON VUELTA ATRÁS ─────────────────────────────── # Por defecto NO lo toca: imprime el bloque y para. En esta caja Caddy sirve 19 dominios y su # Caddyfile ya acumula 20 `.bak` (SDD 28 §1); un guion que lo reescribe a ciegas es cómo se tiran # 19 sitios para levantar uno. # # repo.gioser.net { # root * /srv/repo # file_server browse # } # # Con `--install-vhost` lo hace, pero **el orden importa y es lo único que lo vuelve seguro**: # copia de seguridad → añade → `caddy validate` → sólo entonces `caddy reload`. Si validate o # reload fallan, **restaura la copia y recarga con ella** antes de salir: el estado final es el que # había, no uno a medias. Y si el nombre ya está en la config, no añade nada (idempotente). # # El DNS ya está: `repo` A → 2.29.29.217, ttl 60, en la zona de Hetzner (alta del 2026-09-21). # # ⚠ **Un solo origen es exactamente lo que el ADR 0014 existe para no tener.** Esta caja sirviéndose # a sí misma está bien como camino normal, pero el `--repo` de los clientes debería llevar también # el Storage Box: el primer origen roto, si es el único, es el último. # # Uso: sudo scripts/servidor/publicar-repo.sh [perfil] # sudo scripts/servidor/publicar-repo.sh --install-vhost [perfil] # Env: PERFIL (def servidor) · STORE (def /store) · DESTINO (def /srv/repo) # NOMBRE (def repo.gioser.net) · TRUST (def ./trust) · CADDYFILE (def /etc/caddy/Caddyfile) set -eu ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT" INSTALL_VHOST=0 case "${1:-}" in --install-vhost) INSTALL_VHOST=1; shift ;; esac PERFIL="${1:-${PERFIL:-servidor}}" STORE="${STORE:-/store}" DESTINO="${DESTINO:-/srv/repo}" NOMBRE="${NOMBRE:-repo.gioser.net}" TRUST="${TRUST:-$ROOT/trust}" TAKANA="${TAKANA:-$ROOT/target/release/takana}" CADDYFILE="${CADDYFILE:-/etc/caddy/Caddyfile}" # La guarda pregunta por lo que de verdad hace falta —poder LEER la clave— y no por `id -u == 0`, # que es sólo su causa habitual: así el guion se puede ensayar contra un repo y una clave de # prueba sin ser root, y el día que la clave viva en otro sitio no hay que tocar la condición. KEY="${KEY:-$HOME/.config/takana/keys/release.ed25519}" [ -r "$KEY" ] || { echo "!! no puedo leer la clave de release: $KEY" >&2 echo " En la caja vive en /root/.config/takana/keys/ ⇒ esto va como root (SDD 28 §2116)." >&2 exit 2 } export KEY [ -x "$TAKANA" ] || { echo "!! falta $TAKANA (cargo build --release)" >&2; exit 2; } # ⚠ El binario TIENE que traer el arreglo de los parches múltiples y del target_bin (SDD 28 §5.5): # con uno anterior, las 16 recetas con ≥2 parches se publican irreproducibles y 8 paquetes más # (bash, sed, tar, parted, dhcpcd, squid, wpa_supplicant, zsh) prometen un binario que no existe. "$TAKANA" outdated --help >/dev/null 2>&1 || { echo "!! este $TAKANA es anterior al arreglo del SDD 28 §5.5 (no conoce 'outdated')." >&2 echo " Reconstruilo —cargo build --release— antes de publicar, o el repo sale roto." >&2 exit 2 } # ⚠ ROOT NO ESCRIBE EN EL ÁRBOL DEL USUARIO. `work_root` sale de `dirname(store)/work`, o sea `/work` # con el store en `/store` — y ahí `sources` y `out` son ENLACES a `/work/sergio/work/…`, que es del # usuario. Publicar como root dejaría directorios de root en su árbol y el siguiente build suyo # moriría con un permiso denegado que no menciona esto. Ya pasó con otro guion: root hizo `git pull` # en el clon de tawasuyu, dejó 24 entradas suyas dentro del `.git` y congeló el clon para el dueño # sin que nada fallara del lado de root (ver `publicar-webs.sh`). Así que el scratch va aparte. if [ "$(id -u)" = "0" ] && [ -z "${TAKANA_WORK:-}" ]; then # `/var/tmp` no existe en la caja (sistema mínimo), así que se elige el que haya en vez de # crear un directorio que ese sistema decidió no tener. [ -d /var/tmp ] && _tmp=/var/tmp || _tmp=/tmp TAKANA_WORK="${TAKANA_WORK_ROOT:-$_tmp/takana-publicar-work}" export TAKANA_WORK install -d -m 755 "$TAKANA_WORK" echo "==> como root: scratch de build en $TAKANA_WORK (no en el árbol del usuario)" fi echo "==> publicando el perfil '$PERFIL' en $DESTINO" install -d -m 755 "$DESTINO" PERFIL="$PERFIL" STORE="$STORE" REPO="$DESTINO" TRUST="$TRUST" TAKANA="$TAKANA" \ "$ROOT/scripts/repo-perfil.sh" "$PERFIL" # ── CÓMO SE APLICA UNA CONFIG NUEVA, QUE NO ES `caddy reload` ─────────────────────────────────── # El Caddyfile de la caja lleva `admin off` (por eso el `:2019` está cerrado), y **`caddy reload` # habla por el admin**: sin él no recarga nada y falla. Lo que hay es un supervisor: caddy es un # Ente de arje (`/etc/arje/cards.d/caddy.json`, `Restart{initial:1000,max:30000}`), así que la # recarga de verdad es `arjectl restart caddy` y esperar a que vuelva a escuchar. # # ⚠ Y no alcanza con que el proceso vuelva: hay que comprobar que **sigue sirviendo lo de antes**. # Un restart con la config nueva levanta los 19 dominios o ninguno, así que el testigo es un # dominio que YA funcionaba —sacado de la config previa— y si no responde, se revierte. TESTIGO="${TESTIGO:-}" aplicar_config() { if caddy reload --config "$CADDYFILE" 2>/dev/null; then echo " · recargado por el admin de Caddy" return 0 fi command -v arjectl >/dev/null 2>&1 || { echo ' !! caddy reload falló (¿admin off?) y no hay arjectl para reiniciar' >&2 return 1 } echo " · el admin de Caddy está apagado ⇒ reinicio por el supervisor (arjectl)" arjectl restart caddy >/dev/null 2>&1 || return 1 [ -n "$TESTIGO" ] || return 0 i=0 while [ "$i" -lt 25 ]; do if curl -sk -o /dev/null -m 3 "https://$TESTIGO/"; then echo " · testigo $TESTIGO responde: Caddy volvió con todo lo que ya servía" return 0 fi i=$((i + 1)); sleep 1 done echo " !! $TESTIGO no volvió a responder tras el reinicio" >&2 return 1 } if [ "$INSTALL_VHOST" = "1" ]; then echo echo "==> vhost de Caddy en $CADDYFILE" [ -w "$CADDYFILE" ] || { echo "!! no puedo escribir $CADDYFILE" >&2; exit 2; } command -v caddy >/dev/null || { echo "!! no encuentro el binario 'caddy' en el PATH" >&2; exit 2; } if grep -qE "^[[:space:]]*(https?://)?$NOMBRE[[:space:],{]" "$CADDYFILE"; then echo " = $NOMBRE ya está en la config; no toco nada" else COPIA="$CADDYFILE.antes-de-$NOMBRE-$(date -u +%Y%m%dT%H%M%SZ)" cp -p "$CADDYFILE" "$COPIA" echo " copia de seguridad: $COPIA" # El testigo sale de la config que YA funcionaba: el primer dominio con nombre propio. [ -n "$TESTIGO" ] || TESTIGO=$(grep -oE "^[a-z0-9][a-z0-9.-]+\.[a-z]+" "$COPIA" | head -1) # Sin `|| true` esto mataría el guion con `set -e` cuando no hay testigo: el `&&` con la # condición falsa devuelve 1 y `set -e` no distingue «no aplica» de «falló». [ -n "$TESTIGO" ] && echo " testigo para comprobar que no rompí nada: $TESTIGO" || true # Un site block va al FINAL: las opciones globales de Caddy tienen que ser lo primero del # fichero, así que añadir al final nunca las invalida. cat >> "$CADDYFILE" </dev/null 2>&1; then cp -p "$COPIA" "$CADDYFILE" echo "!! la config NO valida con el bloque añadido — restaurada $COPIA, no recargué" >&2 caddy validate --config "$CADDYFILE" >/dev/null 2>&1 \ || echo "!! y la ORIGINAL tampoco valida: eso ya venía roto, miralo antes de recargar" >&2 exit 1 fi echo " ✓ la config valida" if ! aplicar_config; then # No se pudo aplicar: Caddy sigue con lo que tenía cargado, pero el fichero en disco ya # no es eso. Se restaura para que el próximo arranque no levante una config que no probó # nadie, y se vuelve a aplicar la buena. cp -p "$COPIA" "$CADDYFILE" aplicar_config >/dev/null 2>&1 || true echo "!! no pude aplicar la config — restaurada $COPIA y revertida" >&2 exit 1 fi echo " ✓ aplicada" fi fi echo echo "==> ¿lo sirve Caddy?" if curl -fsS -m 10 "https://$NOMBRE/index.json" -o /dev/null 2>/dev/null; then echo " ✓ https://$NOMBRE/index.json responde" echo echo " probalo desde cualquier hub (necesita el lab: install reproduce desde fuente):" echo " takana install zsh --repo https://$NOMBRE --trust ./trust --require-signed" echo " y el aviso periódico:" echo " REPO=https://$NOMBRE scripts/servidor/avisar-actualizaciones.sh" else if [ "$INSTALL_VHOST" = "1" ]; then # Decir «falta el vhost» justo después de haberlo puesto es la clase de mensaje que manda a # buscar el problema donde no está. cat <