tawasuyu

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 Sutichaykuna

setuidgid: receta sellada y declarada — el ultimo applet sin dueno cae

Sellada en b3:0c018b1a y declarada en los tres perfiles cuyas cards la invocan:
servidor, servidor-minimo y escritorio-mirada (ese ultimo por shuma-daemon).

Con esto, de los CINCO applets que el TSV tenia INDULTADOS por no tener dueno:
  halt/poweroff/reboot  → los cubren los shims de arje-botones.sh, y desde hoy
                          el de poweroff baja los servicios de verdad
  switch_root           → lo trae util-linux en /sbin/switch_root (verificado)
  setuidgid             → esto
⇒ ninguno obliga ya a embarcar busybox.

Dos tropiezos que quedan escritos porque son pies con los que cualquiera se
vuelve a tropezar:

1. EL PIN TIENE QUE INCLUIR EL Cargo.lock, no solo el crate. Apunte primero al
   commit que anade crates/setuidgid y el build murio con «cannot update the
   lock file ... because --locked was passed». El build es hermetico
   (--locked --offline), asi que el lockfile del arbol fijado tiene que YA
   traer la entrada del crate nuevo — y yo la habia dejado en el commit
   SIGUIENTE para no mezclar.

2. EL SANDBOX DE BUILD CORRE COMO uid=1001, NO COMO ROOT (medido). Mi primer
   guardian probaba la bajada de privilegios de verdad y era IMPOSIBLE por
   construccion, no por un defecto. Ahora comprueba lo que si se puede a ese
   uid — y la pieza fuerte es la NEGATIVA: que sin ser root se plante con
   rc=127 en vez de seguir, que es justo la mitad que un atacante probaria
   primero. La bajada esta verificada FUERA del sandbox contra el applet de
   busybox como control, y queda dicho en la receta en vez de fingir un control
   que el sandbox no puede correr:
       busybox: uid=74(sshd) gid=74(sshd) groups=74(sshd)
       nuestro: uid=74(sshd) gid=74(sshd) groups=74(sshd)

ce1150bd05ca18491d05f08647261cac9504326b · ruwaq Sergio · (16 h ñawpaq) · tayta 2de22d242 · kay commitpi sach'ata qhaway

Semántico sayk'uy

Mana ima sutichaypas t'ikrakurqanchu: qillqalla, yachachiylla utaq willaylla t'ikrakurqan.

2 qillqakuna · +116 −0

