Volver a la wiki

Servicio: Autenticación local del Agent (Bearer token)

Descripción general

Servicio de validación de tokens Bearer para la API HTTP local del Agent (puerto 5050) y WebSocket de la consola. Implementa el mecanismo de llave secreta compartida entre el SaaS y el Agent, habilitando multi-tenancy segura en el dominio terminal. Desde el Agente 2.29.0 (v1.159.0) esta protección se complementa con guardas sobre a QUÉ SaaS se vincula el Agente y a QUÉ destinos se conecta en nombre del navegador (ver sección final).

Módulos principales:

Funciones públicas

deposit_local_token(agent_id: str, saas_url: str) -> str

Propósito: El Agent (.exe) deposita su token en el SaaS durante el registro/vinculación.

Firma:

def deposit_local_token(agent_id: str, saas_url: str) -> str:
    """
    POST /api/agent/local-token/deposit (endpoint SaaS)
    
    Args:
        agent_id: Identificador único del Agent (ej: "a1-prod-05")
        saas_url: Raíz del SaaS destino (ej: "https://crearack.com")
    
    Returns:
        Token aleatorio de 64 caracteres (hexadecimal).
    
    Lógica:
        1. Agent genera token local (secrets.token_hex(32)).
        2. Agent lo custodia en ~/.crearack/local_token (mode 0600, solo lectura del Agent).
        3. Agent POST al SaaS → SaaS lo almacena asociado al (agent_id, tenant).
        4. SaaS responde 201 + token confirmado.
    """

Flujo:

  1. Agent arranca, se vincula a un SaaS (connector._register_local_token).
  2. Agent genera token local (criptográficamente seguro).
  3. Agent lo almacena localmente y lo envía al SaaS.
  4. SaaS lo custodia en una tabla (agent_local_tokens en PostgreSQL).

Fuente de datos: Inicializado en connector.py durante _register_local_token().

verify_local_token(bearer_token: str) -> bool

Propósito: Validar que el Bearer recibido en una llamada HTTP coincide con el token depositado.

Firma:

def verify_local_token(bearer_token: str) -> bool:
    """
    Compara el Bearer contra el token custodiado localmente.
    
    Args:
        bearer_token: Token extraído del header Authorization: Bearer <token>
    
    Returns:
        True si coincide (timing-safe comparison), False otherwise.
    
    Lógica:
        1. Lee token local desde ~/.crearack/local_token.
        2. Compara con hmac.compare_digest() (resistente a timing attacks).
        3. Si no existe archivo local → devuelve False.
    """

Uso: Decorador @auth_required() en endpoints de la API local.

auth_required(auth: AgentAuth | None) -> bool

Propósito: Determinar si una llamada HTTP específica debe exigir token Bearer.

Firma:

def auth_required(auth: AgentAuth | None) -> bool:
    """
    Resuelve si el endpoint actual exige autenticación.
    
    Args:
        auth: Contexto de autenticación del Agent (saas_url, agent_id, etc.)
              Si es None → pre-activación (no vinculado aún) → no exige.
    
    Returns:
        True si exige Bearer, False otherwise.
    
    Lógica:
        1. Si AGENT_LOCAL_AUTH_ENFORCE=0 → siempre False (escape hatch).
        2. Si auth es None o no tiene saas_url/agent_id → False (pre-activación).
        3. Si auth vinculado (saas_url + agent_id) → True (enforcement activo).
        4. Respeta EXEMPT_PATHS (bootstrap, /info, /health) → siempre False.
    """

Variables de control:

extract_bearer(headers: dict) -> str | None

Propósito: Extraer token Bearer del header Authorization.

Firma:

def extract_bearer(headers: dict) -> str | None:
    """
    Busca header Authorization: Bearer <token>
    
    Args:
        headers: Headers HTTP (case-insensitive).
    
    Returns:
        Token sin el prefijo "Bearer ", o None si no existe.
    
    Manejo de casos**:
        - "Authorization: Bearer abc123" → "abc123"
        - "authorization: Bearer xyz " → "xyz" (sin espacios)
        - "Bearer abc" (sin colon) → None
        - "Basic ..." → None
    """

