Pular para conteúdo

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 --delete y compose 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=true ni basic auth de borde;
  • el contenedor runtime debe demostrar que puede importar boto3 antes 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=prod y COOKIE_SECURE=true;
  • EPPA_PRIVATE_CAPTURE_ENABLED=false durante 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_ENDPOINT y EPPA_PRIVATE_CAPTURE_S3_REGION;
  • EPPA_PRIVATE_CAPTURE_S3_ACCESS_KEY_FILE y EPPA_PRIVATE_CAPTURE_S3_SECRET_KEY_FILE;
  • EPPA_PRIVATE_CAPTURE_KEK_FILE de 32 bytes y EPPA_PRIVATE_CAPTURE_KEY_VERSION;
  • EPPA_PRIVATE_CAPTURE_ALLOWED_ORIGIN, un único origen HTTPS del piloto;
  • EPPA_PRIVATE_CAPTURE_MAX_BYTES y EPPA_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:

  1. Put/Get/Delete/List funciona sólo bajo pc/ con un objeto sintético.
  2. otro bucket y otro prefijo devuelven AccessDenied;
  3. acceso anónimo y URL sin autenticación fallan;
  4. no existen replication, object lock ni versioning;
  5. 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:

  1. poner EPPA_PRIVATE_CAPTURE_ENABLED=false en ambos colores;
  2. conservar el candidato aislado para evidencia, sin tráfico;
  3. completar purge de toda sesión creada por el smoke y verificar 404/cero inventario;
  4. no revertir la migración expand-only;
  5. verificar de nuevo ambos host legados;
  6. 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, expired o purge_failed por 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