Files
takana/scripts/harkaq/README.md
T
sergioandClaude Opus 4.8 24c6ab455e harkaq: §4 corregido (política POR FICHERO) + Q2 con método y primeros datos reales
CORRECCIÓN ESTRUCTURAL del §4 (otra herencia del marco nix). El Draft 2 decía
`fs_ro: [store paths de la clausura]`. Falso: esos paths NO existen dentro del
sandbox. `Sandbox::bwrap_args` apila las deps con --overlay-src y las FUNDE en
el mismo /usr que el rootfs Alpine, a propósito, para que el compilador las
encuentre sin plumbing de flags. Medido:

  /usr/include/zlib.h   (dep DECLARADA)        dev=63 ino=13641973
  /usr/include/stdio.h  (Alpine, NO declarado) dev=63 ino=4196788
  /usr/include          (el directorio)        dev=61 ino=15  ← overlayfs, uno

Una regla sobre /usr concede las dos ⇒ la evidencia no valdría nada. Ni `dev`
distingue. ⇒ La clausura se enumera FICHERO A FICHERO (computable: en el host
sabemos qué aporta cada dep). Dos consecuencias: `list` ≠ `ro` (pkgconf NECESITA
escanear /usr/lib/pkgconfig, pero listar no es leer; Landlock separa READ_DIR de
READ_FILE) y máscara por tipo (derechos de-sólo-dir sobre un fichero ⇒ EINVAL y
la regla entera se cae).

Piezas nuevas: harkaq-policy.sh (deriva la clausura, D1) + q2-runtime-base.sh
(responde Q2 midiendo, no adivinando) + harkaq-exec con `list`/máscara por tipo.

Q2, primeros datos con un build REAL en el sandbox REAL contra la dep zlib:
  estado: Impuro | canario: True | contador kernel: 3
      fs.read_file /usr/bin/env
      fs.read_file /usr/lib/libz.so.1.3.2   ← la zlib de ALPINE!

El build se linkaba contra la zlib de Alpine en vez de la dep declarada (que
aporta libz.a). Sin harkaq eso pasa en verde y el artefacto queda dependiendo de
Alpine. ES EXACTAMENTE EL BUG DEL §0, cazado en un build real.
Piso duro del runtime base: /bin/sh → /bin/busybox → /lib/ld-musl (sin eso ni el
canario corre ⇒ el mínimo es "lo que hace falta para que el canario pueda correr").

Dos rastrillos: el canario no puede depender de un binario (`cat` moría en
/bin/cat ⇒ ahora `read _ < $CANARY`, sólo builtins) ni vivir en /tmp (la política
concede `rw /tmp` ⇒ el canario caía DENTRO de la clausura y no denegaba nada).
El segundo lo cazó D9 MISMO: el lector se negó a certificar en vez de decir
Hermetico. El canario funcionando como se diseñó, sobre un bug de quien lo diseñó.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:20:39 -04:00

8.3 KiB

harkaq — banco de pruebas y piezas

Las piezas (Fase 1)

Fichero Qué es Dónde corre
harkaq-audit.c el lector de evidencia; emite el Verdict JSON host, junto a hammerd; necesita CAP_AUDIT_READ
harkaq-exec.c aplica la política y ejecuta el builder dentro de bwrap, estático (rootfs musl)
harkaq-run.sh encadena lector + bwrap + exec + canario host
mkdir -p ~/.cache/harkaq
gcc -O1 -Wall -o ~/.cache/harkaq/harkaq-audit harkaq-audit.c
gcc -O1 -Wall -static -o ~/.cache/harkaq/harkaq-exec harkaq-exec.c
sudo setcap cap_audit_read,cap_audit_control+ep ~/.cache/harkaq/harkaq-audit   # 1 vez

printf 'ro /usr\nro /bin\nro /lib\nro /lib64\nro /etc\n' > /tmp/p.txt
./harkaq-run.sh /tmp/p.txt '/bin/echo hola'

