Incidente 2026-05-13 — Crons STAGE devuelven HTTP 302 tras hardening CF Access (s57/s58)
Resumen ejecutivo
El 13 de mayo de 2026 el backup automático D1 y el stale-check de Biblioteca dejaron de funcionar en el entorno STAGE. El widget Salud mostró “atrasado 42h · cron OK” — los crons no fallaban en sí mismos, pero las llamadas al endpoint /api/maintenance/* recibían HTTP 302 (redirección al login de Cloudflare Access) en lugar de 200.
Duración del gap: ~42 horas (desde 2026-05-13 00:00 UTC hasta corrección manual el mismo día).
Cronología
| Hora (UTC) | Evento |
|---|---|
| s57 (fecha anterior) | Migración a CF Access Service Token: el endpoint /api/maintenance/* queda protegido por Cloudflare Access. Los callers deben enviar CF-Access-Client-Id + CF-Access-Client-Secret. |
| d20 s58 (fecha anterior) | Cierre del bypass everyone en CF Access. Se audita y corrige el Help Widget, pero los crons de STAGE (cron-dr-backup.sh, cron-stale-check.sh) quedan fuera de la auditoría. |
| 2026-05-13 00:00 UTC | Primera ejecución fallida del cron de backup: FAILED: HTTP 302. El cron marca OK en el sistema (sale sin error de red), pero el fichero de backup contiene HTML del login en lugar de JSON. |
| 2026-05-13 ~18:00 UTC | Diagnóstico: widget Salud detecta “atrasado 42h”. Revisión de /opt/dr-backups/cron.log confirma FAILED: HTTP 302 desde medianoche. |
| 2026-05-13 ~18:01 UTC | Fix aplicado (commit 83a31cd). Backup manual ejecutado: 12 280 rows, 5.6 MB → /opt/dr-backups/backup-2026-05-13.json. Stale-check manual: 96% healthy, 139 stale nodes. |
Causa raíz
Los scripts cron-dr-backup.sh y cron-stale-check.sh solo enviaban Authorization: Bearer <MCP_TOKEN> en el curl. Tras el hardening s57/s58, Cloudflare Access exige adicionalmente:
CF-Access-Client-Id: <SERVICE_TOKEN_ID>
CF-Access-Client-Secret: <SERVICE_TOKEN_SECRET>
Sin esos headers, CF Access intercepta la request antes de que llegue al origen y devuelve un 302 al portal de login. El script interpretaba el 200 del portal de login como éxito parcial (el curl no fallaba, pero $HTTP_CODE era 302 en los logs).
Este gap es idéntico al que se corrigió para el Help Widget en d20 s58, pero los crons de STAGE no entraron en esa auditoría inicial.
Fix aplicado
Fichero de credenciales
Creado /opt/dr-backups/.cf-access (chmod 600, solo lectura root/operador):
export CF_ACCESS_CLIENT_ID="<service-token-id>"
export CF_ACCESS_CLIENT_SECRET="<service-token-secret>"
Cambios en scripts (Regla 22)
scripts/cron-dr-backup.sh y scripts/cron-stale-check.sh:
- Variable
CF_ACCESS_FILE="/opt/dr-backups/.cf-access"al inicio. - Guard que aborta limpio si el fichero no existe (evita ejecuciones silenciosamente rotas).
source "$CF_ACCESS_FILE"antes del curl.- Dos headers extra en la llamada curl:
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \ -H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
cron-gdrive-sync.sh no se ve afectado — usa rclone, no llama a /api/maintenance/*.
Verificación post-fix
Backup manual (2026-05-13):
HTTP 200 ✓
Rows exportadas: 12 280
Tamaño: 5.6 MB
Destino: /opt/dr-backups/backup-2026-05-13.json
Stale-check manual:
HTTP 200 ✓
Healthy: 96%
Stale nodes: 139
Próximas ejecuciones automáticas:
cron-dr-backup.sh → 2026-05-14 00:00 UTC
cron-stale-check.sh → 2026-05-14 08:00 UTC
Impacto
| Dimensión | Detalle |
|---|---|
| Backup D1 | 1 backup perdido (2026-05-12 → 2026-05-13). Ventana máxima de pérdida: 42 h de datos si hubiera catástrofe en ese periodo. |
| Stale-check | 1 ejecución de stale-check perdida. Sin impacto funcional inmediato (Biblioteca siguió operativa). |
| Datos | No hay pérdida de datos en producción. El gap es solo de cobertura de backup. |
| Usuarios | Ningún usuario afectado. Incidente de infraestructura interna. |
Lecciones aprendidas
- Auditoría incompleta en s58: cuando se cierra un bypass de autenticación, todos los callers del endpoint deben auditarse en la misma sesión — no solo los visibles en el frontend.
- Detección tardía: el cron salía con código 0 aunque el backup fallaba (el curl recibía 302 pero no lo trataba como error). Añadir validación del HTTP code en los scripts fue parte del fix.
- Patrón repetible: cualquier nuevo caller de
/api/maintenance/*(o cualquier endpoint bajo CF Access) debe incluir los headersCF-Access-Client-Id+CF-Access-Client-Secretleídos del fichero.cf-access.
Seguimiento
- Confirmar ejecución automática exitosa el 2026-05-14 00:00 UTC (backup).
- Confirmar ejecución automática exitosa el 2026-05-14 08:00 UTC (stale-check).
- Evaluar añadir alerta proactiva cuando el último backup tiene más de 26 h de antigüedad.
- Revisar si
cron-gdrive-sync.shpodría verse afectado en el futuro.
Véase también
- [[incident—20260512—help-widget-cf-access-s58]]
- [[workspace-tech—tecnico—cloudflare-security]]