gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 604Rust 56Shell 36Markdown 281227 Definitions

puerta 5 cerrada: repo.gioser.net sirve el repo FIRMADO, y el guion aprende cómo se recarga esta caja

`takana install zsh --repo https://repo.gioser.net --trust ./trust --require-signed` ⇒
`release: trusted (by release)`, 1331 ficheros en 0,286 s, zsh 5.9 corriendo. 173 paquetes
anclados, 0 sin ancla, certificado CN=repo.gioser.net de Let's Encrypt.

Tres cosas que sólo se supieron al poder entrar como root, y que el guion ahora sabe:

· `caddy reload` NO funciona acá: el Caddyfile lleva `admin off` y el reload habla por ese admin
  (de ahí que el :2019 estuviera siempre cerrado). La recarga real es `arjectl restart caddy` —
  caddy es un Ente de arje con Restart{1000,30000}, no un proceso suelto.
· Un restart levanta los 19 dominios o ninguno, así que el guion saca un TESTIGO de la config
  previa (acá gitea.gioser.net), espera a que responda, y si no vuelve restaura y revierte.
  Comprobado después: los 8 dominios vivos siguen en pie.
· El certificado tarda más que la comprobación: el guion dio «todavía no responde» y la misma URL
  daba 200 segundos después. El mensaje ahora manda reintentar antes de ir a leer logs.

Y dos fallos míos, medidos: backticks dentro de comillas y de un heredoc sin entrecomillar se
EJECUTAN (salió un `journalctl: not found` en mitad de un mensaje de ayuda), y `[ -n "$X" ] && echo`
como última orden mata el guion entero bajo `set -e`.

El espejo provisional sin firmar de takana.gioser.net/repo se borró: con el firmado en pie, un
origen sin firma no es un origen.

a66346fb05b2ccc61802b48a57542ce1cc0f3348 · by Sergio · (1 month ago) · parent e8524eab9 · browse the tree at this commit

Semantic delta

No definition changed: this change is text, documentation or data.

2 files · +104 −43

diff --git a/docs/28-servidor-de-produccion.md b/docs/28-servidor-de-produccion.md
index d02d52a4..e59c973c 100644
--- a/docs/28-servidor-de-produccion.md
+++ b/docs/28-servidor-de-produccion.md
@@ -563,12 +563,12 @@ curl -H "Authorization: Bearer $TOKEN" https://api.hetzner.cloud/v1/zones
 curl -H "Authorization: Bearer $TOKEN" https://api.hetzner.cloud/v1/zones/986350/rrsets
 ```
 
-Hoy el nombre resuelve y **no sirve nada**: `http://repo.gioser.net` da el 308 global de Caddy y el
-HTTPS muere en el handshake (`tlsv1 alert internal error`) porque no hay site block ⇒ no hay
-certificado. Falta el vhost, que es root y va a mano: `scripts/servidor/publicar-repo.sh` publica y
-firma en `/srv/repo`, comprueba el lazo y **imprime el bloque sin tocar el Caddyfile** — en esta
-caja Caddy sirve 19 dominios y su config ya acumula 20 `.bak`; un guion que la reescribe a ciegas es
-cómo se tiran 19 sitios para levantar uno.
+Durante unas horas el nombre resolvió y **no sirvió nada** —`http://` daba el 308 global de Caddy y
+el HTTPS moría en el handshake (`tlsv1 alert internal error`) porque sin site block no hay
+certificado—, hasta que se pudo entrar como root a la caja. **Cerrado el 2026-09-21** con
+`scripts/servidor/publicar-repo.sh --install-vhost`, que publica y firma en `/srv/repo`, añade el
+bloque y lo aplica; por defecto **imprime el bloque sin tocar el Caddyfile**, porque en esta caja
+Caddy sirve 19 dominios y su config ya acumula 20 `.bak`.
 
 ⚠ **Publicar no es construir, y `repo-perfil.sh` no lo acotaba.** `pack --build` es instantáneo con
 cache-hit y una **compilación entera** si el artefacto no está sellado. Medido al ensayar el guion:
@@ -630,33 +630,50 @@ disfraza de «al día»: lo dice, sale ≠ 0 y **conserva el último estado buen
 lab. El aviso llega a cualquiera; el `install` que lo resuelve sigue siendo reproducir desde fuente
 (§5.4). Por eso `outdated` **no actualiza**: instalar es verificar, y eso no se cuelga de un cron.
 
-### 5.8 El espejo provisional en `takana.gioser.net/repo` — y por qué va SIN FIRMAR *(2026-09-21)*
-
-Mientras `repo.gioser.net` espera su vhost (root, §5.7), el catálogo está servido bajo el sitio que
-Caddy ya atiende: `/work/sergio/gioser-web/takana/repo`, que es un árbol **escribible sin root** y
-con certificado válido. 173 paquetes, y `install zsh` desde la URL pública tarda **0,22 s** (1331
-ficheros) y el binario corre.
-
-**El índice va sin firma a propósito, y eso es lo único honesto que se podía hacer.** La clave de
-release vive en `/root/.config/takana/keys` de la caja y desde la jaula no se alcanza. Firmarlo con
-otra clave habría sido *peor* que no firmarlo: una autoría inventada se lee igual que una real, y es
-exactamente la «firma sin gestión de claves» que `repo-perfil.sh` se niega a hacer (SDD 19 §3.2). Se
-le quitó al índice la firma de la clave de prueba con la que se había ensayado. Control:
-`--require-signed` lo **rechaza** — `Error: el repo no tiene firma de release`.
-
-Lo que sí conserva su valor: cada entrada lleva su `expected_hash` anclado, así que `install`
-reconstruye desde fuente y **compara**. Lo que no se puede verificar acá es *quién* armó el
-catálogo, no si el binario sale de la fuente que dice.
-
-Dos cuidados que lleva puestos, porque vive dentro del clon de git de otra web:
-
-- **`repo/.gitignore` con `*`**: el directorio se ignora a sí mismo. Un `git add -A` en `gioser-web`
-  no se lleva 173 `.tkn` por delante. Comprobado: `git status` del clon sigue limpio.
-- **`hermanas.py` no lo borra**: sólo escribe `takana/index.html` y `hifas/index.html`, no arrasa el
-  directorio, así que el espejo sobrevive a `publicar-webs.sh`.
-
-Se quita con `rm -rf /work/sergio/gioser-web/takana/repo` el día que `repo.gioser.net` sirva el
-firmado. **No es un espejo del ADR 0014**: un origen sin firma no cuenta como origen alternativo.
+### 5.8 Puerta 5, de verdad: `repo.gioser.net` sirve el repo firmado *(2026-09-21)*
+
+```
+$ takana install zsh --repo https://repo.gioser.net --trust ./trust --require-signed
+release: trusted (by release)
+repo: bajados 6 .swm de https://repo.gioser.net
+caché: artefacto ya en el store hash=b3:0be3630d… name=zsh
+  → hidratados 1331 archivo(s)                                    real 0m0,286s
+```
+
+**173 paquetes anclados, 0 sin ancla**, índice firmado con la clave de release de la caja y
+certificado `CN=repo.gioser.net` de Let's Encrypt. Los 2 que faltan (`os-release`, `rust`) no
+estaban sellados y el `SKIP_UNSEALED` los saltó en vez de ponerse a compilar.
+
+**Lo que había que descubrir para poder aplicarlo, y no estaba escrito en ningún sitio:**
+
+- **`caddy reload` NO sirve en esta caja.** El Caddyfile lleva `admin off` (por eso el `:2019`
+  siempre estuvo cerrado) y el `reload` de Caddy habla justamente por ese admin. La recarga real es
+  **`arjectl restart caddy`**: caddy es un Ente de arje (`/etc/arje/cards.d/caddy.json`,
+  `Restart{initial:1000,max:30000}`), no un proceso suelto.
+- **Un restart no es un reload: levanta los 19 dominios o ninguno.** Por eso el guion saca un
+  **testigo** de la config PREVIA —el primer dominio con nombre propio, acá `gitea.gioser.net`— y
+  espera a que vuelva a responder; si no vuelve, restaura la copia y revierte. Comprobado después:
+  los 8 dominios vivos siguen en pie (`gitea` 301 a `git`, el resto 200).
+- **El certificado tarda más que la comprobación.** El guion dio `✗ todavía no responde` y la misma
+  URL respondía **200 unos segundos después**: Caddy pide el certificado en la primera petición.
+  El mensaje ahora dice «reintentá el curl antes de buscar nada» en vez de mandar a leer logs.
+
+**El espejo provisional se quitó.** Mientras no hubo root, el catálogo estuvo servido **sin firmar**
+bajo `takana.gioser.net/repo` (un árbol escribible sin root, con certificado válido). Funcionó
+—`install zsh` en 0,22 s— y sirvió para probar el mecanismo, pero un origen sin firma no es un
+origen: con `repo.gioser.net` firmado en pie, se borró. Lo que quedó de aquello vale la pena
+recordarlo: **firmar con una clave que no es la de release habría sido peor que no firmar**, porque
+una autoría inventada se lee igual que una real; se publicó sin firma y con `--require-signed`
+rechazándolo, que es lo único honesto que se podía hacer sin la clave.
+
+⚠ **Sigue siendo un solo origen** (ADR 0014). La caja se sirve a sí misma; la lista de `--repo` de
+los clientes debería llevar también el Storage Box. El primer origen roto, si es el único, es el
+último.
+
+⚠ **Y el aviso periódico no está cableado en la caja a propósito**: `/var/lib/hammer/installed.json`
+no existe ahí —ningún paquete se instaló por `takana install`, el sistema viene de imágenes—, así
+que `avisar-actualizaciones.sh` sólo diría «nada instalado» cada 30 minutos. Se enchufa el día que
+la caja consuma su propio repo, que es el §6.46 visto desde el otro lado.
 
 ---
 