setcap se pierde al recompilar el lector: hay que repetirlo. Es a propósito que el privilegio viva en un helper chico y no en hammerd (§8 Q1c): CAP_AUDIT_* es superficie de más para el daemon entero.

El hito de Fase 1 (2026-07-15)

limpio  {"estado":"Hermetico","canario_visto":true,"contador_kernel":1,"denials":[]}
impuro  {"estado":"Impuro","canario_visto":true,"contador_kernel":2,
         "denials":[{"blockers":"fs.read_file","path":"/home/sergio/hammer/README.md",}]}

Canario visto en ambos (la evidencia está ganada, no supuesta), contador del kernel coherente en ambos, y el impuro nombra el path exacto. Falta el acto real: derivar la política de la clausura de una receta de verdad (harkaq-policy) y contrastar una receta de-Alpinizada contra una que no lo está.

Sutileza: lo que DAC ya bloquea es invisible

Un acceso que los permisos Unix rechazan no genera registro de Landlock — el hook del LSM ni llega a correr. Descubierto probando con /root/.bashrc (drwx------): el escenario "impuro" dio Hermetico porque DAC lo bloqueó antes.

No rompe la afirmación de D2 (si DAC lo bloqueó, el build tampoco lo usó ⇒ Hermetico sigue siendo cierto), pero acota el diagnóstico: harkaq no te dice que el build lo intentó. Para la de-Alpinización da igual — el rootfs Alpine es todo world-readable — pero al escribir tests hay que elegir paths que DAC permita, o el control no controla nada.


Banco de pruebas

Ver docs/16-harkaq-jaula.md. Acá viven los experimentos que sostienen las afirmaciones del SDD: si el doc dice "medido", el que lo midió está en este directorio.

q1-audit.c — el gate del proyecto (Q1, cerrado 2026-07-15)

Responde: ¿llegan los registros AUDIT_LANDLOCK_* a un lector? Todo harkaq cuelga de eso — su producto es la evidencia, no la jaula.

gcc -O1 -Wall -o q1-audit q1-audit.c
for s in same-exec new-exec new-exec-logon; do sudo ./q1-audit $s; done

Necesita root (CAP_AUDIT_READ para leer el multicast, CAP_AUDIT_CONTROL para encender el audit). Enciende audit_enabled=1 globalmente y no lo restaura — reversible, no persiste al reboot (no toca el cmdline), pero deja dmesg más ruidoso mientras tanto.

Resultados en el laptop (ABI 10, kernel 7.2.0-rc3)

Escenario Registros ACCESS Qué prueba
same-exec 2 el kernel loguea por defecto sin execve
new-exec 0 flags por defecto ⇒ ciego tras execve — el caso real de harkaq
new-exec-logon 4 LOG_NEW_EXEC_ON lo arregla

Los registros traen blockers=fs.read_file path="/etc/passwd" dev="nvme1n1p6" ino=4458369 y el exe= que lo intentó. Análisis completo en docs/16-harkaq-jaula.md §3.4.

Por qué aborta con código 4

Si no puede confirmar audit_enabled=1, aborta en vez de reportar 0 registros. No es paranoia: la primera corrida dio 0 en los tres escenarios y parecía confirmar la hipótesis. Era un bug del instrumento (AUDIT_GET con NLM_F_ACK → el ACK llegaba antes que la respuesta → nunca se encendía el audit). Se detectó sólo porque el control (same-exec) también dio 0, y eso era imposible.

Es el modo de falla de harkaq sobre su propio banco de pruebas: un sistema cuyo producto es la ausencia de algo no falla ruidosamente — dice que todo está bien. De ahí sale D9 (el canario deliberado), y de ahí sale la regla de este directorio: todo experimento lleva un control que debe dar positivo. Un experimento sin control no mide, tranquiliza.

q1b-attrib.c — userns y atribución (Q1b, cerrado 2026-07-15)

Responde: ¿el lector del host ve las denegaciones de adentro de bwrap, y se puede atribuir cada registro a SU build con la granja en paralelo?

gcc -O1 -Wall -o ~/.cache/harkaq/q1b q1b-attrib.c   # NO compilar bajo /tmp: --tmpfs /tmp lo tapa
sudo ~/.cache/harkaq/q1b