diff --git a/docs/state/targets.toml b/docs/state/targets.toml
index 170b6dad..7a6438fc 100644
--- a/docs/state/targets.toml
+++ b/docs/state/targets.toml
@@ -576,6 +576,12 @@ descripcion = "rootfs del USB mirada: compositor Wayland propio sobre mesa softw
 # el DRM y muere con el dispositivo ocupado.
 servicios = ["seatd", "mirada", "shuma-daemon", "shuma-gateway"]
 paquetes = [
+  # ── BAJAR PRIVILEGIOS (2026-10-22) ───────────────────────────────────────────────────────────
+  # `setuidgid` lo invocan las cards de servicio de este perfil por ruta absoluta: lo necesitan
+  # `gitea`, `minga`, `pacha`, `pacha-secretos` y `shuma-daemon`, porque esos daemons se niegan a
+  # correr como root y arje-zero encarna sus cards COMO root. Hasta hoy lo daba el applet de
+  # busybox y era el ÚLTIMO sin dueño propio; ahora es un binario de 330 KB, estático y sólo libc.
+  "setuidgid",
   "expat-shared", "zlib-shared", "libffi-shared",
   "mesa-swrast", "wayland", "libdrm", "seatd", "libinput", "libudev-zero",
   "libxkbcommon", "pixman", "linux-pam", "mtdev", "libevdev",
@@ -2907,6 +2913,12 @@ hereda = ["cli"]
 # sin hora, sin cortafuegos y sin latido. Cuenta esperada al escribirlas: **5 `wanted`**. Si al mirar
 # `build-state.json` aparece un sexto, es un error de tipeo en un nombre, no una receta nueva.
 paquetes = [
+  # ── BAJAR PRIVILEGIOS (2026-10-22) ───────────────────────────────────────────────────────────
+  # `setuidgid` lo invocan las cards de servicio de este perfil por ruta absoluta: lo necesitan
+  # `gitea`, `minga`, `pacha`, `pacha-secretos` y `shuma-daemon`, porque esos daemons se niegan a
+  # correr como root y arje-zero encarna sus cards COMO root. Hasta hoy lo daba el applet de
+  # busybox y era el ÚLTIMO sin dueño propio; ahora es un binario de 330 KB, estático y sólo libc.
+  "setuidgid",
   # ── lo que existe y está sellado ──────────────────────────────────────────────────────────────
   # `openssh`: el acceso. Es la raíz que convierte «la caja arrancó» en «puedo entrar»; sin ella una
   # instalación remota deja una máquina viva e inalcanzable, que es peor que una que no arrancó.
@@ -3150,6 +3162,12 @@ hereda = ["base"]
 # una opinión sobre qué debe llevar un servidor. Si algún día el instalador deja de cargar el
 # sistema en RAM, este perfil deja de tener motivo.
 paquetes = [
+  # ── BAJAR PRIVILEGIOS (2026-10-22) ───────────────────────────────────────────────────────────
+  # `setuidgid` lo invocan las cards de servicio de este perfil por ruta absoluta: lo necesitan
+  # `gitea`, `minga`, `pacha`, `pacha-secretos` y `shuma-daemon`, porque esos daemons se niegan a
+  # correr como root y arje-zero encarna sus cards COMO root. Hasta hoy lo daba el applet de
+  # busybox y era el ÚLTIMO sin dueño propio; ahora es un binario de 330 KB, estático y sólo libc.
+  "setuidgid",
   # ── el acceso y la red ────────────────────────────────────────────────────────────────────────
   "openssh", "curl", "wget", "wireguard-tools", "nftables", "netup", "tmux",
   # ── lo que sirve ──────────────────────────────────────────────────────────────────────────────
diff --git a/recipes/setuidgid.toml b/recipes/setuidgid.toml
new file mode 100644
index 00000000..794ea2b2
--- /dev/null
+++ b/recipes/setuidgid.toml
@@ -0,0 +1,98 @@
+# setuidgid — baja privilegios y ejecuta. Reemplaza el applet homónimo de busybox.
+#
+# ══ POR QUÉ ENTRA: ERA EL ÚLTIMO SIN DUEÑO ═════════════════════════════════════════════════════
+# El TSV de `docs/plan-botar-busybox.md` lo tenía `INDULTADO` con esta razón: *«la card de gitea
+# hace `setuidgid gitea`»*. Medido el 2026-10-22, no es una card sino **CINCO**, todas por ruta
+# absoluta: `gitea`, `minga`, `pacha`, `pacha-secretos` y `shuma-daemon`. Esos daemons se niegan a
+# correr como root —gitea lo dice en su propio mensaje de error— y arje-zero encarna sus cards
+# COMO ROOT, así que alguien tiene que bajar los privilegios en el medio.
+#
+# Con esto, de los cinco applets indultados quedan: `halt`/`poweroff`/`reboot` (los cubren los
+# shims de `scripts/servidor/arje-botones.sh`) y `switch_root` (lo trae `util-linux` en
+# `/sbin/switch_root`, verificado). ⇒ **ninguno obliga a embarcar busybox.**
+#
+# ══ MISMO PATRÓN QUE netup/hammerd/simi ════════════════════════════════════════════════════════
+# Crate del workspace a un commit fijado, deps vendoreadas, estático con zig cc, sólo libc. La
+# razón de «sólo libc» no es purismo: esto lo invocan cards de servicio, o sea que entra en la raíz
+# de confianza del arranque, y cada dep suya entraría con él.
+name    = "setuidgid"
+version = "0.0.1"
+license = "MIT"
+
+[source]
+# ⚠ **El pin tiene que incluir el `Cargo.lock`, no sólo el crate.** Primero apunté al commit que
+# añade `crates/setuidgid` y el build murió así:
+#     cannot update the lock file … because --locked was passed to prevent this
+# El build es hermético (`--locked --offline`), así que el lockfile del árbol tiene que YA traer la
+# entrada del crate nuevo. La había dejado en el commit SIGUIENTE para no mezclar, y eso rompió el
+# pin. ⇒ en una receta de crate del workspace, el commit fijado debe contener los dos.
+repo   = "https://git.gioser.net/sergio/takana.git"
+commit = "2de22d24279621d3605d93d8e18b520b579d7207"
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "static"
+strip_debug = true
+flags    = ["-p", "setuidgid"]
+
+[build.phases]
+install = '''
+set -e
+mkdir -p /out/usr/bin
+find target/release -maxdepth 1 -type f -name setuidgid -exec cp {} /out/usr/bin/ \;
+[ -x /out/usr/bin/setuidgid ] || { echo "setuidgid: no quedó el binario — NO sello" >&2; exit 1; }
+
+# ⚠ GUARDIÁN 1: estático de verdad. Lo invoca PID 1 por ruta absoluta en el arranque; un NEEDED
+# colgante acá no es un programa que falla, es un servicio que nunca arranca.
+if command -v readelf >/dev/null; then
+  n=$(readelf -d /out/usr/bin/setuidgid 2>/dev/null | grep -c NEEDED || true)
+  [ "${n:-0}" -eq 0 ] || { echo "setuidgid: tiene $n NEEDED — tiene que ser estático" >&2; readelf -d /out/usr/bin/setuidgid | grep NEEDED >&2; exit 1; }
+fi
+
+# ⚠ GUARDIÁN 2 — LO QUE SÍ SE PUEDE COMPROBAR A uid 1001, Y POR QUÉ NO ES LA BAJADA
+#
+# Mi primera versión probaba la bajada de privilegios de verdad, con un usuario de prueba en
+# `/etc/passwd`. **Falla, y la razón es buena:** el sandbox de build corre como **uid=1001**, no
+# como root (medido: `sandbox corre como uid=1001 gid=1001`). Sin root no hay privilegio que bajar,
+# así que ese control es IMPOSIBLE acá por construcción — no por un defecto del programa.
+#
+# ⇒ Se comprueba todo lo que sí se puede, y la pieza fuerte es la NEGATIVA: que sin ser root se
+# plante en vez de seguir. Eso no es un consuelo — es la mitad de la propiedad de seguridad, y es
+# justo la mitad que un atacante probaría primero.
+#
+# La bajada SÍ está verificada, fuera del sandbox y contra el applet de busybox como CONTROL, en la
+# caja viva (2026-10-22):
+#     busybox: uid=74(sshd) gid=74(sshd) groups=74(sshd)
+#     nuestro: uid=74(sshd) gid=74(sshd) groups=74(sshd)
+# idénticos, con los suplementarios bajados (sin el 0 de root). Queda dicho acá en vez de fingir un
+# control que el sandbox no puede correr.
+
+# sin argumentos ⇒ 2 (uso), que es lo que un shell interpreta como «mal invocado»
+/out/usr/bin/setuidgid >/dev/null 2>&1 && { echo "setuidgid: sin argumentos debería fallar" >&2; exit 1; }
+rc=0; /out/usr/bin/setuidgid >/dev/null 2>&1 || rc=$?
+[ "$rc" = 2 ] || { echo "setuidgid: sin argumentos salió $rc, esperaba 2" >&2; exit 1; }
+
+# ⚠ LA PIEZA FUERTE: sin ser root NO ejecuta. Si esto pasara, un usuario cualquiera podría usarlo
+# como trampolín — y el programa parecería funcionar.
+sal=$(/out/usr/bin/setuidgid root /usr/bin/id 2>&1 | head -1)
+case "$sal" in
+  *"hace falta ser root"*) : ;;
+  *) echo "setuidgid: a uid 1001 NO se negó — salida: $sal" >&2; exit 1 ;;
+esac
+rc=0; /out/usr/bin/setuidgid root /usr/bin/id >/dev/null 2>&1 || rc=$?
+[ "$rc" = 127 ] || { echo "setuidgid: la negativa salió $rc, esperaba 127" >&2; exit 1; }
+
+# un usuario inexistente también se rechaza, y el mensaje nombra el arreglo
+sal=$(/out/usr/bin/setuidgid no-existe-jamas /usr/bin/id 2>&1 | head -1)
+case "$sal" in
+  *"no está en /etc/passwd"*) : ;;
+  *"hace falta ser root"*) : ;;   # a uid 1001 corta antes, y está bien
+  *) echo "setuidgid: mensaje inesperado para usuario inexistente: $sal" >&2; exit 1 ;;
+esac
+
+echo "setuidgid: estático (0 NEEDED) · rc=2 sin argumentos · se NIEGA a uid 1001 con rc=127 · rechaza usuario inexistente"
+'''
+
+[deps]
+build = ["binutils", "coreutils"]