Files
harmony/docs/guides/web-auth-security.md
Jean-Gabriel Gill-Couture 75aac243c7
All checks were successful
Run Check Script / check (pull_request) Successful in 2m18s
docs: operator dashboard SSO (Zitadel) setup guide
Step-by-step for wiring the operator dashboard's browser SSO: the Zitadel
app settings the code requires (Web app, PKCE/no-secret, redirect +
post-logout URIs), each config value mapped to its source
(ZitadelAuthConfig + cookie key), how to provide them (staging via
FleetDeployConfig/Secrets with hosts derived from base_domain; local via
HARMONY_CONFIG_* env), the derived endpoints, and the common-failure
gotchas (iss/aud/redirect mismatch, no client secret, localhost dev mode,
≥64-byte cookie key). Grounded in harmony_zitadel_auth's login/jwks code.

Registered in SUMMARY and cross-linked from web-auth-security.
2026-06-01 23:06:45 -04:00

8.3 KiB

Web Authentication and CSRF Security Guidelines

These guidelines define the baseline for Harmony web frontends and future operator dashboards that use browser-based authentication, cookie sessions, Axum, HTMX, or OIDC providers such as Zitadel.

Setting up the fleet operator dashboard's SSO concretely (Zitadel app + the exact config values)? See Operator Dashboard SSO (Zitadel) — setup.

Goals

  • Prevent unauthenticated access.
  • Prevent authenticated users from performing actions they are not authorized to perform.
  • Prevent CSRF on state-changing endpoints.
  • Reduce XSS impact with CSP and safe rendering practices.
  • Keep authentication code understandable and reusable across projects.

Required Baseline

Every browser-facing authenticated application must implement the following controls before production use:

  1. OIDC Authorization Code + PKCE for login.
  2. OIDC nonce validation on login callback.
  3. Explicit authorization checks using roles, groups, claims, or permissions.
  4. CSRF protection on all mutating routes.
  5. Secure cookie settings: HttpOnly, Secure in production, constrained SameSite, and appropriate path/domain scoping.
  6. Strict security headers, especially Content Security Policy.
  7. No permissive credentialed CORS for operator dashboards.
  8. Generic client-facing errors with detailed errors logged server-side only.

OIDC Login Requirements

Use Authorization Code flow with PKCE. On login start, generate and persist a short-lived login attempt containing:

  • state
  • pkce_code_verifier
  • nonce
  • creation timestamp or cookie expiration

Send state, PKCE challenge, and nonce to the authorization endpoint.

On callback:

  1. Require a valid login-attempt cookie.
  2. Validate returned state against the stored state.
  3. Exchange the authorization code using the stored PKCE verifier.
  4. Validate the returned ID token as an OIDC ID token, including:
    • signature
    • issuer
    • audience/client ID
    • expiration/not-before
    • nonce
    • authorized party (azp) when applicable
  5. Create the application session only after all checks pass.
  6. Delete the login-attempt cookie.

state and nonce are not interchangeable:

  • state binds the callback redirect to the browser login attempt.
  • nonce binds the returned ID token to the browser login attempt.
  • PKCE binds the code exchange to the client that started the flow.

Session Requirements

For small internal dashboards, a verified short-lived ID token in an HttpOnly cookie may be acceptable. For higher-risk systems, prefer server-side sessions:

  • Store a random session ID in the browser cookie.
  • Store tokens and session metadata server-side.
  • Support revocation, rotation, idle timeout, and absolute timeout.

Session cookies must use:

  • HttpOnly
  • Secure outside local development
  • SameSite=Lax or SameSite=Strict
  • Path=/ unless a narrower path is possible
  • No broad Domain attribute unless explicitly required

Production services should fail closed if HTTPS/secure-cookie configuration is inconsistent.

Authorization Requirements

Authentication is not authorization. A valid identity provider token only proves who the user is.

Every protected application must define required permissions for each state-changing or sensitive route. Examples:

  • fleet:viewer for read-only dashboard access
  • fleet:operator for alert acknowledgement and operational actions
  • fleet:admin for settings, user management, or destructive actions

