Gates del piloto de captura privada¶
Estado inicial: NO-GO. Todos los casos usan cuentas, consentimientos, imágenes, marcadores y resultados sintéticos. Una prueba con foto real invalida la evidencia de este checklist.
Convención de evidencia¶
Crear /var/tmp/eppa-private-capture/<release-id>/ fuera del repo. Cada gate
debe guardar comando, timestamp UTC, commit/digest, salida sanitizada y
responsable. Nunca guardar cookies, tokens, claves, object keys completos,
bytes/base64, nombres ni IDs de sujetos.
G0 — Alcance y aprobaciones¶
- [ ] El commit/digest está fijado y el worktree coincide con el release.
- [ ]
EPPA_PRIVATE_CAPTURE_ENABLED=falsepor defecto. - [ ] La adenda sigue bloqueando uso real hasta tener todas las firmas.
- [ ] El hostname piloto no comparte tráfico, cookies ni config con legacy.
- [ ] No se modificó
.git,.git/lfs,.gitattributesni fuentes de pacientes.
G1 — Dependencias y configuración fail-closed¶
docker run --rm "$IMAGE_DIGEST" \
python3 -c 'import boto3, cryptography, sqlalchemy; print("runtime-deps-ok")'
docker run --rm "$IMAGE_DIGEST" \
sh -c 'test "${EPPA_PRIVATE_CAPTURE_ENABLED:-false}" = false'
- [ ] Producción rechaza arranque sin
EPPA_ENV=prod, cookie secure, JWT explícito, tres secret files o KEK de exactamente 32 bytes. - [ ] TTL mayor a 86400, origen no HTTPS, bucket vacío o límites inválidos rechazan arranque.
- [ ] Ningún secreto aparece en
docker inspect, HTML, bundle Next o logs.
G2 — Bucket y privilegio mínimo¶
Con un objeto de texto sintético bajo pc/<org>/<session>/probe:
- [ ] identidad runtime puede Put/Get/Delete y listar sólo
pc/; - [ ] otro prefijo y cualquier otro bucket responden
AccessDenied; - [ ] acceso anónimo y URL sin autenticación fallan;
- [ ] identidad runtime no puede crear bucket/usuario/policy/lifecycle;
- [ ] identidad admin no está montada en API/worker;
- [ ] versioning, object lock y replication están desactivados;
- [ ] lifecycle exportado coincide con
minio-lifecycle.json; - [ ] bucket y prefijo difieren de todos los destinos de backup.
G3 — Auth, tenant y consentimiento¶
Ejecutar al menos:
cd fronts/eppa
pytest -q python/tests/auth/test_private_capture_pilot.py
- [ ] no autenticado:
401o404sin revelar que el piloto existe; - [ ] autenticado sin grant, grant vencido o rol incorrecto: fail-closed;
- [ ] org ajena:
404, nunca403revelador; - [ ] falta/retiro/contexto distinto de consentimiento: ninguna sesión ni transición, aunque el cliente omita o falsifique headers;
- [ ] origen distinto del único allowlisted:
403; - [ ] ETag/revisión vieja:
409, sin sobrescribir datos; - [ ] eventos de auditoría sólo contienen UUID pseudónimo, acción, estado y request ID sanitizados.
G4 — Canonicalización y cifrado¶
- [ ] sólo JPEG/PNG decodificables dentro de bytes/pixels permitidos;
- [ ] MIME, extensión y magic bytes inconsistentes se rechazan;
- [ ] EXIF/ICC/text chunks y filename original desaparecen al re-encode;
- [ ] dimensiones/transformación canónica son deterministas;
- [ ] AES-GCM usa nonce único y AAD con org/session/view/artifact/schema/key;
- [ ] cambiar un byte, AAD, tag o key version hace fallar autenticación;
- [ ] el objeto MinIO no contiene JPEG/PNG legible ni metadatos del paciente;
- [ ] una excepción no registra bytes, base64, AAD completo, key o KEK.
G5 — Flujo sintético completo¶
El Playwright focal navega a /{locale}/pilot/private-capture y usa una imagen
generada en test, nunca un archivo recibido de una persona:
- capability autorizada y aviso de consentimiento/TTL visibles;
- crear sesión con consentimiento sintético vigente;
- guardar en orden anterior, posterior, lateral izquierda y lateral derecha;
- validar referencia/calibración y marcadores sintéticos por vista;
- finalizar, anonimizar y revisar candidate;
- aprobar/rechazar según el fixture;
- pedir purge inmediato;
-
verificar UI
purged, DB tombstone y cero objetos/versiones/multipart. -
[ ] refresh, doble click, request duplicado y respuesta fuera de orden son idempotentes;
- [ ] caída del worker/storage conserva estado recuperable sin doble candidato;
- [ ] ni sessionStorage, localStorage, IndexedDB, cache HTTP ni screenshot de Playwright contiene la imagen.
G6 — TTL, purge y no-backup¶
- [ ] reloj falso crea una sesión a TTL y el worker la toma al vencer;
- [ ] consentimiento retirado y purge manual tienen prioridad inmediata;
- [ ] purge borra original, normalizado, temporales, candidato y multipart;
- [ ]
HEADy listado administrativo confirman ausencia antes dedeleted; - [ ] fallo transitorio reintenta idempotente; fallo persistente produce
purge_failedsanitizado y alerta; - [ ] objeto más viejo que
TTL + 30 mindispara alerta; - [ ]
backup_restore_drill.pyrechaza bucket y prefijo privados:
cd fronts/eppa
EPPA_BACKUP_DIR=/tmp/eppa-private-backup-negative \
EPPA_MINIO_BUCKET=eppa-private-capture-pilot \
python3 scripts/backup_restore_drill.py --dry-run backup
# esperado: exit != 0 y mensaje de exclusión; eliminar el directorio temporal
- [ ] una restauración de DB no vuelve a crear ni hace descargable un objeto purgado.
G6.1 — Límite de replay MATLAB y fixture¶
El archive aprobado del piloto demuestra únicamente la cadena de custodia de cuatro imágenes anonimizadas y sus snapshots sanitizados. No es, por sí solo, una entrada del oráculo MATLAB: el oráculo de 61 métricas acepta solamente el atlas sintético versionado y sus coordenadas/provenance verificables.
- [ ] el validator del archive confirma orden, hashes y ausencia de referencias raw, URL, object key, paciente o consentimiento;
- [ ] el replay MATLAB se ejecuta exclusivamente contra el atlas sintético
guided-landmark-atlas.v1y su reporte real de 61 métricas; - [ ] las cuatro excepciones de ángulos quedan evaluadas sólo mediante el
contrato
matlab_angle_exceptions, sin copiar ni inferir resultados desde un candidate; - [ ] ninguna aprobación de candidate, aun sintético, se presenta como validación clínica ni como paridad MATLAB de un caso de persona.
G7 — Blue/green y rollback¶
- [ ] health de
eppa.firemandeveloper.comyeppa2.firemandeveloper.comguardados antes del cambio; - [ ] candidato por digest inicia sin tráfico y readiness pasa;
- [ ] migración expand-only funciona con color anterior activo;
- [ ] smoke sintético directo al candidato pasa con flag off y luego on;
- [ ]
nginx -tpasa antes del switch atómico; - [ ] smoke por hostname, métricas y health legados pasan después;
- [ ] conmutar al color previo tarda menos que la ventana aprobada;
- [ ] rollback no hace downgrade DB y purga la sesión sintética;
- [ ] no se ejecutó
scripts/deploy.sh.
G8 — Observabilidad e incidente¶
- [ ] alertas verificadas: purge atrasado/fallido, orphan, multipart, worker stale, drift de policy/lifecycle/public access y aparición en backup;
- [ ] búsqueda negativa en logs/export/traces no encuentra base64, filename, object key, subject, consent evidence ni secretos;
- [ ] revocación de credenciales runtime produce
AccessDeniedy alerta; - [ ] simulacro documenta disable flag, rollback, purge y escalamiento sin copiar la imagen sintética.
Gate final¶
Un QA independiente firma cada bloque con evidencia. Cualquier casilla abierta mantiene NO-GO. Incluso con todos los gates técnicos verdes, una captura real requiere las aprobaciones legales/clínicas explícitas de la adenda; este checklist por sí solo no la autoriza.