Tres piezas nuevas, las del §4 del SDD:
- harkaq-audit.c lector de evidencia; HOST junto a hammerd, CAP_AUDIT_READ;
espera el canario (que revela el domain=), junta las
denegaciones de ESE dominio, cruza contra el contador del
kernel al liberarse, y emite el Verdict JSON de 3 estados.
- harkaq-exec.c aplica la política y execea el builder; DENTRO de bwrap,
estático (rootfs musl). Negocia el ABI por syscall (D7:
<min rechaza, <7 avisa SIN EVIDENCIA) y pone
LOG_NEW_EXEC_ON + TSYNC.
- harkaq-run.sh encadena lector+bwrap+exec, pone el canario con nonce y
espera a que el lector confirme que escucha (--ready-fd)
antes de arrancar el build: sin esa barrera el canario se
emitiría sin nadie escuchando y daría SinEvidencia por una
carrera, no por un problema real.
El hito, con comandos sintéticos sobre el kernel real (ABI 10):
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":"…/README.md",…}]}
Canario visto y contador coherente en ambos ⇒ los 3 estados de D9 funcionan.
Q1c contestada en la práctica: el privilegio vive en un helper chico con
`setcap cap_audit_read,cap_audit_control`, NO en hammerd entero.
Sutileza medida: lo que DAC ya bloquea es INVISIBLE para harkaq (el hook del
LSM no llega a correr). Descubierto porque el escenario "impuro" con
/root/.bashrc (drwx------) dio Hermetico: DAC lo bloqueó antes que Landlock.
No rompe D2 (si DAC lo bloqueó, el build tampoco lo usó ⇒ Hermetico es cierto)
pero acota el diagnóstico, y al escribir tests hay que elegir paths que DAC
permita o el control no controla nada.
Falta para cerrar Fase 1: harkaq-policy derivando de la clausura real + el
contraste receta-de-Alpinizada vs. receta-que-no. El rebuild del kernel
(SECURITY_LANDLOCK) NO bloquea el hito: sólo hace falta para que harkaq corra
DENTRO de hammer; el laptop ya está en ABI 10.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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
- No compilar bajo
/tmp: el sandbox hace--tmpfs /tmpy tapa el binario. - El canario necesita un punto de montaje escribible. Con
--ro-bind / /, bwrap no puede crear/harkaq-canary-Xen 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/tmpno entra en la clausura de la víctima. - Root +
--unshare-allno atraviesa un homedrwx------. bwrap mapea sólo uid 0→0; los ficheros de uid 1000 quedan sin mapear yCAP_DAC_OVERRIDEen 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.