Skip to content

Add an HTTPS listener for the web dashboard — prerequisite for OIDC, and the shared token currently crosses the LAN in the clear #2562

Description

@erikdarlingdata

Erik's call, 2026-08-23: yes, the product gets an HTTPS listener. This is the prerequisite #2550 (OIDC) is blocked on, and it is worth having on its own.

Where things stand

grep -c UseHttps across Darling/ returns 0. DarlingWebHostService calls options.Listen(...) with no TLS configuration at all, so the dashboard is plain HTTP in every mode.

Two consequences:

  1. OIDC cannot work in network mode. Most identity providers refuse a non-HTTPS redirect_uri, with the near-universal exception of http://localhost — which is the loopback case, the one mode that does not need OIDC. Network mode, which does, is the mode that cannot have it.
  2. The shared bearer token crosses the network in the clear, and per OIDC sign-in for the web dashboard: give the web surface a per-user identity instead of one shared token #2550 that token gates the dashboard's write paths (custom-view CRUD, alert tuning). The token→cookie exchange is well built — constant-time compare, HMAC-signed HttpOnly SameSite=Strict cookie, CIDR allowlist, Host-header anti-rebind — but all of it rides on an unencrypted transport once the listener is not on loopback.

That second point stands independently of OIDC. Even if #2550 never happens, a LAN-exposed dashboard behind a bearer token deserves TLS.

Decisions I will take unless told otherwise

  • Opt-in, not default-on. A loopback-only dashboard has no use for TLS and forcing a certificate on it would break the zero-config local case, which is the common one.
  • Operator-supplied certificate first. A PFX or PEM pair referenced from web.network, resolved through the existing file:/env: secret indirection (Darling on Linux: Docker/compose distribution of the service + TimescaleDB store #1804) rather than a new mechanism. Most people exposing this on a LAN have a cert, and an internal CA is the normal answer.
  • A self-signed fallback is a maybe, not a default. It buys encryption without authentication and trains people to click through warnings. If it ships it should be explicit and loud, not silent.
  • Config lives in web.network alongside listen/allowFrom/token, and is therefore file-only and restart-only, matching what that block already logs about itself.
  • HTTP does not silently keep working when TLS is configured. Either redirect or refuse — decided during implementation, but not "both quietly listening".

What needs care

  • Kestrel binds both loopback and the primary address today (DarlingHostBinding's ladder). TLS must not change which addresses are bound or the CIDR gate's behaviour — those are separately reasoned and pinned in DarlingWebAuthTests.
  • The Host-header allowlist (anti-DNS-rebind) is always on and must stay on.
  • Certificate loading has to fail closed. The existing precedent is right there: an undecryptable web token refuses to expose and binds loopback-only rather than exposing tokenless. An unreadable or expired certificate should behave the same way — never silently downgrade to HTTP.
  • Expiry is an operational trap. A certificate that expires takes the dashboard down. At minimum, log the expiry at startup and warn when it is close.
  • The container path (Darling/compose/) mounts secrets already; a certificate is one more of those, and darling.sample.json should show it.

Not in scope

Certificate issuance or ACME. The product should consume a certificate, not manage a PKI.

Related: #2550 (OIDC, blocked on this), #1804 (the file:/env: secret indirection to reuse), #1562 / #1649 (the current browser auth model this must not disturb).

Activity

  1. erikdarlingdata commented on Aug 23, 2026

    @erikdarlingdata
    OwnerAuthor

    Shipped in #2568 (merged to dev as cc335836).

    web.network.tls takes a PKCS#12 bundle or a PEM pair — one form, never both — applied to the network listener only. Loopback stays plain HTTP because the certificate names the LAN address rather than localhost. Redirect-versus-refuse settled by mechanism: one port cannot speak both schemes, so a plain-HTTP client fails the handshake, and a second HTTP port to redirect from would reopen the surface this closes. The session cookie's Secure flag became per-request, since one host now serves both schemes. Fails closed exactly as an undecryptable token does — missing, unreadable, ambiguous, not-yet-valid or expired means Critical then loopback-only — with a 30-day warning before an expiry takes the dashboard down.

    Four things the issue did not anticipate, all found by running it rather than reasoning about it:

    • Key storage flags are wrong in two opposite directions. EphemeralKeySet looks obviously right; Windows accepts it and then fails every handshake, and macOS refuses the flag outright. MachineKeySet on Windows (no PersistKeySet, so disposal removes the key material), default key set elsewhere.
    • Both loaders dropped the intermediate chain. CreateFromPemFile materializes only the first certificate in a PEM and LoadPkcs12FromFile only one of a bundle, so a leaf+intermediate file — exactly what this issue's own config doc told operators to supply — served an incomplete chain. Measured with openssl s_client -showcerts: two certificates on disk, one on the wire. Now two.
    • The leaf could be the root. "First certificate with a private key" served CN=darling-test-root from a bundle exported wholesale off a CA machine. The leaf is now the terminal node — holds a key AND issued nothing else in the bundle.
    • The certificate must carry an iPAddress SAN. The anti-DNS-rebind Host allowlist accepts only localhost, a loopback literal, or the listen IP, so a LAN client cannot reach the dashboard by DNS name — which makes the DNS-name certificate an internal CA issues by default permanently unusable here. The service now says so at startup instead of leaving it to a browser warning.

    That last one led to #2569, filed separately: a wildcard listen (0.0.0.0) makes the same guard refuse every LAN request with a 400, and that is what the compose distribution ships.

    Also corrected two doc comments stale since #1649 that still claimed loopback passes tokenless while exposed, and --configure-network now reports TLS state alongside listen/allowFrom.

    #2550 (OIDC) is unblocked — this was its prerequisite.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions