Incidente s58: CF Access 403 — Help Widget bloqueado tras endurecer bypass-everyone (s57)
Resumen
| Campo | Valor |
|---|---|
| Fecha detección | 2026-05-12 |
| Sesión | s58 |
| Severidad | Alta (PROD — Help Widget 100% inoperativo) |
| Estado | ✅ Resuelto — fix mergeado en commit 881f96f |
| Afectados | Todos los usuarios que abrieran el Help Widget en PROD |
| Detectado por | Edu (antes de que cargara ningún usuario externo) |
Síntomas observados
- “Could not connect to the assistant” al hacer cualquier pregunta en el Help Widget (proxy
POST /api/mcpdevolvía 403/302). - “El panel no muestra documentación” al abrir el Help Widget (llamadas
GET /api/biblioteca/wiki-titlesyGET /api/biblioteca/wikidevolvían 403/302). scripts/harness/bib_report_check.pydegradado a modo warning conURLError(no bloqueaba commits, pero operaba sin capacidad de validar contra el endpoint).
Causa raíz
En sesión s57 (2026-05-12, tarde-noche) se eliminó la policy bypass-everyone de dos apps Cloudflare Access:
- “Workspace MCP API” — protege
workspace.crearack.com/api/mcp/* - “API Biblioteca” — protege
workspace.crearack.com/api/biblioteca/*
El Service Token CF Access se distribuyó correctamente a:
- 8 GitHub workflows
- 6 scripts batch
- Crons (5 sitios documentados en s57)
Pero los callers Django runtime de PROD no estaban en esa lista. Concretamente, las 3 llamadas que hace core/api_help.py (a través de _workspace_headers()) a workspace.crearack.com:
| Caller | Endpoint | Síntoma |
|---|---|---|
| Proxy bib_ask | POST /api/mcp | “Could not connect to the assistant” |
| Traducciones EN | GET /api/biblioteca/wiki-titles | Panel sin títulos |
| Lista artículos | GET /api/biblioteca/wiki | Panel sin documentación |
A partir del endurecimiento, todas devolvían 403 (o redirect 302 a login de CF Access) porque los headers CF-Access-Client-Id y CF-Access-Client-Secret no se enviaban.
Gap sistémico
s57 realizó un audit de callers exhaustivo para workflows/scripts batch, pero no incluyó los callers Django runtime en el inventario. El Help Widget funciona en tiempo real desde el proceso Django de PROD — un tipo de caller distinto que no había sido considerado en el checklist de hardening.
Fix aplicado (commit 881f96f)
1. core/api_help.py — _workspace_headers()
def _workspace_headers():
token = getattr(settings, "WORKSPACE_MCP_TOKEN", "")
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
cf_id = getattr(settings, "CF_ACCESS_CLIENT_ID", "")
cf_secret = getattr(settings, "CF_ACCESS_CLIENT_SECRET", "")
if cf_id and cf_secret:
headers["CF-Access-Client-Id"] = cf_id
headers["CF-Access-Client-Secret"] = cf_secret
return headers
Un único cambio en el helper compartido fija los 3 callers simultáneamente (todos pasan por _workspace_headers()).
2. scripts/harness/bib_report_check.py — _cf_access_headers()
Nuevo helper que inyecta los headers CF Access en las 2 funciones que llaman a /api/biblioteca/recent-reports y /api/biblioteca/skip-log. El hook ya no degrada silenciosamente si las env vars están configuradas.
def _cf_access_headers() -> dict[str, str]:
cf_id = os.environ.get("CF_ACCESS_CLIENT_ID", "").strip()
cf_secret = os.environ.get("CF_ACCESS_CLIENT_SECRET", "").strip()
if cf_id and cf_secret:
return {"CF-Access-Client-Id": cf_id, "CF-Access-Client-Secret": cf_secret}
return {}
3. config/settings/base.py
CF_ACCESS_CLIENT_ID = os.environ.get("CF_ACCESS_CLIENT_ID", "")
CF_ACCESS_CLIENT_SECRET = os.environ.get("CF_ACCESS_CLIENT_SECRET", "")
Vacío en dev local (compatible — no se llama a workspace desde local).
4. compose.yml + compose.prod.yml
Propagan las 2 env vars al contenedor web, siguiendo el patrón de WORKSPACE_MCP_TOKEN. En PROD los valores los inyecta Dokploy desde panel.
Pasos manuales post-merge (pendiente verificar)
- ☐ Configurar
CF_ACCESS_CLIENT_IDyCF_ACCESS_CLIENT_SECRETen Dokploy panel del stack PROD. - ☐ Redeploy del stack PROD desde Dokploy.
- ☐ Verificar Help Widget vivo: hacer una pregunta + abrir lista de artículos.
- ☐ Configurar las 2 env vars en la PowerShell local de cada miembro del staff (para que
bib_report_check.pyopere al 100%, no en degraded mode — Regla 24).
Lección aprendida (Lección s58)
Cuando se endurezca CF Access (quitar
bypass-everyoneo equivalente), el audit de callers debe incluir explícitamente los callers Django runtime, no solo workflows/scripts batch.
El checklist de hardening de CF Access debe tener una sección dedicada a:
grep -r "workspace.crearack.com"en todo el repo.- Revisión explícita de
core/api_help.pyy cualquier módulo Django que llame a endpoints workspace en tiempo de request.
Archivos modificados
| Archivo | Cambio |
|---|---|
core/api_help.py | _workspace_headers() inyecta headers CF Access si configurados |
scripts/harness/bib_report_check.py | _cf_access_headers() helper + integrado en 2 funciones |
config/settings/base.py | Declara CF_ACCESS_CLIENT_ID + CF_ACCESS_CLIENT_SECRET |
compose.yml | Propaga 2 env vars al contenedor web |
compose.prod.yml | Propaga 2 env vars al contenedor web |
CHANGELOG.md | Entrada v1.0.73 (Sesión 58) |
RELEASE_NOTES.md | Entrada hotfix v1.0.73 |
Véase también
- [[entity—core—service—api-help]]
- [[feature—core—help-widget]]
- [[runbook—infra—cf-access-service-token]]
- [[entity—harness—script—bib-report-check]]