Authorization must be enforced server-side. UI hiding is not sufficient.

CSRF Protection Standard

For Axum + HTMX dashboards, the recommended baseline is:

  1. Require a custom header on all mutating requests.
  2. Validate Origin or Referer against the configured application origin.
  3. Keep cookies SameSite=Lax or stricter.
  4. Do not enable permissive credentialed CORS.

Mutating methods are:

  • POST
  • PUT
  • PATCH
  • DELETE

Recommended behavior:

  • Reject mutating requests without x-csrf-token.
  • Reject mutating requests whose Origin is present and does not match the configured base URL origin.
  • If Origin is absent, require Referer to match the configured base URL origin.
  • Reject when neither Origin nor Referer is available, unless the route is explicitly exempted and documented.

The CSRF header value may be static for HTMX dashboards, for example x-csrf-token: 1. The protection comes from the fact that cross-origin HTML forms cannot set custom headers, and cross-origin JavaScript cannot send custom headers with credentials unless CORS allows it.

Do not rely on header presence alone if adding Origin/Referer validation is practical.

HTMX Integration

Add the CSRF header globally from a static JavaScript file:

document.body.addEventListener('htmx:configRequest', (event) => {
  event.detail.headers['x-csrf-token'] = '1';
});

Serve this as a static asset, for example /static/app.js. Avoid inline scripts so that the application can use a strict CSP without unsafe-inline.

Content Security Policy

Every browser-facing dashboard should set a restrictive CSP. A good starting point is:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'

Meaning:

  • Only load scripts, styles, and API/SSE/HTMX connections from the same origin.
  • Prevent clickjacking with frame-ancestors 'none'.
  • Prevent plugin/object execution with object-src 'none'.
  • Prevent injected <base> tags from rewriting relative URLs.
  • Prevent forms from submitting to external origins.

If inline scripts or styles are unavoidable, prefer per-response nonces over unsafe-inline.

Other Security Headers

Set these headers on all HTML responses, or globally when safe:

X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()

When the service is HTTPS-only, also set HSTS:

Strict-Transport-Security: max-age=31536000; includeSubDomains

Only enable HSTS when the domain and subdomains are intended to be HTTPS-only.

CORS Policy

Operator dashboards should normally not enable CORS.

Never combine all of the following unless there is a reviewed, explicit integration need:

  • credentialed requests
  • arbitrary or reflected origins
  • custom request headers such as x-csrf-token

A permissive credentialed CORS policy can bypass custom-header CSRF protection.

Error Handling

Client-facing auth errors should be generic, for example:

Authentication failed. Please start login again.

Detailed causes, provider responses, token validation failures, and stack traces should be logged server-side only.

Avoid returning raw OIDC provider error bodies or JWT validation details to the browser.

Implementation Checklist

Before shipping a Harmony web frontend:

  • Login uses Authorization Code + PKCE.
  • Login attempt stores state, PKCE verifier, nonce, and expires quickly.
  • Callback validates state.
  • Callback validates ID token nonce.
  • JWT validation checks issuer and exact intended audience/client.
  • Authorization roles/permissions are enforced server-side.
  • Mutating routes are protected by CSRF middleware.
  • CSRF middleware requires custom header and same-origin Origin/Referer.
  • Session cookies are HttpOnly, Secure in production, and SameSite=Lax or stricter.
  • No permissive credentialed CORS is enabled.
  • CSP is configured without unsafe-inline where practical.
  • Security headers are configured.
  • Auth errors shown to users are generic.
  • Detailed auth failures are logged server-side.

For current and future Axum + HTMX dashboards, use this default design:

  • Zitadel/OIDC Authorization Code + PKCE + nonce.
  • Short-lived encrypted login-attempt cookie.
  • Server-side authorization middleware based on roles/claims.
  • HttpOnly, Secure, SameSite=Lax or Strict session cookie.
  • CSRF middleware requiring x-csrf-token and same-origin Origin/Referer.
  • Static /static/app.js that adds the HTMX CSRF header.
  • Strict CSP that allows scripts only from self.
  • No CORS unless explicitly reviewed.