Runbook del piloto de captura privada¶
Estado: NO-GO para imágenes reales. Este documento prepara un piloto aislado y verificable; no autoriza subir fotografías de pacientes ni desplegar la funcionalidad.
Bloqueo explícito del despliegue legado¶
El procedimiento existente en scripts/deploy.sh, docker-compose.yml y
deploy/RUNBOOK-eppa2.md no es apto para este piloto:
- reemplaza una única instalación mediante
rsync --deleteycompose up, no implementa blue/green ni rollback por digest; - usa
minio/minio:latest, una identidad root y un volumen compartido; - no crea bucket privado dedicado, política mínima, lifecycle ni worker de purge;
- no excluye por diseño la captura privada de backups;
- no exige
EPPA_ENV=prod,COOKIE_SECURE=trueni basic auth de borde; - el contenedor runtime debe demostrar que puede importar
boto3antes de habilitar almacenamiento.
No ejecutar scripts/deploy.sh para este piloto. No reutilizar el bucket de
restore drill, el hostname público legado, Git, LFS, una carpeta del checkout ni
un backup como almacenamiento temporal.
Arquitectura mínima¶
navegador autenticado y allowlisted
-> TLS + basic auth obligatorio + rate/body limits (host piloto separado)
-> API FastAPI /api/private-capture/* (feature flag default-off)
-> Postgres: grant, consentimiento, metadatos, outbox y auditoría
-> MinIO: bucket dedicado, claves opacas pc/<org>/<session>/<view>/...
-> worker único: finalización/anónimo/purge idempotente
-> lifecycle MinIO de 1 día como defensa, nunca como reloj principal
Las imágenes se normalizan y cifran en el servidor antes de persistirlas. El
navegador no recibe credenciales S3, URLs públicas ni presigned URLs. No se
guarda una imagen en localStorage, IndexedDB, logs, trazas, analytics o el
filesystem del repositorio.
Contrato de configuración¶
El runtime consume los secretos descritos en
deploy/private-capture/SECRETS.md y estas variables:
EPPA_ENV=prodyCOOKIE_SECURE=true;EPPA_PRIVATE_CAPTURE_ENABLED=falsedurante migración y smoke base;EPPA_PRIVATE_CAPTURE_TTL_SECONDS, entre 1 y 86400 en producción;EPPA_PRIVATE_CAPTURE_BUCKET, dedicado y distinto de todo backup;EPPA_PRIVATE_CAPTURE_S3_ENDPOINTyEPPA_PRIVATE_CAPTURE_S3_REGION;EPPA_PRIVATE_CAPTURE_S3_ACCESS_KEY_FILEyEPPA_PRIVATE_CAPTURE_S3_SECRET_KEY_FILE;EPPA_PRIVATE_CAPTURE_KEK_FILEde 32 bytes yEPPA_PRIVATE_CAPTURE_KEY_VERSION;EPPA_PRIVATE_CAPTURE_ALLOWED_ORIGIN, un único origen HTTPS del piloto;EPPA_PRIVATE_CAPTURE_MAX_BYTESyEPPA_PRIVATE_CAPTURE_MAX_PIXELS.
El contrato de consentimiento es eppa-private-fixture-pilot-v1. Una sesión
usa cuatro vistas canónicas: anterior, posterior, lateral izquierda y lateral
derecha. La UI vive en /{locale}/pilot/private-capture.
1. Preflight sin cambios¶
Guardar toda la evidencia en un directorio externo al repo:
export EVIDENCE_DIR="/var/tmp/eppa-private-capture/${RELEASE_ID:?required}"
install -d -m 0700 "$EVIDENCE_DIR"
git rev-parse HEAD | tee "$EVIDENCE_DIR/commit.txt"
git status --short | tee "$EVIDENCE_DIR/git-status.txt"
curl -fsS https://eppa.firemandeveloper.com/api/health \
| tee "$EVIDENCE_DIR/legacy-health-before.json"
curl -fsS https://eppa2.firemandeveloper.com/api/health \
| tee "$EVIDENCE_DIR/eppa2-health-before.json"
Detenerse si el árbol no corresponde al release aprobado, si cualquier
servicio legado está degradado o si el host piloto no tiene TLS y basic auth
independientes. Fijar por digest las imágenes MinIO, mc, Node/Python y EPPA;
se prohíbe latest.
2. Bootstrap del bucket aislado¶
Un operador con acceso temporal a MinIO ejecuta, desde un host bloqueado, el script versionado. Las cinco rutas de secretos apuntan fuera del repo y no se pegan en la terminal:
export MINIO_ENDPOINT='https://<endpoint-interno>'
export MINIO_ADMIN_ACCESS_KEY_FILE='/ruta-segura/bootstrap-access-key'
export MINIO_ADMIN_SECRET_KEY_FILE='/ruta-segura/bootstrap-secret-key'
export EPPA_PRIVATE_CAPTURE_BUCKET='eppa-private-capture-pilot'
export EPPA_PRIVATE_CAPTURE_S3_ACCESS_KEY_FILE='/ruta-segura/runtime-access-key'
export EPPA_PRIVATE_CAPTURE_S3_SECRET_KEY_FILE='/ruta-segura/runtime-secret-key'
bash deploy/private-capture/bootstrap-minio.sh \
| tee "$EVIDENCE_DIR/minio-bootstrap-controls.txt"
El script se niega a adoptar buckets, usuarios o políticas preexistentes. Crea
un bucket sin object lock/versioning, niega acceso anónimo, aplica sólo CRUD al
prefijo pc/ y carga una expiración de un día. Las credenciales admin se
retiran del host inmediatamente después. El lifecycle no sustituye al purge
exacto: el scanner de MinIO puede ejecutarse con demora.
Antes de seguir, demostrar con la identidad runtime:
Put/Get/Delete/Listfunciona sólo bajopc/con un objeto sintético.- otro bucket y otro prefijo devuelven
AccessDenied; - acceso anónimo y URL sin autenticación fallan;
- no existen replication, object lock ni versioning;
- el lifecycle exportado coincide con
deploy/private-capture/minio-lifecycle.json.
3. Migración expand-only¶
Construir una imagen inmutable del commit aprobado. Sin publicar tráfico:
docker run --rm "${IMAGE_DIGEST:?required}" \
python3 -c 'import boto3, cryptography, sqlalchemy; print("runtime-deps-ok")'
docker run --rm --env-file /ruta-segura/migration.env \
"${IMAGE_DIGEST:?required}" \
sh -c 'cd /app && python3 -m alembic upgrade head'
La migración debe ser sólo expansiva y compatible con el color activo. Nunca hacer downgrade destructivo durante rollback. Verificar las tablas de grants, sesiones, vistas y outbox; ninguna fila puede contener bytes, base64, nombres de paciente ni claves S3 descriptivas.
4. Levantar el color candidato¶
Usar un proyecto Compose y puertos exclusivos para el piloto. Ejemplo de
asignación (confirmar libres con ss -lnt):
| Color | Next | API | Tráfico público |
|---|---|---|---|
| blue | 9010 | 8110 | activo o standby |
| green | 9011 | 8111 | candidato o standby |
Levantar el color inactivo por digest, con secretos file-backed y
EPPA_PRIVATE_CAPTURE_ENABLED=false. No montar el socket Docker, el checkout,
fuentes de pacientes ni volúmenes de backup. El API/worker puede acceder al
bucket; Next y Nginx no.
El worker usa el mismo digest, una sola réplica o un lease distribuido, y
procesa el outbox de forma idempotente. Debe reintentar con backoff y dejar
purge_failed más un código sanitizado; nunca loguea payload, objeto, AAD, KEK
ni stack traces con metadatos sensibles.
5. Health y smoke antes del switch¶
El candidato debe exponer:
- liveness sin dependencia externa;
- readiness que compruebe DB, bucket, política/lifecycle esperados y worker reciente, sin leer ni listar datos en la respuesta;
- métricas agregadas sin org, sesión, subject, key ni imagen.
Ejecutar todos los gates de
docs/testing/PRIVATE-CAPTURE-PILOT-GATES.md. El único E2E permitido usa una
imagen generada sintéticamente y recorre capability, consentimiento sintético,
sesión, cuatro vistas, candidate/approval y purge inmediato. Confirmar por S3
que objetos, versiones y multipart huérfanos quedan en cero.
Sólo después del smoke, habilitar el flag en el candidato, repetir los gates y mantener Nginx apuntando al color anterior.
6. Switch atómico¶
El host piloto debe tener basic auth obligatorio, TLS, client_max_body_size
igual al límite de la aplicación, rate limit, timeouts acotados,
Cache-Control: no-store y logs sin cuerpo/query string. No se comparte cookie
con eppa.firemandeveloper.com ni eppa2.firemandeveloper.com.
Preparar un archivo upstream por color, cambiar sólo el symlink del host piloto, validar y recargar:
sudo ln -sfn "/etc/nginx/eppa-private-capture-upstream-${CANDIDATE_COLOR}.conf" \
/etc/nginx/conf.d/eppa-private-capture-upstream.conf
sudo nginx -t
sudo systemctl reload nginx
Repetir smoke sintético por el hostname y ambos health legados. Mantener el color anterior listo, sin nuevas sesiones, por toda la ventana de observación.
7. Rollback¶
Rollback se dispara ante cualquier 5xx, readiness degradada, error de purge, drift de bucket/policy/lifecycle, aparición en backup o filtración en logs:
sudo ln -sfn "/etc/nginx/eppa-private-capture-upstream-${PREVIOUS_COLOR}.conf" \
/etc/nginx/conf.d/eppa-private-capture-upstream.conf
sudo nginx -t
sudo systemctl reload nginx
Luego:
- poner
EPPA_PRIVATE_CAPTURE_ENABLED=falseen ambos colores; - conservar el candidato aislado para evidencia, sin tráfico;
- completar purge de toda sesión creada por el smoke y verificar
404/cero inventario; - no revertir la migración expand-only;
- verificar de nuevo ambos host legados;
- abrir incidente si existió una imagen real o una copia no autorizada.
8. TTL, purge y monitoreo¶
El reloj autoritativo es expires_at en DB, máximo 24 horas. Un job cada 15
minutos reclama sesiones vencidas mediante lease/outbox, elimina todas las
partes/objetos, verifica ausencia con HEAD/inventario y recién entonces marca
deleted con purged_at. Una revocación de consentimiento o purge manual se
prioriza sin esperar TTL.
Alertas obligatorias:
- objeto más viejo que
TTL + 30 min; - sesión
deleting,expiredopurge_failedpor más de 15 minutos; - multipart incompleto por más de una hora;
- objeto sin fila o fila activa sin objeto;
- versiones/delete markers distintos de cero;
- acceso anónimo, versioning, replication, policy o lifecycle con drift;
- heartbeat del worker mayor a 5 minutos;
- nombre/prefijo privado presente en un manifiesto o destino de backup.
El backup de Postgres puede conservar metadatos mínimos conforme su política,
pero ningún objeto privado, KEK ni credencial S3 se respalda. El guard de
scripts/backup_restore_drill.py rechaza el bucket/prefijo privado. Una
restauración de DB nunca debe resucitar una imagen ya purgada.
9. Consentimiento y autorización final¶
Antes de cualquier fotografía real deben estar completos y firmados:
docs/security/private-capture-consent-addendum.md;- responsable legal/privacidad, custodio de datos, responsable clínico e infraestructura;
- ubicación real del almacenamiento, TTL, canal de revocación y tratamiento de la imagen derivada/anónima;
- evidencia de gates y simulacro de incidente/purge.
Sin esas aprobaciones, el único modo autorizado es local/sintético, con feature flag apagado por defecto. Este runbook no constituye aprobación legal ni clínica.
Referencias operativas¶
- Política y lifecycle:
deploy/private-capture/. - Secretos:
deploy/private-capture/SECRETS.md. - Consentimiento:
docs/security/private-capture-consent-addendum.md. - Gates:
docs/testing/PRIVATE-CAPTURE-PILOT-GATES.md. - MinIO
mcoficial: https://min.io/docs/minio/linux/reference/minio-mc.html. - Lifecycle oficial: https://min.io/docs/minio/linux/reference/minio-mc/mc-ilm-rule-import.html.