github →

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.

Issue
enhancementauth

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 devops profile
  • Person C (coworker) — should only see the coworkers profile

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 user
  • profile: The resolved profile (or None for unrestricted access)
  • auth_type: "trusted"

On every subsequent request:

  1. Verify session cookie is valid
  2. If session.profile is set, enforce that the hermes_profile cookie matches
  3. 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 TypeLogout 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
PasskeysDelete cookies, redirect to /login
No auth enabledNo 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: Returns trusted_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 GroupHermes ProfilePurpose
hermes_devopsdevopsRoutes DevOps users to devops profile
hermes_coworkerscoworkersRoutes coworkers to coworkers profile
ai_usersControls access to the WebUI itself

This avoids polluting functional groups (like admins, solar_users) with Hermes-specific concerns.

Security Considerations

ConcernMitigation
Header spoofingHeaders are only trusted from localhost/reverse proxy; never exposed to the internet directly
Cookie tamperingSession stores bound profile; cookie mismatch → 401
Session fixationNew session created on each trusted-auth login; old sessions invalidated on logout
No auth bypassIf 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_HEADER is set, behavior is unchanged
  • Existing HERMES_WEBUI_PASSWORD continues 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 binding
  • api/routes.py/api/auth/logout endpoint, /api/auth/status extension
  • server.py — Cookie handling for trusted-auth sessions
  • static/ui.js / static/panels.js — Logout button, profile picker visibility
  • static/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
Assessmentadvisory
feature●● medium80% confidence

Adds trusted-header authentication and profile binding across the API server, configuration loader, and deployment environment definitions.

Likely files
  • entry/api/auth.py
  • entry/api/server.py
  • entry/bootstrap.py
  • static/login.js
  • infra/config/.env.example
Create the request

This opens a fresh agent run and a draft PR for issue #3351.

Cancel