Files
takana/scripts/qorpa/evidence-probe.sh
T
SergioandClaude Opus 5 4a68dfe761 qorpa D9: el canal de evidencia, cableado — la única forma de auditar el montón B
De un binario ajeno no hay fuente que leer. Lo único observable es lo que el
kernel le NIEGA y anota, y hasta acá ese canal existía en el build pero ninguna
instancia lo abría. `hammer qorpa run <id> --evidence` levanta el lector
(`harkaq-audit`) en el HOST —el audit no está namespaceado— antes de que arranque
la instancia, y al terminar imprime uno de tres estados. Los tres, medidos:

  HERMÉTICO     0 denegaciones Y el canario las respalda
  IMPURO        `touch /usr/INTRUSO; mkdir /opt/INTRUSO` →
                  fs.make_reg · /usr     fs.make_dir · /opt
  SIN EVIDENCIA quitándole las capabilities al lector. NO es «limpio»

**El canario es lo que hace que «cero denegaciones» valga algo:** un fichero
donde la política no alcanza; al leerlo, el kernel emite una denegación que
revela el `domain=` de ESTE dominio Landlock, un número que desde fuera no se
adivina. Sin él, `denials=[]` sería el instrumento callado.

**Dos condiciones estructurales, y se FALLA en vez de dar un veredicto vacío:**
con `nesting` no hay Landlock (D9 conflicto 1) ⇒ o anidás o auditás; y sin
`seal_image` la política es `rw /` ⇒ no hay NADA denegable y el veredicto sería
limpio por construcción, no por mérito. No es un defecto de la implementación:
**la evidencia sólo existe donde algo puede ser negado.**

**Un bug del propio instrumento, que sólo salió usándolo:** sin CAP_AUDIT_READ
el kernel RESPONDE que no (`NLMSG_ERROR`/EPERM) y el lector ignoraba esa
respuesta esperando una que no iba a llegar — 8 s por consulta, 16 s en su
compuerta. Como `qorpa run` lo despierta al terminar, moría por señal dentro de
la compuerta **sin emitir nada**: un «no» tardío se parecía demasiado a un
cuelgue. Ahora atiende el NLMSG_ERROR y dice su motivo en 2 s. Y si aun así el
veredicto sale vacío, se reporta con el código de salida del lector, que es el
único dato que queda.

Guardián: `scripts/qorpa/evidence-probe.sh`, con las tres aserciones. La 2 es la
que sostiene a la 1 — sin algo que TIENE que salir sucio, «HERMÉTICO» lo cumple
igual un canal muerto. Comprueba también las capabilities del lector, que **se
pierden en cada recompilación** y son la forma más probable de que el canal
muera en silencio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZjGRqvjWij9dew7NVFu4Q
2026-09-03 21:57:26 +00:00

75 lines
4.4 KiB
Bash
Executable File

