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/*
- Ejecutar comandos en los equipos de red via
-
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.
- CORS
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:
| Fase | Cuándo | Quién | Qué |
|---|---|---|---|
| Fase 1 | ✓ PROD | SaaS | Genera/custodia el token local de cada Agent (cifrado en BD) |
| Fase 2 | ✓ PROD | Frontend (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 .exe | Genera el token, lo persiste localmente, lo deposita en el SaaS, lo exige en toda la API HTTP y el WS |
Componentes (Fase 3)
-
[[entity—agent—service—local-token]] — Generación, persistencia (DPAPI) y validación del token:
get_or_create_local_token(): generatoken_urlsafe(32), lo almacena cifrado en%APPDATA%/CreaRackAgent/local_token.enc, lo cachea en memoria.verify_local_token(candidate): compara en tiempo constante conhmac.compare_digest().auth_required(auth_manager): retornaTruesolo si el Agent está vinculado a un SaaS.extract_bearer(headers): parseaAuthorization: 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).
-
[[entity—agent—service—connector-register-local-token]] — Integración del token con el SaaS:
connector._register_local_token(): POST/api/agent/register-local-tokencon el token.- Best-effort: reintenta en cada reconexión WS; si falla, el frontend sigue sin token (middleware local rechaza la solicitud).
-
[[entity—agent—service—terminal-ws-auth]] — Validación en tiempo de conexión WS:
- Primer mensaje obligatorio:
{"action": "auth", "token": <token>}. terminal.htmllo recibe del SaaS víapostMessage SET_AUTH_TOKEN(Fase 2) y lo manda en open/reconexión.- Sin token válido → WebSocket cierra con código 4401.
- Primer mensaje obligatorio:
-
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_originspasa 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 quecredentials.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-tokencon headerAuthorization: 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.exeviejos. - 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 Agent | SaaS Fase 1 | Frontend Fase 2 | Comportamiento |
|---|---|---|---|
| 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 cuandosaas_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]]