qorpa create/recreate/run: la instancia nace sin ver nada
Paso 3 del ADR 0015. El overlay lo monta bwrap dentro de su propio namespace (--overlay-src + --overlay), así que no hace falta root ni se monta nada en el host. `run` entra con --clearenv y con --unshare-net salvo que se declare `network`: el entorno del host TAMBIÉN es una concesión, y lo que no se declara no entra (D2/D7). `--dry-run` imprime el bwrap entero, una línea por concesión, porque una jaula que no se puede leer no se puede auditar. `list` ahora enumera también las instancias con lo que abre cada una — una instancia sin política y una con la pantalla abierta se ven IGUAL desde fuera y no son lo mismo. Los campos del manifiesto van en inglés (regla 4); el ADR los tenía en castellano y quedan corregidos, igual que las rutas images/ e instances/. D3 VALIDADO en la mano, no en el papel: la escritura va al upper, la imagen base no se toca, y `recreate` tira la capa y la instancia sigue siendo la misma. Y se midió la otra mitad del paso 1, que el ADR daba por «lo primero que va a fallar». Falla, sí, pero el veredicto es MEJOR de lo que decía: uid_map: 0 1001 1 · setgroups: deny - apt: el método http hace setgroups para bajar a _apt ⇒ para. Salteándolo con -o APT::Sandbox::User=root baja 34 MB, instala y corre los triggers de dpkg enteros; el único residuo es un AVISO de chown a root:adm. - pacman: chownea el directorio de descarga a `alpm` ⇒ para en duro. Con DownloadUser comentado sincroniza, y tras pacman-key --init/--populate instala y el binario corre. ⇒ subuid no es un muro, es un IMPUESTO: un solo id alcanza para instalar paquetes reales en los dos gestores, y lo que rompe es el chown/setgroups a OTRO id, que cada gestor hace en un sitio distinto. Y quitarlo pide algo que el ADR no decía: bwrap crea el userns con un solo id A PROPÓSITO y no llama a newuidmap, así que además del setcap hay que crear el namespace aparte, mapear el rango y pasárselo con --userns FD. 5 tests nuevos (nace sin concesiones, rechaza imagen vacía, el upper es caché, sin grants la red queda fuera, y que el aviso de wayland no sea tibio). 40/40. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
This commit is contained in:
@@ -4,8 +4,8 @@
|
||||
§Orden de trabajo. Este documento decide la frontera; el código viene después.
|
||||
- **Fecha:** 2026-09-03
|
||||
- **Frontera (a crear):** `hammer qorpa {pull,create,run,export,list,prune}`,
|
||||
`/var/lib/hammer/qorpa/imagenes/<sha256>/`, `/var/lib/hammer/qorpa/instancias/<id>/`,
|
||||
`instancia.toml` (manifiesto), clase de nodo `ajeno` en `build-state.py`.
|
||||
`/var/lib/hammer/qorpa/images/<sha256>/`, `/var/lib/hammer/qorpa/instances/<id>/`,
|
||||
`instance.toml` (manifiesto), clase de nodo `ajeno` en `build-state.py`.
|
||||
- **Superficie:** verbos en inglés, mensajes en castellano — `CLAUDE.md` regla 4. Este ADR nació
|
||||
proponiendo `traer/crear/correr` y se corrigió el 2026-09-03; el nombre `qorpa` sí es quechua.
|
||||
- **Continúa:** [SDD 04](../04-overlay.md) (overlay), [SDD 16](../16-harkaq-jaula.md) (harkaq),
|
||||
@@ -91,9 +91,9 @@ todo fue bien»).
|
||||
Espacio de nombres paralelo, direccionado por digest, **fuera de `hash_inputs` de todo**:
|
||||
|
||||
```
|
||||
/var/lib/hammer/qorpa/imagenes/<sha256>/ imagen — INMUTABLE, verificada por digest
|
||||
/var/lib/hammer/qorpa/instancias/<id>/
|
||||
├── instancia.toml manifiesto — LA VERDAD
|
||||
/var/lib/hammer/qorpa/images/<sha256>/ imagen — INMUTABLE, verificada por digest
|
||||
/var/lib/hammer/qorpa/instances/<id>/
|
||||
├── instance.toml manifiesto — LA VERDAD
|
||||
├── upper/ capa mutable — CACHÉ
|
||||
└── work/ overlayfs
|
||||
```
|
||||
@@ -129,20 +129,22 @@ host «para ahorrar espacio».
|
||||
Si el `upper` con 200 paquetes instalados a mano fuese el activo, tendríamos un blob irreemplazable:
|
||||
justo lo que hammer existe para no tener. La relación correcta es la misma que receta↔artefacto:
|
||||
|
||||
Los campos van en inglés como el resto de la superficie (`CLAUDE.md` regla 4):
|
||||
|
||||
```toml
|
||||
# /var/lib/hammer/qorpa/instancias/juegos/instancia.toml
|
||||
base = "sha256:…" # digest de la imagen, inmutable
|
||||
distro = "fedora-43" # informativo
|
||||
paquetes = ["steam", "mesa-dri-drivers", "mesa-vulkan-drivers"]
|
||||
# /var/lib/hammer/qorpa/instances/games/instance.toml
|
||||
base = "sha256:…" # digest de la imagen, inmutable
|
||||
distro = "arch" # informativo
|
||||
packages = ["steam", "mesa"]
|
||||
|
||||
[concesiones] # POR DEFECTO NADA
|
||||
sockets = ["wayland", "pipewire"]
|
||||
devices = ["/dev/dri", "/dev/ntsync"]
|
||||
directorios = [{ host = "~/Juegos", dentro = "/home/usuario/Juegos", modo = "rw" }]
|
||||
red = true # lo necesita dnf y lo necesita Steam
|
||||
[grants] # POR DEFECTO NADA
|
||||
sockets = ["wayland", "pipewire"]
|
||||
devices = ["/dev/dri", "/dev/ntsync"]
|
||||
dirs = [{ host = "~/Juegos", inside = "/home/user/Juegos", mode = "rw" }]
|
||||
network = true # lo necesita el gestor de paquetes y lo necesita Steam
|
||||
|
||||
[exporta] # capa de transparencia; ver D5
|
||||
binarios = ["steam"]
|
||||
[export] # capa de transparencia; ver D5
|
||||
binaries = ["steam"]
|
||||
apps = ["steam.desktop"]
|
||||
```
|
||||
|
||||
@@ -151,7 +153,7 @@ Consecuencias que se caen solas:
|
||||
descartable.
|
||||
- **Actualizar la base** (Fedora 43 → 44) no es un rebase riesgoso: se cambia el digest y se recrea.
|
||||
- **El respaldo** es el manifiesto (KB), no el `upper` (GB). `respaldo-storagebox.sh` no toca
|
||||
`/var/lib/hammer/qorpa/instancias/*/upper` y eso es correcto, no un olvido.
|
||||
`/var/lib/hammer/qorpa/instances/*/upper` y eso es correcto, no un olvido.
|
||||
- La poda tiene una regla trivial: **un `upper` siempre se puede borrar.**
|
||||
|
||||
### D4 — Granularidad: cuatro tipos, no uno
|
||||
@@ -191,7 +193,7 @@ una línea de comando que no escribimos en el menú del usuario.
|
||||
|
||||
**Capa 2 — se exporta lo declarado.** Si se exporta todo, el `ls` y el `systemctl` de Fedora compiten
|
||||
con los nuestros. Ésa es la falla de Bedrock: **arbitra en tiempo de `exec`**, con heurísticas. Acá
|
||||
la lista `[exporta]` del manifiesto resuelve la ambigüedad **al declarar**. Tiene precedente directo:
|
||||
la lista `[export]` del manifiesto resuelve la ambigüedad **al declarar**. Tiene precedente directo:
|
||||
la hidratación del escritorio ya va por lista explícita (`TARGETS="…"`), nunca por glob.
|
||||
|
||||
**Capa 3 — el grafo lo sabe.** Los nodos exportados entran en `build-state.json` con **clase
|
||||
@@ -232,7 +234,7 @@ la evidencia negativa que hace valioso al subproducto ([SDD 16](../16-harkaq-jau
|
||||
|
||||
**Una imagen ajena no tiene clausura declarada.** Es el primer y único lugar del sistema donde la
|
||||
política hay que **autorarla**. No es un defecto: es una excepción que conviene nombrar y acotar en
|
||||
vez de descubrir. El bloque `[concesiones]` del manifiesto es esa política, **por defecto vacío**, y
|
||||
vez de descubrir. El bloque `[grants]` del manifiesto es esa política, **por defecto vacío**, y
|
||||
se compila al mismo `PolicySpec` que ya consume `harkaq-exec`.
|
||||
|
||||
Un `dnf` que no alcanza `~/.ssh` es un argumento que ninguna distro ofrece.
|
||||
@@ -246,7 +248,7 @@ Dos preguntas que se confunden y tienen respuestas distintas.
|
||||
|
||||
| qué | ¿pineado? | cómo se actualiza |
|
||||
|---|---|---|
|
||||
| el rootfs base | **sí**, por sha256 | cambiar el digest en `instancia.toml` + `hammer qorpa recreate` |
|
||||
| el rootfs base | **sí**, por sha256 | cambiar el digest en `instance.toml` + `hammer qorpa recreate` |
|
||||
| lo que instalás adentro (`dnf install steam`, `pacman -Syu`) | **no, y no puede estarlo** | con el gestor de la imagen, cuando quieras |
|
||||
|
||||
Subir la base de versión es barato **precisamente por D3**: como el manifiesto es la verdad y el
|
||||
@@ -296,12 +298,23 @@ Se escriben acá para que no se descubran en producción.
|
||||
capturar la pantalla y sintetizar teclas**, y el manifiesto debe decirlo en la cara del usuario.
|
||||
Cerrarlo es tarea propia y probablemente el ticket más valioso que sale de este ADR.
|
||||
|
||||
2. **UID mapping.** Sin userns privilegiados, `root` adentro es tu UID afuera — que es la propiedad
|
||||
de seguridad que queremos, pero rompe operaciones que hacen `chown` a UIDs distintos (rpm crea
|
||||
usuarios de servicio). La salida son rangos subuid, y **las piezas ya están en el corpus**:
|
||||
`recipes/shadow.toml` instala `newuidmap`/`newgidmap` y crea `/etc/subuid` vacío. Falta
|
||||
provisionarlo (capacidad de fichero o setuid + rango en `/etc/subuid`) y probarlo con un `dnf
|
||||
install` real. Es la primera cosa que va a fallar.
|
||||
2. **UID mapping — MEDIDO 2026-09-03, y el veredicto es mejor de lo que decía este párrafo.**
|
||||
Sin userns privilegiados, `root` adentro es tu UID afuera. La instancia ve `uid_map: 0 1001 1` y
|
||||
`setgroups: deny`, y eso rompe todo lo que quiera *otro* id. Medido en las dos imágenes curadas:
|
||||
|
||||
| gestor | qué pasa | severidad |
|
||||
|---|---|---|
|
||||
| `apt` (Ubuntu 24.04) | el método http hace `setgroups` para bajar a `_apt` ⇒ `Operation not permitted` | **para**, pero se saltea con `-o APT::Sandbox::User=root`, y entonces baja 34 MB, instala y corre los triggers de dpkg enteros. Único residuo: `W: chown to root:adm … failed`, un **aviso** |
|
||||
| `pacman` (Arch) | chownea el directorio de descarga al usuario `alpm` ⇒ `Invalid argument` | **para en duro**; con `DownloadUser` comentado sincroniza, y tras `pacman-key --init && --populate` instala y el binario corre |
|
||||
|
||||
⇒ **subuid no es un muro, es un impuesto.** Un solo id alcanza para instalar paquetes reales en
|
||||
los dos gestores; lo que rompe es el `chown`/`setgroups` a *otro* id, y cada gestor lo hace en un
|
||||
sitio distinto. Lo que sigue faltando para quitar el impuesto: `newuidmap`/`newgidmap` con
|
||||
`cap_setuid`/`cap_setgid` (`recipes/shadow.toml` los INSTALA pero no los provisiona — el mismo
|
||||
hueco espera en la distro propia) **y** una vía para que bwrap use ese mapa: bwrap crea el userns
|
||||
con un solo id a propósito y no llama a `newuidmap`, así que hay que crear el namespace aparte,
|
||||
mapear el rango y pasárselo con `--userns FD`. `scripts/qorpa/qorpa-preflight.sh` ya mide las dos
|
||||
mitades.
|
||||
|
||||
3. **Segunda cadena de suministro, sin ninguna garantía de hammer.** Sin repro, sin cierre firmado,
|
||||
sin escaneo de licencias. El riesgo no es técnico: es que se normalice. Mitigación = D5 capa 3
|
||||
@@ -355,8 +368,12 @@ Se escriben acá para que no se descubran en producción.
|
||||
|
||||
1. **Provisionar subuid** y probar `dnf install` en una instancia mínima. Es lo primero que falla
|
||||
(§NO-resuelve 2) y define si el resto es fácil o difícil.
|
||||
2. `hammer qorpa pull <url> --sha256` + verificación de digest. Reusa el patrón de `lab-image.sh`.
|
||||
3. Instancia = overlay sobre la imagen + `instancia.toml`; `create` / `recreate` / `run`.
|
||||
2. ✅ **HECHO 2026-09-03.** `hammer qorpa pull <url> --sha256` + verificación de digest antes de
|
||||
desempacar. El rootfs se ancla por estructura (`etc/` + `usr|bin`), con `--subdir` de escape.
|
||||
3. ✅ **HECHO 2026-09-03.** Instancia = overlay sobre la imagen + `instance.toml`; `create` /
|
||||
`recreate` / `run`. El overlay lo monta bwrap (`--overlay-src` + `--overlay`) dentro de su
|
||||
propio namespace: sin root y sin montar nada en el host. `run` entra con `--clearenv` y con
|
||||
`--unshare-net` salvo que se declare `network` — el entorno del host también es una concesión.
|
||||
4. Concesiones → `PolicySpec` de harkaq. Empezar por **nada** y abrir sólo lo declarado.
|
||||
5. Shims + `.desktop` generados (`export`), y clase `ajeno` en `build-state.py`.
|
||||
6. **Steam de punta a punta**, con verificación explícita del bwrap anidado.
|
||||
|
||||
Reference in New Issue
Block a user