Lanza 2 víctimas concurrentes en bwrap --unshare-all (los ns de Sandbox::bwrap_args), cada una con un canario de nonce distinto, y escucha desde el host. Las víctimas bajan a $SUDO_UID: el despliegue real es lector privilegiado + builds sin privilegio.

Resultado: los registros cruzan el userns/pidns/mountns, y el canario con nonce atribuye sin ambigüedad (AAAA=1 BBBB=1, cero cruces). Análisis en docs/16-harkaq-jaula.md §3.5.

Los tres rastrillos de este banco, para el próximo que lo toque

  1. No compilar bajo /tmp: el sandbox hace --tmpfs /tmp y tapa el binario.
  2. El canario necesita un punto de montaje escribible. Con --ro-bind / /, bwrap no puede crear /harkaq-canary-X en la raíz de sólo lectura (el sandbox real usa --tmp-overlay / y no tendría el problema). Va bajo /tmp, y por eso /tmp no entra en la clausura de la víctima.
  3. Root + --unshare-all no atraviesa un home drwx------. bwrap mapea sólo uid 0→0; los ficheros de uid 1000 quedan sin mapear y CAP_DAC_OVERRIDE en un userns sólo vale sobre uids mapeados. De ahí drop_privs().

Por qué también aborta con código 4

Mismo gate que q1-audit, más uno: si alguna víctima no sale con status 0, aborta en vez de contar registros. La corrida que lo motivó imprimió "NADA cruzó ⇒ harkaq-audit no puede vivir en el host: replantear" cuando bwrap ni siquiera había podido ejecutar el binario. Una conclusión arquitectónica falsa, a partir de cero estímulo.

Van tres veces en una sola sesión (NLM_F_ACK, el redirect a /dev/null, el exec) que un cero resultó ser el instrumento y no el kernel. Un veredicto que no verifica su propio estímulo es el falso Hermetico de D9, pero del lado del banco.

harkaq-policy.sh + q2-runtime-base.sh — la clausura y el runtime base

harkaq-policy.sh <dir-dep>... deriva la política de la clausura (D1: no se escribe, se deriva). Enumera fichero a fichero —no por directorio— porque el sandbox funde las deps y Alpine en el mismo /usr (§4.1). Emite list de los directorios para que pkgconf/configure puedan escanear sin poder leer lo no declarado.

q2-runtime-base.sh responde Q2 midiendo, no adivinando: corre un build real en el sandbox real con política = clausura y deja que las denegaciones nombren el runtime base. Se itera con BASE_EXTRA (una línea por regla), y cada línea que se añade ahí debe venir de una denegación que el kernel denunció, nunca de una corazonada — si se define de más, la política concede Alpine entero y la evidencia deja de valer.

export BASE_EXTRA='ro /bin/sh
ro /bin/busybox
ro /lib/ld-musl-x86_64.so.1'
HARKAQ_TIMEOUT=25 scripts/harkaq/q2-runtime-base.sh

Resultado (2026-07-15, contra la dep zlib del store):

estado: Impuro | canario: True | contador kernel: 3
    fs.read_file /usr/bin/env
    fs.read_file /usr/lib/libz.so.1.3.2      ← ¡la zlib de ALPINE, no la dep declarada!

Dos rastrillos más, aprendidos acá

  1. El canario no puede depender de un binario. Con cat $CANARY moría en /bin/cat (applet de busybox fuera de la clausura) y el probe no probaba nada. Ahora usa sólo builtins: read _ < $CANARY || true.
  2. El canario no puede vivir en /tmp. La política concede rw /tmp (es el HOME del build), así que un canario ahí cae DENTRO de la clausura, se lee sin problema y no deniega nada ⇒ SinEvidencia eterno. Va en la raíz, donde la política no alcanza por construcción.

El segundo lo cazó D9 mismo: el lector se negó a certificar (SinEvidencia) en vez de decir Hermetico. El canario funcionando como se diseñó, sobre un bug de quien lo diseñó.