diff --git a/scripts/servidor/publicar-repo.sh b/scripts/servidor/publicar-repo.sh
index b164d3cf..e34ee84b 100755
--- a/scripts/servidor/publicar-repo.sh
+++ b/scripts/servidor/publicar-repo.sh
@@ -75,7 +75,10 @@ export KEY
 # 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
-    TAKANA_WORK="${TAKANA_WORK_ROOT:-/var/tmp/takana-publicar-work}"
+    # `/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)"
@@ -86,6 +89,40 @@ 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"
@@ -99,6 +136,12 @@ if [ "$INSTALL_VHOST" = "1" ]; then
         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" <<EOF
@@ -121,16 +164,16 @@ EOF
         fi
         echo "  ✓ la config valida"
 
-        if ! caddy reload --config "$CADDYFILE"; then
-            # El reload falló: 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 reintenta el reload con la buena.
+        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"
-            caddy reload --config "$CADDYFILE" >/dev/null 2>&1 || true
-            echo "!! el reload falló — restaurada $COPIA y recargada" >&2
+            aplicar_config >/dev/null 2>&1 || true
+            echo "!! no pude aplicar la config — restaurada $COPIA y revertida" >&2
             exit 1
         fi
-        echo "  ✓ recargado"
+        echo "  ✓ aplicada"
     fi
 fi
 
@@ -150,8 +193,9 @@ else
         cat <<EOF
   ✗ el vhost está puesto y Caddy recargado, pero https://$NOMBRE todavía no responde.
     Lo normal a mirar, en este orden:
-      · el CERTIFICADO: Caddy lo pide en la primera petición y tarda unos segundos; si falla,
-        `journalctl -u caddy` (o el log de arje) lo dice — casi siempre es el :80 tapado o el DNS.
+      · el CERTIFICADO: Caddy lo pide en la PRIMERA petición y puede tardar unos segundos más de
+        los que espera esta comprobación — reintentá el curl antes de buscar nada. Si de verdad
+        falla, el log de caddy (arjectl status caddy) lo dice: casi siempre el :80 tapado o el DNS.
       · el DNS: $NOMBRE tiene que resolver a ESTA caja (hoy: A → 2.29.29.217).
       · el repo: $DESTINO tiene que tener el index.json que acaba de firmarse.
 EOF