Interceptor del frontend (agent_auth.js)

getAgentLocalToken()

Propósito: Obtener el token Bearer del SaaS e inyectarlo en futuras llamadas HTTP al Agent.

Flujo:

async function getAgentLocalToken() {
  const infoResp = await fetch("http://127.0.0.1:5050/info");
  const info = await infoResp.json();
  const agentId = info.saas.agent_id;
  const tokenResp = await fetch("https://crearack.com/api/agent/local-token", { method: "GET", credentials: "include" });
  const { token } = await tokenResp.json();
  _injectedToken = token;
  return token;
}

function interceptFetch(origFetch) {
  return function(url, opts) {
    if (url.startsWith("http://127.0.0.1:5050")) {
      opts.headers = opts.headers || {};
      opts.headers["Authorization"] = `Bearer ${_injectedToken}`;
    }
    return origFetch(url, opts);
  };
}

Bug corregido en 2.1.2: info.saas_info.agent_id (clave inexistente) → info.saas.agent_id.

v1.159.0 (mega-auditoría T22, B-18/B-20): openInNewWindow (usada por los botones Debug/Tools/Dashboard del Agente en el Observatory) añade el token local en el fragmento de la URL (#token=...), solo para destinos 127.0.0.1:5050 — así la consola en vivo y la tabla de traps siguen recibiendo el Bearer aunque se abran en una ventana nueva. Además abre la ventana en el mismo clic (evita el bloqueo de pop-ups) y navega a la página del Agente en cuanto fetchToken resuelve, con un timeout de 5 s si no responde. agent_auth.js?v=3, base.js?v=15.

Ciclo de vida del token

Agent arranca + se vincula a SaaS (ej: crearack.com/setup)
  → deposit_local_token(): genera token, lo guarda en ~/.crearack/, lo envía al SaaS
  → SaaS lo custodia en DB (agent_local_tokens: agent_id, token, tenant, created_at)
  → Navegador (tenant legítimo) abre dashboard o consola SSH
  → Frontend solicita token: GET /api/agent/local-token (sesión + fleet:view)
  → SaaS valida sesión + permisos, entrega el token (200 + {"token": "abc..."})
  → agent_auth.js inyecta Bearer en llamadas HTTP locales
  → Agent valida Bearer con verify_local_token() → procesa o rechaza 401

Configuración

Env varDefectoDescripción
AGENT_LOCAL_AUTH_ENFORCE"1" (ON)Activa/desactiva exigencia de Bearer (desde 2.1.2). 0 = escape hatch.
AGENT_LOCAL_TOKEN_PATH~/.crearack/local_tokenRuta donde el Agent custodia el token localmente.
AGENT_LOCAL_TOKEN_TTL86400 (24h)TTL del token en el SaaS (renovación automática en login).
AGENT_EXTRA_SAAS_ORIGINS (2.29.0)vacíoDominios propios adicionales admitidos en la primera vinculación, además de los oficiales (ver abajo).

CORS vivo (auditoría MEDIA #13, v1.116.0)

allowed_origins_for se evaluaba una sola vez, al instalar el middleware en el arranque de install_local_security. Un Agente recién instalado arranca sin vincular, así que la lista de orígenes quedaba fijada a ["*"] — y seguía siéndolo DESPUÉS de emparejarse, hasta que alguien reiniciara el servicio.

LiveOriginsCORSMiddleware (subclase de CORSMiddleware de Starlette) resuelve esto re-derivando los orígenes en cada petición HTTP: si allowed_origins_for(get_auth_manager(), port) cambia respecto a la última vez, vuelve a ejecutar el __init__ de la clase base con la lista nueva. Desde que hay vinculación, allowed_origins_for nunca vuelve a devolver ["*"].

Agente 2.29.0 — orígenes SaaS y destinos salientes (mega-auditoría ronda 4, 25-09-2026, PR #607)

Hasta 2.29.0, un Agente sin vincular aceptaba cualquier SaaS como destino de primera vinculación, y una vez vinculado, obedecía instrucciones de descarga/proxy sin comprobar que vinieran realmente del SaaS emparejado. El binario tampoco distinguía “esto es un origen de desarrollo” de “esto es producción” — los orígenes de desarrollo viajaban dentro del .exe que llega al cliente.

B-17/B-18/B-19/B-20/B-54 — la API local deja de obedecer a webs y programas ajenos:

terminal/agent/core/net_guard.py (módulo nuevo del Agente, desde 05-09-2026, ampliado en esta ronda): expone resolve_allowed_target(host, saas_url) / is_allowed_target(host, saas_url) — la contraparte, en el lado del Agente, del guard SSRF que monitoring.services.net_guard aplica en el lado del SaaS: valida que un destino de red (host que el navegador o el SaaS piden alcanzar) no sea loopback, link-local o un rango privado inesperado antes de que el Agente abra la conexión saliente.

Tests que cambian por decisión de Edu (documentado en CHANGELOG): tres tests existentes de tests/agent/ ajustan sus expectativas a la nueva restricción de orígenes. Test nuevo: tests/agent/test_mega25_T22_api_local.py (504 líneas) — cobertura de las guardas de origen y destino descritas arriba.

Versión del Agente: 2.29.0 (AGENT_VERSION sin subir en este PR — pendiente de versionado explícito).

Seguridad

Amenazas mitigadas (Raíz 3 de la Auditoría Suprema + mega-auditoría ronda 4)

AmenazaMitigación
DNS-rebinding: Navegador abre http://127.0.0.1:5050 y envía comandosBearer válido solo en sesión del tenant.
CSRF: Formulario en web maliciosa + sesión del navegador activaBearer no es cookie; no se roba con CSRF.
Navegador comprometido: JavaScript malicioso en pestaña ajenaSin token válido del SaaS, rechazado 401.
Interceptión local: Atacante en LAN local observa tráficoToken es aleatorio; además HTTPS en el SaaS.
Vinculación a un SaaS ajeno (2.29.0)Primera vinculación restringida a dominios oficiales + AGENT_EXTRA_SAAS_ORIGINS.
Instrucción de descarga/proxy de un origen ajeno (2.29.0)is_saas_download_url exige esquema+host+puerto exactos del SaaS vinculado.
SSRF / DNS-rebinding en conexiones salientes del Agente (2.29.0)net_guard valida y pinnea la IP antes de conectar (ping, scan, discover, signage).

Protecciones implementadas

Tests

test_deposit_local_token(tmp_path, monkeypatch)

token = local_token.deposit_local_token(agent_id="test-agent", saas_url="https://test.crearack.com")
assert len(token) == 64
assert local_token.verify_local_token(token) is True

test_verify_token(tmp_path, monkeypatch)

token = local_token.deposit_local_token(...)
assert local_token.verify_local_token(token) is True
assert local_token.verify_local_token("wrong_token") is False
assert local_token.verify_local_token("") is False

test_auth_required_on_by_default()

assert local_token.LOCAL_AUTH_ENFORCE is True
assert local_token.auth_required(_FakeAuth(saas_url="...", agent_id="a1")) is True
assert local_token.auth_required(_FakeAuth()) is False

test_auth_required_escape_hatch(monkeypatch)

monkeypatch.setattr(local_token, "LOCAL_AUTH_ENFORCE", False)
assert local_token.auth_required(_FakeAuth(saas_url="...", agent_id="a1")) is False

v1.159.0 — tests/agent/test_mega25_T22_api_local.py

Cobertura de la restricción de orígenes de vinculación, is_saas_download_url, el rescate de /saas/* solo sin conexión viva, /ws/debug y /traps/recent con token, y las guardas de destino de net_guard en ping/scan/discover/signage.

Véase también

Subir