docs(dashboard): clarify auth provider suitability + registration across dashboard/Docker/Desktop docs (#39633)
* docs(dashboard): clarify auth provider suitability + document dashboard registration - Add a 'Registering a dashboard' subsection under the Nous Research provider covering both the 'hermes dashboard register' CLI command and the Portal /local-dashboards GUI page. - Note that the Nous provider is the one suitable for public-internet exposure (logins verified against your Nous account). - Add a warning that the username/password provider is for trusted networks / VPN only and is not suitable for direct public-internet exposure; point readers to the Nous / OIDC / custom OAuth providers. - Surface the same distinction in the two-provider intro list. * docs(dashboard): count three bundled auth providers, add self-hosted OIDC to intro 'Two providers ship in the box' undercounted — the bundled plugins/dashboard_auth/self_hosted (generic OpenID Connect) is a third. List all three in the gated-mode intro and link each to its section. * docs(dashboard): extend auth provider updates to Docker and Desktop pages - docker.md: list all three bundled gate providers (was username/password + OAuth only), adding the self-hosted OIDC provider and its env vars, and note username/password is not for public-internet exposure. - desktop.md: reframe the remote-backend connection so OAuth (Nous Portal) is the preferred option for any backend reachable beyond the local machine, with username/password positioned for local / trusted-network use only. Cover the 'Sign in with <provider>' OAuth flow in the in-app steps and scope the VPN warning to the password path. * docs(dashboard): align env-var, CLI, and remote-Desktop recipe with provider changes - environment-variables.md: reframe the Web Dashboard & Hermes Desktop intro (OAuth preferred for remote/public, username/password for trusted networks), add the self-hosted OIDC env vars (HERMES_DASHBOARD_OIDC_*) that were missing from the table, and note hermes dashboard register provisions the OAuth client_id. - cli-commands.md: document the 'hermes dashboard register' subcommand (flags, behavior, /local-dashboards GUI alternative). - web-dashboard.md: apply the OAuth-preferred reframe to the bottom 'Connecting Hermes Desktop to a remote backend' recipe and scope its VPN warning to the username/password path, matching desktop.md. * docs(dashboard): move 'recommended remote Desktop path' framing from username/password to OAuth The gated-mode intro list claimed the username/password provider was the recommended path for a remote Hermes Desktop connection, contradicting the OAuth-preferred framing established elsewhere. Move that recommendation onto the OAuth (Nous Portal) item so the docs are consistent: OAuth is the recommended provider for any remote/internet-facing backend; username/password is for trusted networks only. * docs(dashboard): drop unreleased managed/hosted-install provisioning notes Remove the 'not available in managed/hosted installs, where the client id is provisioned by the hosting platform' line from the dashboard register docs (web-dashboard.md, cli-commands.md) and the 'provisioned by the Nous Portal for hosted deploys' clause from the HERMES_DASHBOARD_OAUTH_CLIENT_ID env-var row — that platform-provisioning path is unreleased. * docs(dashboard): drop --portal-url / HERMES_DASHBOARD_PORTAL_URL from user docs The portal-URL override targets a non-production Nous Portal and only works for internal Nous usage — it won't function for end users (the access token must be issued by the same portal). Remove it from the register CLI flags, the Nous-provider config/env tables, and the verify-the-gate example so users aren't pointed at an option that can't work for them. * docs(dashboard): add worked examples for Nous and username/password providers The self-hosted OIDC provider already had a full 'Worked example: Keycloak' walkthrough; the Nous and username/password providers only had scattered config snippets. Add parallel '#### Worked example' sections for both (register/run/login + /api/status verification), mirroring the Keycloak example's structure so all three bundled providers read consistently. * docs(env): move HERMES_DESKTOP_REMOTE_URL to end of the dashboard auth table It was sitting between the HERMES_DASHBOARD_BASIC_AUTH_* block and the HERMES_DASHBOARD_OAUTH/OIDC block, splitting the dashboard-side vars. As the only desktop-side var in the table, it belongs at the end so the dashboard provider vars (basic, OAuth, OIDC) stay grouped together. * docs(dashboard): remove Fly.io references from dashboard auth docs Fly.io is the internal hosting implementation for hosted Hermes — it shouldn't leak into user-facing dashboard auth docs. Reword the OAuth provider intro, the env-var-path rationale, the public-URL-override section, the cookie Secure note, and the verify-the-gate example to generic 'hosting platform' / 'reverse proxy' / 'TLS terminator' phrasing. Left the legitimate user-facing Fly.io mentions in telegram.md (a deliberate cloud-deployment walkthrough) and work-with-skills.md (a generic example) untouched.
This commit is contained in:
@@ -493,10 +493,11 @@ same auth gate as the rest of `/api/`.
|
||||
|
||||
## Authentication (gated mode)
|
||||
|
||||
When the dashboard is bound to a public or non-loopback address — anything other than `127.0.0.1` / `localhost` — Hermes Agent engages an auth gate. Every request must carry a verified session cookie or it's bounced to the login page. Two providers ship in the box:
|
||||
When the dashboard is bound to a public or non-loopback address — anything other than `127.0.0.1` / `localhost` — Hermes Agent engages an auth gate. Every request must carry a verified session cookie or it's bounced to the login page. Three providers ship in the box:
|
||||
|
||||
- **[Username/password](#usernamepassword-provider-no-oauth-idp)** — the simplest way to put auth on a self-hosted / on-prem / homelab dashboard (and the recommended path for a [remote Hermes Desktop connection](#connecting-hermes-desktop-to-a-remote-backend)). No external identity provider.
|
||||
- **OAuth (Nous Portal)** — for hosted deployments (typically Fly.io) where the dashboard is reachable over the public internet.
|
||||
- **[Username/password](#usernamepassword-provider-no-oauth-idp)** — the simplest way to put auth on a self-hosted / on-prem / homelab dashboard. No external identity provider. **Use it only on a trusted network or behind a VPN — not for public-internet exposure.**
|
||||
- **[OAuth (Nous Portal)](#default-provider-nous-research)** — for hosted deployments and any dashboard reachable over the public internet, and the recommended path for a [remote Hermes Desktop connection](#connecting-hermes-desktop-to-a-remote-backend). Every login is verified against your Nous account, so this is the provider suitable for internet-facing use.
|
||||
- **[Self-hosted OIDC](#self-hosted-oidc-provider)** — for bringing your own identity provider via standard OpenID Connect (Keycloak, Auth0, Okta, Google, GitHub via an OIDC bridge, etc.). No Nous Portal involved; suitable for public-internet exposure when fronted by a conformant OIDC server.
|
||||
|
||||
Operator-owned dashboards bound to loopback are unaffected — no auth, no login page.
|
||||
|
||||
@@ -524,6 +525,22 @@ If the gate would engage but **no** `DashboardAuthProvider` is registered (no No
|
||||
|
||||
The bundled `plugins/dashboard_auth/nous` plugin is **always installed** and auto-loaded. It auto-registers a `DashboardAuthProvider` named `nous` when a client ID is configured.
|
||||
|
||||
Because every login is verified against Nous Portal and protected by your Nous account, **the Nous provider is the one suitable for exposing a dashboard to the public internet.**
|
||||
|
||||
#### Registering a dashboard
|
||||
|
||||
To use the Nous provider you need an OAuth client ID (shape `agent:{id}`). There are two ways to get one:
|
||||
|
||||
- **CLI — `hermes dashboard register`.** Run it on the host where the dashboard lives. It resolves your existing Nous login (run `hermes setup` first if you're not logged in), registers a self-hosted OAuth client with the Portal, and writes `HERMES_DASHBOARD_OAUTH_CLIENT_ID` into `~/.hermes/.env` for you. Optional flags: `--name` (a human-readable label, otherwise auto-generated) and `--redirect-uri` (a public HTTPS callback URL for an internet-facing host).
|
||||
|
||||
```bash
|
||||
hermes dashboard register
|
||||
# ✓ Registered dashboard "swift_falcon"
|
||||
# …writes HERMES_DASHBOARD_OAUTH_CLIENT_ID to ~/.hermes/.env
|
||||
```
|
||||
|
||||
- **GUI — the Local Dashboards page.** Open [`/local-dashboards`](https://portal.nousresearch.com/local-dashboards) in the Nous Portal to register, name, manage, and revoke self-hosted dashboards from the browser. Copy the resulting `agent:{id}` client ID into `HERMES_DASHBOARD_OAUTH_CLIENT_ID` (env) or `dashboard.oauth.client_id` (config.yaml). This is also where you revoke a dashboard registered via the CLI.
|
||||
|
||||
#### Configuration
|
||||
|
||||
The plugin reads from two surfaces, with the environment variable winning when set non-empty:
|
||||
@@ -534,19 +551,17 @@ The plugin reads from two surfaces, with the environment variable winning when s
|
||||
dashboard:
|
||||
oauth:
|
||||
client_id: agent:01HXYZ… # required to engage the gate
|
||||
portal_url: https://portal.nousresearch.com # optional; defaults to production
|
||||
```
|
||||
|
||||
**Environment variables** — operator overrides:
|
||||
|
||||
| Env var | Overrides | Format | Provisioned by |
|
||||
|---------|-----------|--------|----------------|
|
||||
| `HERMES_DASHBOARD_OAUTH_CLIENT_ID` | `dashboard.oauth.client_id` | `agent:{instance_id}` | Nous Portal at Fly.io provisioning time |
|
||||
| `HERMES_DASHBOARD_PORTAL_URL` | `dashboard.oauth.portal_url` | URL (default: `https://portal.nousresearch.com`) | Portal — override only for staging or a custom deployment |
|
||||
| `HERMES_DASHBOARD_OAUTH_CLIENT_ID` | `dashboard.oauth.client_id` | `agent:{instance_id}` | `hermes dashboard register` |
|
||||
|
||||
Per the Hermes Agent convention (`~/.hermes/.env` is for API keys / secrets only), **`config.yaml` is the recommended place to set these values** for local dev, on-prem, and any deployment you control directly. The environment-variable path exists so Fly.io's platform-secret injection can push per-deploy `client_id`s without anyone having to edit `config.yaml` inside the image — that's its primary purpose.
|
||||
Per the Hermes Agent convention (`~/.hermes/.env` is for API keys / secrets only), **`config.yaml` is the recommended place to set these values** for local dev, on-prem, and any deployment you control directly. The environment-variable path exists so a hosting platform's secret injection can push per-deploy `client_id`s without anyone having to edit `config.yaml` inside the image — that's its primary purpose.
|
||||
|
||||
Empty environment values are treated as unset, so a provisioned-but-not-populated Fly secret can't accidentally shadow a valid `config.yaml` entry.
|
||||
Empty environment values are treated as unset, so a provisioned-but-not-populated platform secret can't accidentally shadow a valid `config.yaml` entry.
|
||||
|
||||
If neither source provides a client_id, the plugin reports the specific reason and the dashboard's fail-closed bind error tells you exactly what to fix:
|
||||
|
||||
@@ -566,12 +581,45 @@ Or pass --insecure to skip the auth gate (NOT recommended on untrusted
|
||||
networks).
|
||||
```
|
||||
|
||||
#### Worked example: Nous Research
|
||||
|
||||
From a logged-in Hermes install to a Nous-gated dashboard in three steps.
|
||||
|
||||
**1. Log in and register the dashboard.** `hermes dashboard register` uses your existing Nous login to provision an OAuth client and writes `HERMES_DASHBOARD_OAUTH_CLIENT_ID` into `~/.hermes/.env` for you:
|
||||
|
||||
```bash
|
||||
hermes setup # if you're not already logged into Nous Portal
|
||||
hermes dashboard register
|
||||
# ✓ Registered dashboard "swift_falcon"
|
||||
# …writes HERMES_DASHBOARD_OAUTH_CLIENT_ID to ~/.hermes/.env
|
||||
```
|
||||
|
||||
**2. Run the dashboard on a reachable address.** A non-loopback bind without `--insecure` engages the OAuth gate, and the `client_id` just written activates the `nous` provider:
|
||||
|
||||
```bash
|
||||
hermes dashboard --host 0.0.0.0 --port 9119 --no-open
|
||||
```
|
||||
|
||||
**3. Log in.** Open `http://<host>:9119/`, you'll be bounced to `/login`. Click **Sign in with Nous Research** → authenticate at the Portal → land back on the authenticated dashboard. Verify the gate from any machine:
|
||||
|
||||
```bash
|
||||
curl -s http://<host>:9119/api/status | jq '.auth_required, .auth_providers'
|
||||
# true
|
||||
# ["nous"]
|
||||
```
|
||||
|
||||
`GET /api/auth/me` then returns the verified session (`provider: nous`). For an internet-facing host, register with `--redirect-uri https://hermes.example.com/auth/callback` and set `HERMES_DASHBOARD_PUBLIC_URL` so the OAuth callback resolves to your public URL (see [Public URL override](#public-url-override)).
|
||||
|
||||
### Username/password provider (no OAuth IDP)
|
||||
|
||||
If you don't want to wire up an OAuth identity provider — a self-hosted "just put a password on my dashboard" deployment — the bundled `plugins/dashboard_auth/basic` plugin registers a `DashboardAuthProvider` named `basic` that authenticates with a **username and password** instead of an OAuth redirect.
|
||||
|
||||
It plugs into the same gate as the OAuth provider: the gate engages on a non-loopback bind without `--insecure`, the login page renders a credential form for this provider (instead of a "Log in with X" button), and everything downstream of login — session cookies, transparent refresh, WS tickets, logout, the audit log — is identical to the OAuth path. Sessions are stateless HMAC-signed tokens the provider mints itself, so there's **no database and no external IDP**. Password hashing uses stdlib `scrypt` (no third-party dependency).
|
||||
|
||||
:::warning Use this on trusted networks only — not the public internet
|
||||
The username/password provider is intended for self-hosted / on-prem / homelab dashboards on a **trusted network**, or reachable only over a **VPN**. It protects a single shared credential with no external identity provider, MFA, or per-user accounts behind it, so it is **not suitable for exposing a dashboard directly to the public internet**. For an internet-facing dashboard, use the [Nous Research provider](#default-provider-nous-research) (or your own [self-hosted OIDC](#self-hosted-oidc-provider) / [custom OAuth](#custom-providers) provider) instead.
|
||||
:::
|
||||
|
||||
#### Configuration
|
||||
|
||||
Like the Nous provider, it reads from `config.yaml` (canonical) with environment variables winning when set non-empty. It activates only when `username` plus either `password_hash` (preferred) or `password` are configured — otherwise it's a no-op, so OAuth users and loopback/`--insecure` operators are unaffected.
|
||||
@@ -607,6 +655,40 @@ When `secret` is empty, a random per-process signing key is generated. That's fi
|
||||
|
||||
The `/auth/password-login` endpoint is rate-limited per client IP (default 10 attempts/minute → HTTP 429) and returns a single generic `401 Invalid credentials` for both unknown users and wrong passwords, so it can't be used as a username-enumeration oracle.
|
||||
|
||||
#### Worked example: username/password
|
||||
|
||||
From nothing to a password-gated dashboard on a trusted network in three steps.
|
||||
|
||||
**1. Set credentials in `~/.hermes/.env`.** Hash the password so no plaintext sits at rest, and set a stable signing secret so sessions survive restarts:
|
||||
|
||||
```bash
|
||||
# Compute a scrypt hash of your chosen password:
|
||||
HASH=$(python -c "from plugins.dashboard_auth.basic import hash_password; print(hash_password('choose-a-strong-password'))")
|
||||
|
||||
cat >> ~/.hermes/.env <<EOF
|
||||
HERMES_DASHBOARD_BASIC_AUTH_USERNAME=admin
|
||||
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD_HASH=$HASH
|
||||
HERMES_DASHBOARD_BASIC_AUTH_SECRET=$(openssl rand -base64 32)
|
||||
EOF
|
||||
chmod 600 ~/.hermes/.env
|
||||
```
|
||||
|
||||
**2. Run the dashboard on a reachable address.** A non-loopback bind without `--insecure` engages the gate, and the username + hash activate the `basic` provider:
|
||||
|
||||
```bash
|
||||
hermes dashboard --host 0.0.0.0 --port 9119 --no-open
|
||||
```
|
||||
|
||||
**3. Log in.** Open `http://<host>:9119/`, you'll be bounced to `/login` — a **credential form** (not a "Sign in with X" button). Enter `admin` / your password → land on the authenticated dashboard. Verify the gate from any machine:
|
||||
|
||||
```bash
|
||||
curl -s http://<host>:9119/api/status | jq '.auth_required, .auth_providers'
|
||||
# true
|
||||
# ["basic"]
|
||||
```
|
||||
|
||||
`GET /api/auth/me` then returns the verified session (`provider: basic`). Keep this behind a VPN — see the warning above; for a public host use the [Nous Research](#default-provider-nous-research) or [self-hosted OIDC](#self-hosted-oidc-provider) provider instead.
|
||||
|
||||
#### Writing your own password provider
|
||||
|
||||
`basic` is just one implementation of an extension point. Any plugin can register a password provider: set `supports_password = True` on your `DashboardAuthProvider` subclass and implement `complete_password_login(*, username, password) -> Session` (raise `InvalidCredentialsError` on rejection, `ProviderError` if your backing store is down). The OAuth `start_login` / `complete_login` methods can be left as `NotImplementedError` stubs for a pure-password provider. This is the path for LDAP-bind, a credentials database, or any other non-redirect auth scheme — the framework handles the form, the route, the cookies, and refresh for you.
|
||||
@@ -739,9 +821,9 @@ engages the OAuth gate.
|
||||
|
||||
### Public URL override
|
||||
|
||||
By default, the dashboard reconstructs the OAuth callback URL from the request — `X-Forwarded-Host` + `X-Forwarded-Proto` + `X-Forwarded-Prefix` (when uvicorn is configured with `proxy_headers=True`, which `start_server` enables under the gate). This works out of the box on Fly.io, which sets all three headers correctly.
|
||||
By default, the dashboard reconstructs the OAuth callback URL from the request — `X-Forwarded-Host` + `X-Forwarded-Proto` + `X-Forwarded-Prefix` (when uvicorn is configured with `proxy_headers=True`, which `start_server` enables under the gate). This works out of the box behind a reverse proxy that sets all three headers correctly.
|
||||
|
||||
For deploys behind reverse proxies that don't reliably forward those headers (manual nginx setups, on-prem ingresses, custom-domain Fly deploys with partial proxy chains), set `dashboard.public_url` (or `HERMES_DASHBOARD_PUBLIC_URL`) to the **complete public URL** the dashboard is reached at:
|
||||
For deploys behind reverse proxies that don't reliably forward those headers (manual nginx setups, on-prem ingresses, custom-domain deploys with partial proxy chains), set `dashboard.public_url` (or `HERMES_DASHBOARD_PUBLIC_URL`) to the **complete public URL** the dashboard is reached at:
|
||||
|
||||
```yaml
|
||||
dashboard:
|
||||
@@ -755,7 +837,7 @@ Same precedence as the other dashboard settings — env wins over `config.yaml`:
|
||||
| Surface | Override path | When to use |
|
||||
|---------|---------------|-------------|
|
||||
| `dashboard.public_url` in `config.yaml` | `HERMES_DASHBOARD_PUBLIC_URL` | Local dev / on-prem (canonical) |
|
||||
| `HERMES_DASHBOARD_PUBLIC_URL` env var | — | Fly.io platform secrets / CI |
|
||||
| `HERMES_DASHBOARD_PUBLIC_URL` env var | — | Hosting-platform secrets / CI |
|
||||
| (unset) | — | Default — reconstruct from `X-Forwarded-*` headers |
|
||||
|
||||
Validation rejects values without `http://` / `https://` scheme, without a host, or containing quote / angle / whitespace / control characters. A malformed value silently falls through to header reconstruction so the login flow keeps working rather than dispatching the user to a hostile URL.
|
||||
@@ -783,7 +865,7 @@ Access tokens have a 15-minute TTL. **There is no refresh token in contract v1**
|
||||
| `hermes_session_pkce` | 10 min | HttpOnly; holds the PKCE verifier + provider hint during the round trip |
|
||||
| `hermes_session_rt` | unused in v1 | Reserved for forward-compat; not written when `refresh_token` is empty |
|
||||
|
||||
All three are `Path=/` and `SameSite=Lax`. The `Secure` flag is set when the dashboard is reached over HTTPS (detected via the request URL scheme — honours `X-Forwarded-Proto` from Fly's TLS terminator under `proxy_headers=True`).
|
||||
All three are `Path=/` and `SameSite=Lax`. The `Secure` flag is set when the dashboard is reached over HTTPS (detected via the request URL scheme — honours `X-Forwarded-Proto` from an upstream TLS terminator under `proxy_headers=True`).
|
||||
|
||||
### Logout
|
||||
|
||||
@@ -820,8 +902,7 @@ The login page lists all registered providers; multiple providers can be stacked
|
||||
### Verifying the gate is on
|
||||
|
||||
```bash
|
||||
# Quick env-var path (Fly.io shape). HERMES_DASHBOARD_PORTAL_URL is
|
||||
# optional — defaults to production.
|
||||
# Quick env-var path.
|
||||
HERMES_DASHBOARD_OAUTH_CLIENT_ID=agent:test \
|
||||
hermes dashboard --host 0.0.0.0
|
||||
|
||||
@@ -846,7 +927,9 @@ The dashboard's React StatusPage shows the same fields under "Web server". A sid
|
||||
|
||||
Hermes Desktop can drive a Hermes backend running on another machine (a VPS, a home server, a Mini behind Tailscale). In the app this lives under **Settings → Gateway → Remote gateway**, which asks for a **Remote URL** and a way to **Sign in**. (For the desktop app itself — install, settings, chat — see the [Hermes Desktop](/user-guide/desktop) page.)
|
||||
|
||||
You protect the remote dashboard with a **username and password** (the bundled [username/password provider](#usernamepassword-provider-no-oauth-idp)). Binding the dashboard to a non-loopback address engages its auth gate; the username/password provider is what the desktop app signs in against. Once signed in, Desktop reuses the session for the chat WebSocket automatically — there is no token to copy or paste.
|
||||
You protect the remote dashboard with one of the bundled auth providers, and the desktop app signs in against whichever one the backend advertises. For a backend reachable beyond your own machine — a VPS, a public host, anything internet-facing — the recommended provider is **OAuth (Nous Portal)** (register it with [`hermes dashboard register`](#registering-a-dashboard) and sign in with *Sign in with Nous Research*). The bundled [username/password provider](#usernamepassword-provider-no-oauth-idp) is the quickest option when the backend is on a trusted LAN or reachable only over a VPN, but is **not suitable for direct public-internet exposure**. Binding the dashboard to a non-loopback address engages its auth gate; once signed in, Desktop reuses the session for the chat WebSocket automatically — there is no token to copy or paste.
|
||||
|
||||
The recipe below uses the username/password path because it's the quickest to stand up on a trusted network; for the OAuth path see [Default provider: Nous Research](#default-provider-nous-research).
|
||||
|
||||
### On the backend (the remote machine)
|
||||
|
||||
@@ -870,7 +953,7 @@ Prefer no plaintext at rest? Use `HERMES_DASHBOARD_BASIC_AUTH_PASSWORD_HASH` wit
|
||||
If you run the dashboard as a systemd service, `~/.hermes/.env` is picked up automatically when the unit has `EnvironmentFile=%h/.hermes/.env`, so the credentials are in the environment at boot.
|
||||
|
||||
:::warning
|
||||
The dashboard reads and writes your `.env` (API keys, secrets) and can run agent commands. Even behind a username and password, never expose it directly to the open internet. Put it behind a VPN. [Tailscale](https://tailscale.com/) is the clean option: bind to the machine's tailscale IP (`--host <tailscale-ip>`) and use `http://<tailscale-ip>:9119` as the Remote URL. Only devices on your tailnet can reach it.
|
||||
The dashboard reads and writes your `.env` (API keys, secrets) and can run agent commands. The **username/password** setup shown here is for a trusted network — never expose a password-protected dashboard directly to the open internet. Put it behind a VPN. [Tailscale](https://tailscale.com/) is the clean option: bind to the machine's tailscale IP (`--host <tailscale-ip>`) and use `http://<tailscale-ip>:9119` as the Remote URL. Only devices on your tailnet can reach it. To reach a backend over the public internet, use the **OAuth (Nous Portal)** provider instead.
|
||||
:::
|
||||
|
||||
### In Hermes Desktop
|
||||
|
||||
Reference in New Issue
Block a user