CreaRack-SL

Incidente s58: CF Access 403 — Help Widget bloqueado tras endurecer bypass-everyone (s57)

Resumen

CampoValor
Fecha detección2026-05-12
Sesións58
SeveridadAlta (PROD — Help Widget 100% inoperativo)
Estado✅ Resuelto — fix mergeado en commit 881f96f
AfectadosTodos los usuarios que abrieran el Help Widget en PROD
Detectado porEdu (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/mcp devolvía 403/302).
  • “El panel no muestra documentación” al abrir el Help Widget (llamadas GET /api/biblioteca/wiki-titles y GET /api/biblioteca/wiki devolvían 403/302).
  • scripts/harness/bib_report_check.py degradado a modo warning con URLError (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:

CallerEndpointSíntoma
Proxy bib_askPOST /api/mcp“Could not connect to the assistant”
Traducciones ENGET /api/biblioteca/wiki-titlesPanel sin títulos
Lista artículosGET /api/biblioteca/wikiPanel 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)

  1. ☐ Configurar CF_ACCESS_CLIENT_ID y CF_ACCESS_CLIENT_SECRET en Dokploy panel del stack PROD.
  2. ☐ Redeploy del stack PROD desde Dokploy.
  3. ☐ Verificar Help Widget vivo: hacer una pregunta + abrir lista de artículos.
  4. ☐ Configurar las 2 env vars en la PowerShell local de cada miembro del staff (para que bib_report_check.py opere al 100%, no en degraded mode — Regla 24).

Lección aprendida (Lección s58)

Cuando se endurezca CF Access (quitar bypass-everyone o 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.py y cualquier módulo Django que llame a endpoints workspace en tiempo de request.

Archivos modificados

ArchivoCambio
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.pyDeclara CF_ACCESS_CLIENT_ID + CF_ACCESS_CLIENT_SECRET
compose.ymlPropaga 2 env vars al contenedor web
compose.prod.ymlPropaga 2 env vars al contenedor web
CHANGELOG.mdEntrada v1.0.73 (Sesión 58)
RELEASE_NOTES.mdEntrada 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]]