Files
takana/scripts/harkaq/harkaq-run.sh
T
sergioandClaude Opus 4.8 4eecba06f4 harkaq: Fase 1 — la cadena corre de punta a punta (Hermetico / Impuro sobre kernel real)
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>
2026-07-15 15:04:27 -04:00

71 lines
3.2 KiB
Bash
Executable File

#!/bin/sh
# harkaq-run — corre un comando enjaulado y emite su Verdict (SDD 16, Fase 1).
#
# harkaq-run.sh <fichero-política> <cmd...>
#
# Encadena las tres piezas: harkaq-audit escuchando en el HOST, bwrap poniendo los namespaces,
# harkaq-exec poniendo la política dentro. El canario (D9) lleva un nonce único por corrida: es
# lo que distingue "build limpio" de "lector ciego" —que producen los mismos bytes— y de paso
# es la clave primaria que atribuye los registros a ESTE build y no a otro de la granja (§3.5).
#
# Variables: HARKAQ_BIN (default ~/.cache/harkaq), NONCE (default: derivado del pid).
set -eu
BIN="${HARKAQ_BIN:-$HOME/.cache/harkaq}"
POLICY="$1"
shift
NONCE="${NONCE:-$$-$(date +%s 2>/dev/null || echo 0)}"
CANARY="/tmp/harkaq-canary-$NONCE"
# El canario es un fichero que EXISTE en el sandbox y que la política deja deliberadamente
# fuera de la clausura. Su fuente da igual (sólo importa que el open rebote): usamos algo
# mínimo y siempre presente.
CANARY_SRC="$(mktemp)"
echo "harkaq-canary $NONCE" > "$CANARY_SRC"
trap 'rm -f "$CANARY_SRC" "$FIFO" 2>/dev/null || true' EXIT
# Arrancar el lector ANTES del build y esperar a que confirme que escucha. Sin esta barrera,
# las primeras denegaciones —el canario entre ellas— se emitirían sin nadie escuchando y el
# veredicto sería SinEvidencia por una carrera, no por un problema real.
FIFO="$(mktemp -u)"
mkfifo "$FIFO"
VERDICT="$(mktemp)"
trap 'rm -f "$CANARY_SRC" "$FIFO" "$VERDICT" 2>/dev/null || true' EXIT
"$BIN/harkaq-audit" --canary "$CANARY" --timeout "${HARKAQ_TIMEOUT:-120}" --ready-fd 3 \
3>"$FIFO" >"$VERDICT" &
AUDIT_PID=$!
read -r _ < "$FIFO" || true # bloquea hasta que el lector esté escuchando
# El probe del canario va DENTRO del `sh -c`, o sea DESPUÉS del execve: es el único sitio donde
# valida lo que tiene que validar (que el logging post-exec funciona). Si lo hiciera harkaq-exec
# antes de ejecutar, sería una denegación same-exec —que el kernel loguea por defecto— y no
# probaría nada sobre el caso que importa (§3.4).
# Sin redirects: `>/dev/null` exige /dev en la clausura y, si falta, sh falla en la redirección
# y el probe no llega a correr — el error medía otra cosa (§3.5, nota de método).
CMD="cat $CANARY; $*"
# harkaq-exec y la política van bajo /tmp (tmpfs) y NO bajo /: con `--ro-bind / /` la raíz es de
# sólo lectura y bwrap no puede crear ahí el punto de montaje. Que vivan en /tmp no los mete en
# la clausura: se leen y se ejecutan ANTES del restrict_self — la jaula empieza en el `sh -c`.
set +e
bwrap --unshare-all --ro-bind / / --tmpfs /tmp \
--ro-bind "$CANARY_SRC" "$CANARY" \
--ro-bind "$BIN/harkaq-exec" /tmp/harkaq-exec \
--ro-bind "$POLICY" /tmp/harkaq.policy \
--die-with-parent \
/tmp/harkaq-exec --policy /tmp/harkaq.policy -- /bin/sh -c "$CMD"
BUILD_RC=$?
set -e
# Gracia para que llegue el `deallocated` (el kernel lo emite con retraso: RCU/workqueue), y
# después despertar al lector si sigue esperando — pasa cuando el build ni arrancó y el canario
# nunca se emitió. Sin esto se comería el timeout entero por un fallo trivial.
sleep 2
kill -TERM "$AUDIT_PID" 2>/dev/null || true
wait "$AUDIT_PID" 2>/dev/null || true
cat "$VERDICT"
echo "[harkaq-run] build rc=$BUILD_RC" >&2
exit $BUILD_RC