CreaRack-SL

Fase 3 · Autenticación local del Agent (Raíz 3 Auditoría Suprema)

Resumen

Agent 2.0.24 → 2.1.0 · Cierra los 12 hallazgos de severidad ALTA de la Raíz 3 de la Auditoría Suprema del dominio terminal. El Agent local genera un token secreto (opaco, 32B), lo persiste cifrado en el PC del cliente, lo deposita en el SaaS al conectar, y a partir de ahí solo acepta órdenes de quién presente ese token. Combinado con las Fases 1-2 (ya en PROD), cierra definitivamente la exposición a DNS-rebinding/CSRF.

El problema (Raíz 3)

Hasta 2.1.0, la API HTTP local del Agent (http://127.0.0.1:PORT/) atendía a cualquiera que llamara desde el PC del cliente:

  • Una web maliciosa abierta en el navegador podía, con ciertos trucos (DNS rebinding, CSRF):

    • Ejecutar comandos en los equipos de red via /cluster/execute
    • Leer/escribir ficheros via /sftp/download, /sftp/write
    • Desinstalar el Agent via /admin/uninstall
    • Acceder a signage via /signage/*
  • Las protecciones previas eran débiles:

    • CORS ["*"] + credentials → cualquier origin podía pedir; no bloqueaba conexiones locales.
    • Sin verificación de origen real en WS de consola.

La auditoría identificó esto como el bloque de seguridad más grave del Agent (12 hallazgos ALTA).

La solución (Fase 3)

La autenticación local del Agent sigue un modelo de three-phase rollout:

FaseCuándoQuiénQué
Fase 1✓ PRODSaaSGenera/custodia el token local de cada Agent (cifrado en BD)
Fase 2✓ PRODFrontend (navegador)Recibe el token del SaaS, lo inyecta en llamadas a la API local via Bearer + WS via postMessage
Fase 3✓ Esta PR (2.1.0, build pendiente)Agent .exeGenera el token, lo persiste localmente, lo deposita en el SaaS, lo exige en toda la API HTTP y el WS

Componentes (Fase 3)

  1. [[entity—agent—service—local-token]] — Generación, persistencia (DPAPI) y validación del token:

    • get_or_create_local_token(): genera token_urlsafe(32), lo almacena cifrado en %APPDATA%/CreaRackAgent/local_token.enc, lo cachea en memoria.
    • verify_local_token(candidate): compara en tiempo constante con hmac.compare_digest().
    • auth_required(auth_manager): retorna True solo si el Agent está vinculado a un SaaS.
    • extract_bearer(headers): parsea Authorization: Bearer <token>.
    • allowed_origins_for(auth_manager, port): retorna origins CORS permitidos (SaaS + UI local, o ["*"] pre-activación).
    • EXEMPT_PATHS: /info, /health, /check, /favicon.ico, /, /terminal/ui, /saas/configure|status|connect (bootstrap del frontend + activación inicial).
  2. [[entity—agent—service—connector-register-local-token]] — Integración del token con el SaaS:

    • connector._register_local_token(): POST /api/agent/register-local-token con el token.
    • Best-effort: reintenta en cada reconexión WS; si falla, el frontend sigue sin token (middleware local rechaza la solicitud).
  3. [[entity—agent—service—terminal-ws-auth]] — Validación en tiempo de conexión WS:

    • Primer mensaje obligatorio: {"action": "auth", "token": <token>}.
    • terminal.html lo recibe del SaaS vía postMessage SET_AUTH_TOKEN (Fase 2) y lo manda en open/reconexión.
    • Sin token válido → WebSocket cierra con código 4401.
  4. Middleware HTTP en main.py:

    • Instala security headers (CSP + X-Frame).
    • Gate Bearer: antes de cualquier endpoint (salvo EXEMPT_PATHS), valida el token.
    • CORS cerrado: allow_origins pasa de ["*"] al origin del SaaS + UI local.
    • Orden de middlewares (IMPORTANTE): gate + headers → CORS (capa más externa).

Seguridad detallada

Token local

  • Generación: secrets.token_urlsafe(32) (~43 caracteres base64url).
  • Persistencia: Cifrado DPAPI en %APPDATA%/CreaRackAgent/local_token.enc (mismo mecanismo que credentials.enc).
  • Almacén corrupto: Si el fichero está dañado, se regenera silenciosamente (no tumba el Agent).
  • Verificación: hmac.compare_digest() → time-constant, inmune a ataques por timing.

Depósito en el SaaS

  • Endpoint: POST /api/agent/register-local-token con header Authorization: Bearer <access_token> (credencial del Agent en el SaaS).
  • Payload: {"local_token": <token>}.
  • Reintentos: En cada reconexión WS; falla silenciosa (no tumba la conexión principal).
  • Custodia del SaaS: El token se almacena cifrado asociado al Agent; el frontend solo lo recibe cuando solicita la sesión del tenant dueño.

Gate Bearer en la API local

Si (method == OPTIONS) OR (path in EXEMPT_PATHS) OR (not auth_required()) OR (token válido):
  → llamada normal
Else:
  → 401 "Local auth token required"
  • Pre-activación (auth_required() = False): sin exigencia; compat con los .exe viejos.
  • Post-activación: todas las llamadas (salvo bootstrap) exigen Bearer.

CORS

  • Antes (2.0.24): allow_origins=["*"], allow_credentials=True → cualquier origin.
  • Ahora (2.1.0):
    • allow_origins = [f"http://127.0.0.1:{port}", f"http://localhost:{port}", <saas_origin>]
    • allow_credentials = False (no necesario; el token va en Bearer, no en cookies).
    • Capa externa: CORS middleware se añade al final (aplica preflight + headers también a los 401).

WebSocket de consola

// client (terminal.html)
window.addEventListener('message', (event) => {
  if (event.data.type === 'SET_AUTH_TOKEN') {
    localAuthToken = event.data.token;
    sendWsAuth(ws);  // Envía {"action":"auth","token":...} en open
  }
});

// server (routes/terminal.py)
if auth_required():
    first = await ws.receive_text()  // Espera primer mensaje
    if first.action != "auth" or not verify_local_token(first.token):
        await ws.close(4401)
        return

Compatibilidad

Versión AgentSaaS Fase 1Frontend Fase 2Comportamiento
2.0.24 (old)✓✓Token opcional; no verificado (compat)
2.1.0 (nuevo)✓✓Token exigido cuando vinculado; WS exige auth
2.1.0✓✗ (viejo)Token depositado; frontend no lo inyecta → 401 en primeras llamadas

En la release final: compilar 2.1.0, hacer smoke tests (terminal, SSH, discovery, signage), publicar agent-v2.1.0, borrar release anterior.

Testing

6 tests en tests/agent/test_agent_local_auth.py:

  • test_token_generated_and_persisted: generación, persistencia, lectura posterior.
  • test_verify_token: validación token correcto/incorrecto/nulo.
  • test_auth_required_only_when_linked: solo exigido cuando saas_url + agent_id.
  • test_extract_bearer: parseo del header Authorization.
  • test_exempt_paths_cover_bootstrap: rutas exentas incluyen bootstrap.
  • test_corrupt_store_regenerates: almacén dañado → regeneración.

Estado: ✓ Verdes en Docker; py_compile de los 5 módulos OK.

Véase también

  • [[entity—agent—service—local-token]]
  • [[entity—agent—service—connector-register-local-token]]
  • [[entity—agent—service—terminal-ws-auth]]
  • [[concept—security—cross-origin-attacks]]
  • [[decision—terminal-auth-local-cross-origin]]