CUTOVER de `sergio.gioser.net`: el último vhost sale de gioser — ya no queda ninguno
`https://sergio.gioser.net/` y `/shuma/` contestan 200 contra 2.29.29.217, con TLS público. **Ningún dominio resuelve ya a gioser.** ⚠ Y el 200 de `/shuma/` había que mirarlo dos veces: el bloque termina en `try_files … /index.html`, así que CUALQUIER ruta inexistente devuelve 200 con la SPA — un `curl -o /dev/null` habría dado el mismo verde con el proxy mal puesto. Lo que decide es el cuerpo (17 bytes: «shuma-gateway ok») y el control es compararlo con lo que el origen viejo sigue sirviendo. Igual con `/shuma/rpc`: las dos máquinas contestan el mismo 400 con el mismo JSON. Lo construido, en orden: las dos recetas sellaron en el worker con los hashes anticipados y salen `statically linked` (nada de cargador, que era lo que le faltaba al binario glibc de gioser) · la cuenta `shuma` declarada con `[[user]]`, uid 967, y el descenso con `setuidgid` en el argv de la Card porque el payload Native de arje NO tiene campo de usuario · la config que no se regenera (`gateway-token`, `identity.x25519`) instalada, y la guarda de la Card la EXIGE en vez de crearla: un token nuevo deja fuera a los clientes ya emparejados · las dos Cards en `cards.d` Y en el genesis de la semilla · las dos recetas y los dos labels declarados en `perfil.servidor`. ⚠ LA SHELL DE LA CUENTA NO ES UN DETALLE: el default de `[[user]]` es `/bin/false` —correcto para gitea o squid, exactamente lo contrario para un demonio que abre PTYs—. Con la shell inerte el servicio arranca, se supervisa, contesta 200 y cada pestaña muere al instante: un fallo que no se ve al desplegar, se ve al usarlo. Va `shell = "/bin/sh"`. `[[user]]` y `[[service]]` están fuera de `hash_inputs`: declarar todo eso no movió ningún ArtifactHash, comprobado antes y después en los dos. Y una corrección al §6.25: para cambiar el VALOR de un rrset no hace falta DELETE+POST (que deja una ventana sin registro). La API tiene `POST /v1/zones/<id>/rrsets/<n>/<t>/actions/set_records`, que es atómica. Con TTL 60 la resolución pública cambió en menos de 8 segundos. El origen de gioser sigue corriendo a propósito, como en §6.26: volver atrás es una llamada. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Delta semántico
Ninguna definición cambió: el cambio es de texto, documentación o datos.
4 archivos · +135 −1
- docs/28-servidor-de-produccion.md +67 −0
- docs/state/targets.toml +11 −1
- recipes/shuma-daemon.toml +36 −0
- recipes/shuma-gateway.toml +21 −0
diff --git a/docs/28-servidor-de-produccion.md b/docs/28-servidor-de-produccion.md
index 43df6e35..cc601bc0 100644
--- a/docs/28-servidor-de-produccion.md
+++ b/docs/28-servidor-de-produccion.md
@@ -2153,6 +2153,73 @@ por criterio ajeno.
De `/var/lib` (8,1 G) se movió sólo el estado de los daemons que viven: **6,1 G son swap** y 1,9 G
el gitea, que ya está mudado desde el §6.14.
+### 6.37 ✅ CUTOVER DE `sergio.gioser.net` — el último vhost, y gioser deja de servir *(2026-09-16)*
+
+**Ya no queda ningún dominio sirviéndose desde gioser.** El último era éste, y lo que lo ataba no
+era el sitio sino su consola: `/shuma/*`. Verificado desde fuera, contra la IP nueva:
+
+```
+ https://sergio.gioser.net/ → 200 408 bytes, la SPA
+ https://sergio.gioser.net/shuma/ → 200 «shuma-gateway ok» ← el gateway, no la SPA
+ https://sergio.gioser.net/ruta-inventada → 200 la SPA (el fallback, que es lo correcto)
+```
+
+⚠ **El `200` de `/shuma/` había que mirarlo dos veces.** El bloque termina en
+`try_files {path} {path}/ /index.html`, así que **cualquier ruta inexistente devuelve 200 con la
+SPA**: un `curl -o /dev/null` que sólo mira el código habría dado el mismo verde con el proxy mal
+puesto. Lo que decide es el CUERPO — 17 bytes que dicen `shuma-gateway ok` — y el control es
+compararlo con lo que el origen viejo sigue sirviendo, byte por byte. Igual con `/shuma/rpc`: las
+dos máquinas contestan el MISMO `400 {"error":"bad json: …"}`.
+
+#### Lo que hubo que construir, en orden
+
+1. **Las dos recetas** (§6.34) sellaron en el worker: `b3:107dfb54…-shuma-gateway` (6,3 M) y
+ `b3:f9b92acc…-shuma-daemon` (5,6 M) — los hashes que `takana hash` había anticipado antes de
+ encolarlas. `file` dice **`statically linked`**: nada de cargador, que era exactamente lo que le
+ faltaba al binario glibc de gioser (y cuyo inodo, además, ya estaba borrado del disco).
+2. **La cuenta, declarada y sin privilegio.** `[[user]] shuma` (uid 967) en la receta del daemon, y
+ el descenso con `setuidgid` dentro del `argv` de la Card, igual que gitea — porque el payload
+ `Native` de arje **no tiene campo de usuario**. Declararlo «como estaba» lo habría puesto de
+ root, y esto es un demonio que abre PTYs y lanza shells.
+3. **⚠ Y la shell de la cuenta NO es un detalle.** El default de `[[user]]` es `/bin/false`, que es
+ lo correcto para gitea o squid y **exactamente lo contrario** para éste: con la shell inerte el
+ servicio arranca, se supervisa, contesta 200… y cada pestaña que alguien abra muere al instante.
+ Un fallo que no se ve al desplegar: se ve al usarlo. Va `shell = "/bin/sh"`.
+4. **La config que no se regenera.** El `gateway-token` (32 B) y `keys/identity.x25519` viajaron a
+ `/var/lib/shuma/.config/shuma/`: generar unos nuevos deja fuera a los clientes ya emparejados.
+ Por eso la guarda de la Card **exige** el token y no lo crea, y dice de dónde sale.
+5. **Las dos Cards** entran en `/etc/arje/cards.d/` **y** en el `genesis` de `/ente/seed.card.json`
+ (son dos preguntas distintas: qué se puede encarnar a pedido y qué arranca solo). La semilla
+ quedó con 13 entradas y vuelve a parsear.
+6. **Declarado en `perfil.servidor`**: los dos paquetes y los dos labels en `servicios`. Entran como
+ PAR — el gateway sin el daemon es un puente a ninguna parte.
+
+`[[user]]` y `[[service]]` están **fuera de `hash_inputs`**, así que declarar todo eso no movió
+ningún ArtifactHash: comprobado en los dos, antes y después.
+
+#### 🔁 El DNS: la API tiene una acción para esto y el §6.25 no la había encontrado
+
+Aquel cutover dejó escrito «un `PUT` sobre el rrset no puede cambiar el tipo: hay que `DELETE` del
+CNAME y `POST` del A». Es cierto para un cambio de TIPO. Para cambiar el VALOR, el `PUT` tampoco
+sirve —`can't update records with this endpoint`— pero existe una acción que lo hace **atómica**:
+
+```
+POST /v1/zones/<id>/rrsets/<nombre>/<tipo>/actions/set_records {"records":[{"value":"…"}]}
+```
+
+Sin ventana sin registro, que es lo que sí tiene el `DELETE`+`POST`. Con TTL 60 la resolución
+pública cambió **en menos de 8 segundos**, y volver atrás es la misma llamada con la IP vieja.
+
+#### El origen sigue vivo, a propósito
+
+Como en §6.26: en gioser `shuma-daemon`, `shuma-gateway` y su bloque de caddy **siguen corriendo**.
+Mudar el nombre y dejar el origen encendido permite volver en un minuto, y no cuesta nada mientras
+la máquina exista. Lo que ya no existe es la dependencia: **ningún dominio resuelve a gioser**.
+
+⚠ Pendiente menor, anotado donde se va a leer: el daemon avisa al arrancar que no encuentra
+`shuma-askpass` («sudo/ssh pedirán la clave por el TTY»). Es otro crate del mismo workspace y es
+otra receta — no bloquea la consola.
+
## 7. Reusar los scripts que ya existen, y no escribir de nuevo
Pedido explícito del usuario. El inventario de lo que ya hace el trabajo:
diff --git a/docs/state/targets.toml b/docs/state/targets.toml
index 43764965..c889dd1f 100644
--- a/docs/state/targets.toml
+++ b/docs/state/targets.toml
@@ -1325,6 +1325,10 @@ paquetes = [
# copiar un directorio — lo que NO es trivial es el binario, ver el encabezado de `recipes/gitea.toml`.
# Su `[[service]]` está en la receta y el perfil lo arranca (abajo, en `servicios`).
"gitea",
+ # `shuma-daemon` / `shuma-gateway` (2026-09-16): la consola remota que sirve `/shuma/*` en
+ # `sergio.gioser.net`. Construidos desde el monorepo de tawasuyu por decisión del usuario, en vez
+ # de mudar el binario glibc de gioser —cuyo inodo, además, ya estaba BORRADO del disco.
+ "shuma-daemon", "shuma-gateway",
# `curl`/`wget`: bajar del mirror y del repo. `takana install --repo https://…` los necesita del
# lado del cliente, y son también la única forma de diagnosticar un origen caído desde la caja.
"curl", "wget",
@@ -1428,4 +1432,10 @@ paquetes = [
# métrica reclama: el latido y la hora. Sus paquetes ya estaban declarados arriba y sellados —lo que
# faltaba era el eslabón que los ARRANCA, que es este. Una caja con `cronie` instalado y sin `crond`
# corriendo se ve idéntica a una con latido, y no rota un log ni respalda nada.
-servicios = ["sshd", "gitea", "caddy", "minga", "squid", "crond", "chronyd"]
+# ⚠ `shuma-daemon` y `shuma-gateway` (2026-09-16): la consola de `sergio.gioser.net`, que es el
+# ÚLTIMO vhost que quedaba atado a gioser (SDD 28 §6.34). Entran como PAR: el gateway sin el daemon
+# es un puente a ninguna parte, y por eso los dos labels van juntos o no va ninguno.
+# La cuenta `shuma` la declara `recipes/shuma-daemon.toml` con `[[user]]`, sin privilegio y con
+# `shell = /bin/sh` —no `/bin/false`—, porque este demonio existe para abrir PTYs: con la shell
+# inerte el servicio arranca, se supervisa, y cada pestaña muere al instante.
+servicios = ["sshd", "gitea", "caddy", "minga", "squid", "crond", "chronyd", "shuma-daemon", "shuma-gateway"]
diff --git a/recipes/shuma-daemon.toml b/recipes/shuma-daemon.toml
index f77d7263..a91d81b0 100644
--- a/recipes/shuma-daemon.toml
+++ b/recipes/shuma-daemon.toml
@@ -24,3 +24,39 @@ compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "static"
flags = ["-p", "shuma-daemon", "--bin", "shuma-daemon"]
+
+# ── LA CUENTA (SDD 30) ──────────────────────────────────────────────────────────────────────────
+# Fuera de `hash_inputs`: declarar esto NO re-hashea shuma-daemon (control corrido, el hash no se
+# movió).
+#
+# ⚠ EN GIOSER CORRE COMO `sergio`, Y ESO NO SE PUEDE TRADUCIR SOLO. El payload `Native` de arje no
+# tiene campo de usuario (SDD 29 §4.0 quinquies), así que una tarjeta escrita «como estaba» lo
+# pondría de ROOT — y esto es un demonio que abre PTYs y ejecuta shells: correrlo de root no es
+# perder una opción, es entregar la máquina. La cuenta es propia y sin privilegio, y el descenso se
+# hace en el comando con `setuidgid`, igual que gitea.
+[[user]]
+name = "shuma"
+uid = 967
+home = "/var/lib/shuma"
+# ⚠ Y LA SHELL NO ES UN DETALLE ACÁ. El default de `[[user]]` es `/bin/false`, que es lo correcto
+# para un demonio (gitea, squid) y **lo exactamente contrario** para éste: shuma-daemon existe para
+# abrir PTYs y lanzar shells. Con `/bin/false` el servicio arranca, se supervisa, contesta… y cada
+# pestaña que alguien abra muere al instante. Un fallo que no se ve al desplegar: se ve al usarlo.
+shell = "/bin/sh"
+
+# ── EL SERVICIO ─────────────────────────────────────────────────────────────────────────────────
+# Las guardas salen 78 NOMBRANDO EL ARREGLO. Es lo que hace la diferencia entre un servicio que
+# reintenta para siempre con «unknown user» y uno que dice qué falta: medido con gitea, cuya imagen
+# arrancaba perfecta y sin el servicio por el que existe.
+#
+# `XDG_RUNTIME_DIR` no es decoración: es donde el daemon pone su socket, y es por ese socket que el
+# gateway le habla. En gioser vale `/run/shuma`, medido en el entorno del proceso vivo.
+[[service]]
+label = "shuma-daemon"
+id = "01M2EKDA00SH0MADAEM0N7X4K2"
+exec = "/bin/busybox"
+argv = ["sh", "-c", "/bin/grep -q '^shuma:' /etc/passwd || { echo 'shuma-daemon: no existe el usuario `shuma` en /etc/passwd — la receta lo declara con [[user]]; la imagen lo compone con `takana users`' >&2; exit 78; }; mkdir -p /run/shuma /var/lib/shuma || exit 78; chown shuma:shuma /run/shuma /var/lib/shuma || exit 78; cd /var/lib/shuma || exit 78; exec /usr/bin/setuidgid shuma /usr/bin/shuma-daemon"]
+envp = [["PATH", "/usr/bin:/bin"], ["HOME", "/var/lib/shuma"], ["USER", "shuma"], ["XDG_RUNTIME_DIR", "/run/shuma"]]
+networking = "full"
+cgroup = "arje.slice/shuma-daemon"
+restart = { initial_ms = 1000, max_ms = 30000 }
diff --git a/recipes/shuma-gateway.toml b/recipes/shuma-gateway.toml
index 39f85cd1..efd2dbb8 100644
--- a/recipes/shuma-gateway.toml
+++ b/recipes/shuma-gateway.toml
@@ -32,3 +32,24 @@ target = "x86_64-linux-musl"
# exactamente lo que le falta hoy al binario de gioser, que es ELF dinámico contra glibc.
link = "static"
flags = ["-p", "shuma-gateway", "--bin", "shuma-gateway"]
+
+# ── EL SERVICIO (SDD 30) ────────────────────────────────────────────────────────────────────────
+# Fuera de `hash_inputs`: declarar esto NO re-hashea el gateway.
+#
+# La cuenta la declara `shuma-daemon` —es la misma— y por eso acá no va otro `[[user]]`: dos recetas
+# declarando la misma cuenta con distinto uid abortan la imagen a propósito (`takana users --merge`).
+#
+# ⚠ DOS GUARDAS, Y LAS DOS SALEN 78 DICIENDO EL ARREGLO:
+# 1. sin el usuario, esto correría de root — y es el puente HTTP hacia un demonio que abre shells;
+# 2. sin `gateway-token` no autentica a nadie. El fichero son 32 bytes que en gioser viven en
+# `~/.config/shuma/gateway-token`; si se genera uno nuevo, los clientes ya emparejados dejan de
+# entrar. Por eso la guarda no lo crea: lo EXIGE y nombra de dónde sale.
+[[service]]
+label = "shuma-gateway"
+id = "01M2EKDA00SH0MAGATEWAY4K21"
+exec = "/bin/busybox"
+argv = ["sh", "-c", "/bin/grep -q '^shuma:' /etc/passwd || { echo 'shuma-gateway: falta el usuario `shuma` — lo declara recipes/shuma-daemon.toml' >&2; exit 78; }; test -s /var/lib/shuma/.config/shuma/gateway-token || { echo 'shuma-gateway: falta /var/lib/shuma/.config/shuma/gateway-token (32 B) — viene de ~/.config/shuma/gateway-token del origen; si generás uno nuevo, los clientes emparejados dejan de entrar' >&2; exit 78; }; cd /var/lib/shuma || exit 78; exec /usr/bin/setuidgid shuma /usr/bin/shuma-gateway"]
+envp = [["PATH", "/usr/bin:/bin"], ["HOME", "/var/lib/shuma"], ["USER", "shuma"], ["XDG_RUNTIME_DIR", "/run/shuma"]]
+networking = "full"
+cgroup = "arje.slice/shuma-gateway"
+restart = { initial_ms = 1000, max_ms = 30000 }