#!/bin/sh
# Prueba el canal de evidencia de una instancia qorpa: que distinga HERMÉTICO de IMPURO, y que se
# NIEGUE a opinar cuando no hay nada que medir. ADR 0015 D9 punto 3.
#
# ── POR QUÉ ESTE GUARDIÁN ───────────────────────────────────────────────────────────────────────
# De un binario ajeno no hay fuente que leer: lo único observable es lo que el kernel le NIEGA y
# anota. Si ese canal se rompe —caps que se pierden al recompilar, un kernel sin audit, una
# política que ya no deniega nada—, el síntoma es **un veredicto limpio**. O sea que el modo de
# fallo de este instrumento es *parecer que todo está bien*, y por eso hace falta medirlo con algo
# que TIENE que salir sucio.
#
# ── LAS TRES ASERCIONES ─────────────────────────────────────────────────────────────────────────
# 1. una instancia que no toca nada prohibido → HERMÉTICO
# 2. la MISMA instancia tocando /usr y /opt → IMPURO, y con esas rutas
# 3. una instancia sin `seal_image` → se NIEGA a correr (no hay nada denegable, así que
# un `denials=[]` sería el instrumento mintiendo)
# La 2 es la que sostiene a la 1: sin ella, «HERMÉTICO» lo cumple igual un canal muerto.
set -eu
QORPA_ROOT="${HAMMER_QORPA_ROOT:-/var/lib/hammer/qorpa}"
HAMMER="${HAMMER_BIN:-./target/release/hammer}"
ID="${1:-arch-probe}"
export HAMMER_QORPA_ROOT="$QORPA_ROOT"
INST="$QORPA_ROOT/instances/$ID"
[ -f "$INST/instance.toml" ] || { echo "no existe la instancia $ID en $QORPA_ROOT" >&2; exit 2; }
AUDIT="${HARKAQ_BIN:-$HOME/.cache/harkaq}/harkaq-audit"
if [ ! -x "$AUDIT" ]; then
echo "falta $AUDIT — gcc -O1 -Wall -o $AUDIT scripts/harkaq/harkaq-audit.c" >&2; exit 2
fi
# El `setcap` se pierde en CADA recompilación del lector, y ésa es la forma más probable de que el
# canal muera sin que nadie se entere: por eso se comprueba acá y no se descubre en el veredicto.
case "$(getcap "$AUDIT" 2>/dev/null)" in
*cap_audit_read*) : ;;
*) echo "el lector no tiene capabilities (se pierden al recompilar):" >&2
echo " sudo setcap cap_audit_read,cap_audit_control+ep $AUDIT" >&2; exit 2 ;;
esac
LOG=$(mktemp -d); trap 'rm -rf "$LOG"' EXIT
# A fichero y no a `$( )`: la sustitución espera al PIPE, no al proceso, y un descendiente vivo la
# cuelga para siempre (misma cicatriz que `pressure-vessel-probe.sh`).
corre() { timeout -k 5 180 "$HAMMER" qorpa run "$ID" --evidence -- /bin/sh -c "$1" > "$2" 2>&1 </dev/null || true; }
sed -i 's/^seal_image = false/seal_image = true/' "$INST/instance.toml"
echo "== 1. sin tocar nada prohibido"
corre 'true' "$LOG/limpio"
grep -q "HERMÉTICO" "$LOG/limpio" || { tail -5 "$LOG/limpio"; echo "FALLA: no dio hermético" >&2; exit 1; }
echo " HERMÉTICO ✓"
echo "== 2. tocando la imagen sellada"
corre 'touch /usr/INTRUSO 2>/dev/null; mkdir /opt/INTRUSO 2>/dev/null; true' "$LOG/sucio"
grep -q "IMPURO" "$LOG/sucio" || { tail -8 "$LOG/sucio"; echo "FALLA: el canal no vio nada — un canal muerto se ve así" >&2; exit 1; }
for ruta in "/usr" "/opt"; do
grep -q " $ruta\$" "$LOG/sucio" || { tail -8 "$LOG/sucio"; echo "FALLA: falta la denegación de $ruta" >&2; exit 1; }
done
grep -E "fs\.(make_reg|make_dir)" "$LOG/sucio" | sed 's/^/ /'
echo " IMPURO con las dos rutas ✓"
echo "== 3. sin seal_image: tiene que NEGARSE, no dar limpio"
sed -i 's/^seal_image = true/seal_image = false/' "$INST/instance.toml"
corre 'true' "$LOG/sinsello"
sed -i 's/^seal_image = false/seal_image = true/' "$INST/instance.toml"
# Se busca la LÍNEA de veredicto (`evidencia: …`), no la palabra «denials»: el propio mensaje de
# rechazo la contiene al explicar por qué se niega, y la primera versión de este guardián falló
# contra su propia explicación.
if grep -q "^evidencia:" "$LOG/sinsello"; then
tail -5 "$LOG/sinsello"; echo "FALLA: opinó sobre una política que no puede denegar nada" >&2; exit 1
fi
grep -q "seal_image" "$LOG/sinsello" || { tail -5 "$LOG/sinsello"; echo "FALLA: no explicó por qué se niega" >&2; exit 1; }
echo " se niega y dice por qué ✓"
echo "OK: el canal de evidencia distingue hermético de impuro, y no opina cuando no puede medir"