SDD 28 §3.6-3.8: la caja corre takana puro — y el muro era la puerta de enlace, no el arranque
Puerta 1 pasada: PID 1 `arje-zero`, kernel 7.1.2 propio, `/dev/sda3 -> /store` y `/dev/sda4 -> /var/lib/hammer` montados por etiqueta, IP y salida por `netup`, y se entra por SSH. Una caja Hetzner sin una línea de Debian debajo. Escribir los 7,1 G comprimidos: ~17 s. Queda escrito lo que costó, que es lo reusable: - **El kernel del corpus no sirve para hcloud** (`linux.toml` apaga SCSI; el disco es virtio-SCSI). `linux-generic` sí, y su `CONFIG_SCSI_VIRTIO=y` no está en la receta: lo pone `defconfig` y sólo se ve en el `.config` que el artefacto publica. La receta dice lo que se cambió; el `.config` sellado dice lo que quedó. - **La hidratación pisa `/sbin/init`** con el de busybox: la imagen habría arrancado perfecta con el init equivocado. - **La puerta de enlace de Hetzner está FUERA del prefijo** (IP `/32`, gw `172.31.1.1`). Sin ruta on-link el default se rechaza y la caja queda con IP y sin salida — arranca, levanta sshd y no se puede entrar. Nunca había aparecido porque netup sólo se probaba contra el slirp de QEMU, que da un /24 con la puerta dentro: el único entorno de prueba no contenía el caso. - **Un binario dentro del `product-rootfs` está congelado**: arreglar netup no llegaba a la imagen hasta declararlo raíz del perfil. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
This commit is contained in:
@@ -232,6 +232,139 @@ consultarlo el día que se compra.
|
|||||||
por clave igual). El journal del LXC muestra fuerza bruta a root desde IPs random; esta caja está
|
por clave igual). El journal del LXC muestra fuerza bruta a root desde IPs random; esta caja está
|
||||||
igual de expuesta. Se cierra cuando se instale takana puro, o antes si se queda días con Debian.
|
igual de expuesta. Se cierra cuando se instale takana puro, o antes si se queda días con Debian.
|
||||||
|
|
||||||
|
### 3.6 La imagen del `perfil.servidor`, armada y validada *(2026-09-10)*
|
||||||
|
|
||||||
|
**Receta del ensamblado** (todo con scripts que ya existían, §7):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. base con init: el product-rootfs sellado (trae /sbin/init -> /usr/bin/arje-zero)
|
||||||
|
cp -al /mnt/cosecha/store/5011955a…-product-rootfs/. $D/
|
||||||
|
# 2. userland del perfil encima
|
||||||
|
scripts/hydrate-profile.py servidor --into $D --store /mnt/cosecha/store --keep
|
||||||
|
# 3. ⚠ restaurar el init (ver abajo) y meter la clave
|
||||||
|
ln -sf /usr/bin/arje-zero $D/sbin/init ; cp github5.pub $D/root/.ssh/authorized_keys
|
||||||
|
# 4. imagen
|
||||||
|
ROOTFS=$D STAGE=/mnt/cosecha/.install-stage KERNEL=store/23f1cc43…-linux-generic/boot/bzImage \
|
||||||
|
CMDLINE="console=ttyS0,115200 console=tty0 root=/dev/sda2 rw init=/usr/bin/arje-zero" \
|
||||||
|
./scripts/install-image.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado: **86 nodos, 30.500 ficheros, 2,0 G de rootfs → imagen de 7.172 M (2,1 G en disco, sparse)**.
|
||||||
|
|
||||||
|
#### El kernel correcto es `linux-generic`, y `linux.toml` NO sirve para hcloud
|
||||||
|
|
||||||
|
El disco de una caja hcloud lo maneja **`virtio_scsi`** (driver `sd`, disco `sda`) — no `virtio_blk`.
|
||||||
|
`recipes/linux.toml` **apaga SCSI explícitamente** (`-d SCSI`) ⇒ su kernel arranca y **no ve el
|
||||||
|
disco**. `linux-generic`, `linux-metal` y `linux-metal-dual` sí sirven.
|
||||||
|
|
||||||
|
⚠ **Y ninguna receta nombra `SCSI_VIRTIO`.** Está encendido igual, porque `make defconfig` lo trae y
|
||||||
|
`-e SCSI -e BLK_DEV_SD` lo mantiene. Se supo mirando el `.config` que `linux-generic` **publica en su
|
||||||
|
artefacto** (`/boot/config-7.1.2-generic`), no leyendo la receta. La receta dice lo que se cambió;
|
||||||
|
sólo el `.config` sellado dice lo que quedó.
|
||||||
|
|
||||||
|
#### ⚠ La hidratación PISA `/sbin/init`, y el resultado arranca igual
|
||||||
|
|
||||||
|
`busybox` entra en la clausura del perfil y su `/sbin/init -> ../bin/busybox` **gana por llegar
|
||||||
|
después**, borrando el `-> /usr/bin/arje-zero` del product-rootfs. La imagen habría arrancado
|
||||||
|
perfecta **con el init de busybox**: no takana puro, y sin un solo error. Se tapa por las dos puntas
|
||||||
|
— restaurando el symlink al ensamblar **y** con `init=/usr/bin/arje-zero` explícito en el cmdline,
|
||||||
|
que es lo que hace gioser y es autoritativo pase lo que pase con el symlink.
|
||||||
|
|
||||||
|
**Y la guarda que existía para esto estaba INVERTIDA** (arreglada en el mismo commit): `[ -e
|
||||||
|
"$ROOTFS/sbin/init" ]` sigue el symlink y lo resuelve contra el *host*, así que el symlink ABSOLUTO
|
||||||
|
a arje-zero daba falso y **el rootfs correcto era rechazado**, mientras que el relativo a busybox
|
||||||
|
—el equivocado— pasaba. Ver [[guardian-que-nunca-fallo]]: una guarda que nunca falló no se sabe si
|
||||||
|
sirve; ésta fallaba al revés.
|
||||||
|
|
||||||
|
#### EXDEV, dos veces en la misma tarde
|
||||||
|
|
||||||
|
`store` y `work/out` son bind-mounts de `/dev/sdb`; `work/` está en `/dev/sdc`. Un hardlink no cruza
|
||||||
|
un montaje **aunque sea el mismo disco**, así que reventaron (1) `hydrate-profile.py` con el destino
|
||||||
|
fuera del montaje del store —que avisa RUIDOSO y bien— y (2) el staging de `install-image.sh`, que
|
||||||
|
lo tenía cableado a `work/`. Ahora `STAGE` es env. Ver [[hardlink-cruza-mount-exdev]].
|
||||||
|
|
||||||
|
#### La validación: arrancó en QEMU reproduciendo virtio-scsi, y se entró por SSH
|
||||||
|
|
||||||
|
```
|
||||||
|
SeaBIOS → GRUB → kernel 7.1.2 propio → EXT4-fs (sda2): mounted → Pivoted into new rootfs
|
||||||
|
→ Run /usr/bin/arje-zero as init process → "arje-zero: despierta como PID 1"
|
||||||
|
→ netup (DHCP) → ssh-keygen -A → sshd
|
||||||
|
```
|
||||||
|
|
||||||
|
Y desde fuera: `ssh -p 2299 root@127.0.0.1` **entra**, `/proc/1/comm` dice `arje-zero`, `ip addr` da
|
||||||
|
la IP que negoció `netup`. Es la puerta 1 del §6 ensayada en local antes de gastar la caja.
|
||||||
|
|
||||||
|
El card `sshd` de `/ente/seed.card.json` ya hacía el trabajo entero sin que hubiera que tocar nada:
|
||||||
|
`sh -c "/usr/bin/netup; /usr/bin/ssh-keygen -A; exec /usr/sbin/sshd -D -e"` — red, claves de host y
|
||||||
|
demonio en una línea. Lo único que hubo que inyectar fue `authorized_keys`.
|
||||||
|
|
||||||
|
#### Un resto del renombre, encontrado por el arranque
|
||||||
|
|
||||||
|
```
|
||||||
|
arje-zero: hammer no corre, sin menú de arranque: No such file or directory (os error 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
**`arje-zero` invoca el binario por el nombre VIEJO (`hammer`).** No es fatal —sigue arrancando— pero
|
||||||
|
el menú de arranque por grafo del [ADR 0010](adr/0010-arranque-grafo-mirada.md) queda muerto. Y viene
|
||||||
|
de tawasuyu, pineado por `commit` en `recipes/arje-zero.toml`, así que **no se arregla desde este
|
||||||
|
repo**: hay que subirlo allá y re-pinear. Se suma a que la imagen tampoco trae `takana` (§3.4).
|
||||||
|
|
||||||
|
### 3.7 ⚠ El muro real: la puerta de enlace de Hetzner está FUERA del prefijo
|
||||||
|
|
||||||
|
La imagen validada en QEMU se escribió a la caja, arrancó… y **no se pudo entrar**. `hcloud` decía
|
||||||
|
`running`, el puerto 22 cerrado, y desde fuera `Connection timed out` — el modo de fallo más caro de
|
||||||
|
todos, porque no hay consola serie en hcloud para mirar.
|
||||||
|
|
||||||
|
La causa, medida entrando al rescue de la propia caja:
|
||||||
|
|
||||||
|
```
|
||||||
|
inet 2.29.29.217/32 scope global eth0
|
||||||
|
default via 172.31.1.1 dev eth0
|
||||||
|
172.31.1.1 dev eth0 scope link ← ESTA es la que faltaba
|
||||||
|
```
|
||||||
|
|
||||||
|
**La IPv4 viene con prefijo `/32` y la puerta (`172.31.1.1`) no pertenece a ninguna red conectada.**
|
||||||
|
`netup` mandaba el default con `RTA_GATEWAY` y scope UNIVERSE y nada más, así que el kernel lo
|
||||||
|
rechazaba con `ENETUNREACH`: la máquina se quedaba **con IP y sin salida**, contestando el DHCP y
|
||||||
|
levantando sshd, sin nada visiblemente roto por dentro.
|
||||||
|
|
||||||
|
**Control causal**, en un netns con una dummy y una `/32`:
|
||||||
|
|
||||||
|
```
|
||||||
|
ip route add default via 172.31.1.1 dev dummy0 ⇒ Error: Nexthop has invalid gateway.
|
||||||
|
ip route add 172.31.1.1 dev dummy0 scope link ⇒ ok
|
||||||
|
ip route add default via 172.31.1.1 dev dummy0 ⇒ ACEPTADO
|
||||||
|
```
|
||||||
|
|
||||||
|
Arreglado con `add_onlink_host_route` (dst `/32`, scope LINK) antes del default.
|
||||||
|
|
||||||
|
**Por qué no había aparecido nunca**: `netup` se validaba contra el DHCP de QEMU slirp, que entrega
|
||||||
|
un `/24` con la puerta dentro de la subred. El escenario que rompe sólo existe en una nube de verdad.
|
||||||
|
Es [[cache-congela-regresiones]] en su otra forma: no es que el test estuviera mal, es que el ÚNICO
|
||||||
|
entorno donde se probaba no contenía el caso.
|
||||||
|
|
||||||
|
**Y destapó un segundo problema de fondo**: el binario de `netup` venía dentro del `product-rootfs`,
|
||||||
|
o sea **congelado en un artefacto sellado del bootstrap** ⇒ arreglar netup no llegaba a la imagen.
|
||||||
|
Por eso `netup` pasa a ser **raíz de `perfil.servidor`**: así la hidratación lo proyecta encima y la
|
||||||
|
imagen lleva el vigente. Verificado por sha256 (imagen `a23df20e…`, product-rootfs `a84b0a0d…`).
|
||||||
|
|
||||||
|
### 3.8 ✅ Puerta 1, pasada *(2026-09-10)*
|
||||||
|
|
||||||
|
```
|
||||||
|
PID 1 : arje-zero kernel : 7.1.2 (linux-generic propio)
|
||||||
|
IP : 2.29.29.217 rutas : default via 172.31.1.1 + 172.31.1.1 scope link
|
||||||
|
montajes : /dev/sda3 -> /store /dev/sda4 -> /var/lib/hammer
|
||||||
|
salida : ping 1.1.1.1 0% pérdida · curl http://1.1.1.1 -> 301 · resolv.conf escrito por netup
|
||||||
|
```
|
||||||
|
|
||||||
|
**Una caja Hetzner Cloud corriendo takana puro**: nuestro kernel, nuestro init, nuestro cliente DHCP,
|
||||||
|
nuestro sshd — sin una línea de Debian debajo. El camino entero es
|
||||||
|
`scripts/servidor-image.sh` + rescue + `dd`, y la escritura de los 7,1 G comprimidos tarda **~17 s**
|
||||||
|
(gioser y la caja están las dos en hel1).
|
||||||
|
|
||||||
|
Lo que la caja **todavía no** tiene, y es el paso 2: `takana` (sin receta, §3.4), el store real (el
|
||||||
|
volumen no se engancha hasta acá) y las otras cuatro raíces `wanted`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. La utilidad de migración: absorber lo VIVO, no lo declarado
|
## 4. La utilidad de migración: absorber lo VIVO, no lo declarado
|
||||||
@@ -385,7 +518,7 @@ del store a propósito. Queda anotado para que se **decida**, no para que se des
|
|||||||
|---|---|
|
|---|---|
|
||||||
| **0.** `perfil.servidor` + este documento + el renombre `hammer-*` de los instaladores | ✅ **2026-09-09** |
|
| **0.** `perfil.servidor` + este documento + el renombre `hammer-*` de los instaladores | ✅ **2026-09-09** |
|
||||||
| **1a.** caja de sacrificio creada — `cx33` (no había `cx53`), hel1, §3.5 | ✅ **2026-09-10** |
|
| **1a.** caja de sacrificio creada — `cx33` (no había `cx53`), hel1, §3.5 | ✅ **2026-09-10** |
|
||||||
| **1b.** imagen del `perfil.servidor` + rescue → `dd` → arranca takana puro (§3) | pendiente |
|
| **1b.** imagen del `perfil.servidor` + rescue → `dd` → arranca takana puro (§3) | ✅ **2026-09-10** — puerta 1 pasada, §3.8 |
|
||||||
| **2.** el servidor se sirve sus paquetes y se actualiza a sí mismo (§5) | pendiente — requiere clave estable + receta de `takana` |
|
| **2.** el servidor se sirve sus paquetes y se actualiza a sí mismo (§5) | pendiente — requiere clave estable + receta de `takana` |
|
||||||
| **3.** mudanza con las 8 puertas del §6, y recetas nuevas del §6.2 | pendiente |
|
| **3.** mudanza con las 8 puertas del §6, y recetas nuevas del §6.2 | pendiente |
|
||||||
| **4.** la utilidad de absorción (§4), estrenándose con esta migración | pendiente |
|
| **4.** la utilidad de absorción (§4), estrenándose con esta migración | pendiente |
|
||||||
|
|||||||
Reference in New Issue
Block a user