New request
#3351: Feature Request: Trusted Header Authentication with Profile Binding
We'll provision a sandbox, run an agent against the issue, and open a draft PR. You can pull the branch and iterate from there.
Summary
Add support for external authentication systems (e.g., Authelia, Authentik, Keycloak, OAuth2-Proxy) to authenticate users and optionally bind them to specific Hermes profiles via HTTP headers. This enables multi-user deployments where different users automatically land in different profiles without managing separate passwords or login flows within Hermes WebUI itself.
Motivation
Currently, Hermes WebUI supports:
- Single shared password (
HERMES_WEBUI_PASSWORD) - Passkeys (WebAuthn)
Both require users to authenticate directly with the WebUI. In deployments where a reverse proxy already handles authentication (Authelia, Authentik, etc.), users must authenticate twice: once with the proxy, once with the WebUI. This is friction-heavy and prevents automatic profile assignment based on the user's identity or group membership.
Use Case
A self-hosted Hermes instance is shared among:
- Person A (admin) — needs access to all profiles
- Person B (DevOps team) — should only see the
devopsprofile - Person C (coworker) — should only see the
coworkersprofile
The reverse proxy (with Authelia) already knows who the user is and what groups they belong to. The WebUI should trust this information and route the user to the correct profile automatically.
Proposed Solution
1. Environment Variables
# Required: Header containing the authenticated username
HERMES_WEBUI_TRUSTED_AUTH_HEADER=Remote-User
# Optional: Header containing comma-separated group list
HERMES_WEBUI_TRUSTED_GROUPS_HEADER=Remote-Groups
# Optional: JSON mapping of group names to Hermes profile names
HERMES_WEBUI_GROUP_PROFILE_MAP='{"hermes_devops":"devops","hermes_coworkers":"coworkers"}'
# Optional: URL to redirect for logout (e.g., Authelia logout)
HERMES_WEBUI_TRUSTED_AUTH_LOGOUT_URL=https://auth.example.com/logout
2. Authentication Flow
Browser → Reverse Proxy (Authelia) → Hermes WebUI
│ │
│ Remote-User: alice │
│ Remote-Groups: ai_users,hermes_devops
│ │
└───────────────────────────┘
│
▼
1. Read trusted headers
2. Resolve profile from group mapping
3. Create session (auto-login)
4. Set cookies: hermes_session + hermes_profile
5. User lands in bound profile
3. Profile Resolution Logic
def resolve_profile_from_headers(headers):
user = headers.get('Remote-User')
if not user:
return None # No trusted auth, fall through to password/passkey
groups = headers.get('Remote-Groups', '').split(',')
group_map = json.loads(os.getenv('HERMES_WEBUI_GROUP_PROFILE_MAP', '{}'))
for group in groups:
if group in group_map:
return group_map[group] # e.g., "devops"
return None # No mapping matched → default profile
4. Session Binding
When a session is created via trusted header auth, it stores:
username: The authenticated userprofile: The resolved profile (orNonefor unrestricted access)auth_type:"trusted"
On every subsequent request:
- Verify session cookie is valid
- If
session.profileis set, enforce that thehermes_profilecookie matches - If the user tampered with the profile cookie → reject with 401
This prevents a user authenticated for devops from switching to coworkers by manipulating cookies.
5. Logout Behavior
| Auth Type | Logout Action |
|---|---|
| Trusted Header (Authelia) | Delete hermes_session + hermes_profile cookies, redirect to HERMES_WEBUI_TRUSTED_AUTH_LOGOUT_URL |
Password (HERMES_WEBUI_PASSWORD) | Delete cookies, redirect to /login |
| Passkeys | Delete cookies, redirect to /login |
| No auth enabled | No logout button shown |
6. UI Changes
- Profile picker: Hidden or read-only when session is profile-bound
- Profiles panel: Shows only the bound profile, no switch option
- Logout button: Visible in settings/topbar when any auth is enabled
/api/auth/status: Returnstrusted_auth_enabled,bound_profile,user
Example: Authelia + Docker Compose
Authelia Configuration
# configuration.yaml
access_control:
rules:
- domain: 'ai.example.com'
policy: one_factor
subject:
- 'group:ai_users'
# users_database.yaml
users:
alice:
displayname: "Alice Admin"
password: "$argon2id$..."
groups:
- ai_users
- hermes_devops # Dummy group → maps to "devops" profile
bob:
displayname: "Bob Coworker"
password: "$argon2id$..."
groups:
- ai_users
- hermes_coworkers # Dummy group → maps to "coworkers" profile
Docker Compose
services:
authelia:
image: authelia/authelia:latest
volumes:
- ./authelia:/config
# ...
hermes-webui:
image: ghcr.io/nesquena/hermes-webui:latest
environment:
HERMES_WEBUI_TRUSTED_AUTH_HEADER: Remote-User
HERMES_WEBUI_TRUSTED_GROUPS_HEADER: Remote-Groups
HERMES_WEBUI_GROUP_PROFILE_MAP: '{"hermes_devops":"devops","hermes_coworkers":"coworkers"}'
HERMES_WEBUI_TRUSTED_AUTH_LOGOUT_URL: https://auth.example.com/logout
# Password auth disabled — Authelia handles everything
# HERMES_WEBUI_PASSWORD: ""
labels:
- "traefik.http.routers.hermes.middlewares=authelia@docker"
Dummy Groups Pattern
The proposed design uses dummy groups in Authelia that serve no purpose other than profile mapping:
| Authelia Group | Hermes Profile | Purpose |
|---|---|---|
hermes_devops | devops | Routes DevOps users to devops profile |
hermes_coworkers | coworkers | Routes coworkers to coworkers profile |
ai_users | — | Controls access to the WebUI itself |
This avoids polluting functional groups (like admins, solar_users) with Hermes-specific concerns.
Security Considerations
| Concern | Mitigation |
|---|---|
| Header spoofing | Headers are only trusted from localhost/reverse proxy; never exposed to the internet directly |
| Cookie tampering | Session stores bound profile; cookie mismatch → 401 |
| Session fixation | New session created on each trusted-auth login; old sessions invalidated on logout |
| No auth bypass | If HERMES_WEBUI_TRUSTED_AUTH_HEADER is set but missing in request → fall through to password or 401 |
Backwards Compatibility
- Fully backwards compatible: If no
HERMES_WEBUI_TRUSTED_AUTH_HEADERis set, behavior is unchanged - Existing
HERMES_WEBUI_PASSWORDcontinues to work as fallback - Passkeys continue to work independently
Related Issues
- #1576 — Multi-user accounts + 2FA / passkeys / external IdP (umbrella issue, much broader scope)
- #1700 — Parallel multi-profile runs (already resolved; this feature builds on the cookie+TLS profile isolation)
- #798 — Profile isolation via cookie + thread-local context (foundation for this feature)
Implementation Notes
The implementation touches:
api/auth.py— Trusted header resolution, session bindingapi/routes.py—/api/auth/logoutendpoint,/api/auth/statusextensionserver.py— Cookie handling for trusted-auth sessionsstatic/ui.js/static/panels.js— Logout button, profile picker visibilitystatic/index.html— Logout button placement
Estimated effort: ~150 lines backend + ~50 lines frontend + tests.
Checklist
- I have searched existing issues and this is not a duplicate
- This feature aligns with the project's philosophy of simple, self-hosted auth
- I am willing to test this feature in my Authelia deployment
Adds trusted-header authentication and profile binding across the API server, configuration loader, and deployment environment definitions.
- entry/api/auth.py
- entry/api/server.py
- entry/bootstrap.py
- static/login.js
- infra/config/.env.example