Un token, todo el dashboard: el token de despliegue pasa a ser una credencial de pleno derecho


Mueves el dashboard web de Hermes a un servidor interno para que tu equipo pueda abrirlo desde el navegador, cambias la dirección de bind de 127.0.0.1 a 0.0.0.0 y se niega a arrancar. El log dice que necesita un provider de autenticación y que no va a hacer bind. Tú sí tienes un token de despliegue, inyectado desde tu gestor de secretos, pero nada lo trata como una credencial.

La media hora siguiente es conocida: acabas configurando basic auth, aparece el cuadro de login del navegador, las credenciales terminan en .env y la app de escritorio informa “HTTP reachable, but the WebSocket connection failed”. Todo a un paso de encajar, y nada encaja.

Por qué fallaba: una puerta que falla en cerrado

La puerta de autenticación del dashboard se endureció en junio de 2026: cualquier bind a una dirección que no sea loopback exige un provider de autenticación registrado y, sin él, se niega a hacer bind. Mejor ningún puerto abierto que uno abierto por descuido. El diseño es correcto.

El hueco estaba en qué providers contaban. Solo contaban los interactivos: basic auth, una cuenta de Nous, OIDC autoalojado. Y sin embargo la forma habitual en un despliegue es un token compartido único, descifrado por un gestor de secretos a un archivo y entregado al servicio. El código antiguo no solo lo ignoraba: además te empujaba a escribir en .env un juego de credenciales de basic auth que el despliegue nunca había pedido.

Incluso con el token provisionado, las dos patas lo rechazaban: una petición REST que llevaba el bearer recibía 401 no_cookie, y el upgrade a /api/ws devolvía un 403 pelado. El escritorio lo presentaba como “llegó al gateway por HTTP, pero el WebSocket en vivo falló”, lo cual confunde, porque la comprobación HTTP solo toca el /api/status público y parece perfectamente sana.

La solución: el token es un provider más

Hermes incluye un provider empaquetado, dashboard_auth/session_token. Se mantiene al margen: en cuanto existe HERMES_DASHBOARD_SESSION_TOKEN se registra solo, y sin token no hace absolutamente nada.

Dos formas de provisionarlo:

  • un valor literal: HERMES_DASHBOARD_SESSION_TOKEN=<token>;
  • una ruta de archivo: HERMES_DASHBOARD_SESSION_TOKEN=file:/run/secrets/hermes-dashboard-token.

La segunda forma es la que necesitan los gestores de secretos. Una ruta puede vivir en una línea Environment= de systemd, en un plist de launchd o en el entorno de un servicio renderizado por Nix; el secreto en sí no. Los módulos de Nix de Hermes lo exponen como backend.sessionTokenFile y lo renderizan en la forma file:. Al arrancar se lee el archivo (se recorta el espacio en blanco de los extremos y se tolera el BOM); si no se puede leer o está vacío, el proceso sale con un error explícito en vez de correr con una credencial vacía.

La verificación usa una comparación en tiempo constante, así que el tiempo de respuesta no sirve para adivinar el token. Las dos patas que usa un cliente en modo token —la cabecera REST Authorization: *** y el parámetro ?token= del upgrade a /api/ws— se resuelven por la misma costura, de modo que un solo token provisionado autoriza ambas. Es exactamente lo que el campo “Session token” del escritorio siempre prometió.

Una decisión deliberada: no hay página de login. /login muestra una nota que explica que este despliegue se autentica con token, en lugar de un botón que nadie podría completar, y la redirección automática de SSO nunca ofrece este provider. El modo token no es una persona iniciando sesión, es una máquina presentando una credencial.

Desplegarlo

# 1. genera un token aleatorio de 32 bytes
openssl rand -base64 32 > /etc/hermes/dashboard-token
chmod 600 /etc/hermes/dashboard-token
chown hermes:hermes /etc/hermes/dashboard-token

# 2. entrega al servicio una ruta, no el valor
export HERMES_DASHBOARD_SESSION_TOKEN=file:/etc/hermes/dashboard-token

# 3. haz bind a una dirección que no sea loopback (ahora la puerta puede satisfacerse)
hermes dashboard --host 0.0.0.0 --port 9119

En Nix no escribes la variable tú: los módulos toman backend.sessionTokenFile, apuntan al secreto descifrado y renderizan la entrada file: del entorno, de modo que el store solo ve la ruta.

Comprobar que la puerta está realmente cerrada

# estado de la puerta y providers registrados
curl -s http://YOUR_HOST:9119/api/status | jq '.auth_required, .auth_providers'
# true
# ["session-token"]

# con el token: 200
curl -s -o /dev/null -w '%{http_code}\n' \
  -H "Authorization: Bearer ***" http://YOUR_HOST:9119/api/auth/me
# 200

# sin el token: esta es la parte que importa
curl -s -o /dev/null -w '%{http_code}\n' http://YOUR_HOST:9119/api/auth/me
# 401

Para el gateway remoto del escritorio, pega ese mismo token en el campo “Session token”; se usa tanto en la sonda HTTP como en el upgrade del WebSocket.

Tabla de diagnóstico

Síntoma Causa Arreglo
El proceso sale al arrancar mencionando un token ilegible o vacío Ruta file: equivocada, permisos incorrectos o archivo vacío Haz cat del archivo con la cuenta que ejecuta el servicio
auth_providers está vacío en /api/status La variable nunca llegó al proceso del servicio Revisa Environment= de systemd, el plist o el env del contenedor
“HTTP reachable, but the WebSocket fails” La comprobación HTTP pega en una ruta pública mientras /api/ws necesita el token, o el token se rotó Pega el token nuevo; rotar implica redesplegar el secreto
Quieres basic auth u OIDC en la puerta y el token solo internamente El provider está activo por defecto Pon dashboard.session_token_auth.enabled: false

El opt-out, escrito del todo:

dashboard:
  session_token_auth:
    enabled: false

Cuándo no usarlo

  • Un dashboard compartido entre varias personas. Detrás de este token no hay identidad de usuario: quien lo tenga es administrador. Si necesitas saber quién cambió qué, usa OIDC (Authentik, Keycloak, Zitadel, Authelia, Auth0, Okta y compañía) o el provider de Nous.
  • Cualquier cosa expuesta a internet. Un bearer token de larga vida está a una filtración del acceso completo, y rotarlo obliga a actualizar todos los clientes. En despliegues públicos, mejor login interactivo.
  • Setups locales de un solo usuario. Un bind a loopback no necesita provider alguno; el escritorio y la SPA local usan el token interno. No añadas configuración que no necesitas.

Una regla operativa: trata el token como una contraseña. Guárdalo en un gestor de secretos, nunca en una imagen ni en un repositorio, y rótalo redesplegando el secreto en lugar de editar una variable de entorno. Así todas las copias de los clientes quedan invalidadas a la vez, lo que se manifiesta como ese desconcertante “WebSocket fails”.

Para el trasfondo de la puerta del dashboard, lee endurecimiento del login del dashboard. Si tu síntoma son clientes de escritorio cruzando tokens, mira la nota sobre colisión de session tokens, y para formas de autenticación a través de un túnel, OAuth sobre SSH. Los flags y los puertos por defecto están en la página de referencia de hermes dashboard.

Estado del release

El provider se integró en main el 28 de octubre de 2026 (PR #146139, construido sobre la resolución de archivos del #146093 y el renderizado de Nix del #146104). El release actual, v0.22.5 (23 de octubre de 2026), todavía no lo incluye.

Un token, las dos patas, ninguna página de login: un despliegue interno ya no tiene que inventarse un sistema de contraseñas solo para pasar la puerta.