#!/bin/sh # harkaq-run — corre un comando enjaulado y emite su Verdict (SDD 16, Fase 1). # # harkaq-run.sh # # 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 a /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). # Y sin `cat`: dependía de /bin/cat (applet de busybox); fuera de la clausura, el probe moría # antes de probar nada. Sólo builtins: `read` + redirección abre el fichero igual y dispara el # hook del LSM, sin depender de un solo binario del sistema. CMD="read _ < $CANARY || true; $*" # 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