Replies: 4 comments 4 replies
|
I agree with all statements above, I would only release this in a single v12 release instead of having more phases. Aobut the zwave js serve thing I think it could be worth adding a tracking issue there too with a reference to this one |
|
Strong RFC — I went through it against the current code and the premises hold up: scrypt hashing, the fail-closed session secret, rate limiting, JWT sessions, and the 1. "Flip everyone on upgrade" (D2) is the riskiest call. The install base is large and heavily headless — Docker on a LAN, Node-RED/scripts hitting REST + Socket.IO without a token. Flipping auth on in an ordinary upgrade 401s all of them at once, and auto-updaters (Watchtower, add-on auto-update) mean most users won't see the changelog first. The forced-password-change mitigation only helps installs that already had auth on with default creds — the minority. Worth considering: tie the flip to a major version bump, secure new installs (empty 2. The proxy-IP gate (D4) is where a mistake is catastrophic, and it needs to be pinned down. The gate must compare the raw TCP peer ( 3. The ingress path prefix (Phase 2) is bigger than it looks. 4. Consider splitting D5 from D6. Generic hashed/revocable API tokens (D5) are useful for every programmatic client (Node-RED, custom dashboards), so they could ship independently and early. The host-mints-and-injects provisioning (D6) couples three repos and can stall on HA-core coordination — no need to make generic tokens wait on it. 5. §10 undercuts the "secure by default" headline. The Cheap hardening to fold in while in the area: One detail for D6/D7: a credential reset must also invalidate API tokens (or orphaned valid tokens survive the reset), and ZUI should register the injected host token in an idempotent "host token" slot rather than appending a new hash on every restart. |
|
Additional info when running as a HA addon:
We need to be able to specify this when starting Z-UI. |
|
RFC updated with all feedback |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
RFC: Authentication & onboarding for Z-Wave JS UI
Status: Draft for discussion
Audience: Z-Wave JS UI maintainers + HA add-on/integration owners
1. Summary
Z-Wave JS UI ships with authentication disabled by default and, when enabled, a hardcoded
admin/zwavecredential that is never forced to change. This RFC makes ZUIsecure-by-default on every install method and removes shipped default credentials — while
keeping access frictionless when ZUI runs embedded behind a host that already authenticates its
users (most notably the Home Assistant add-on behind ingress, where ZUI simply defers auth to the
ingress proxy). It also adds long-lived API tokens for programmatic clients (e.g. a future HA
integration that consumes ZUI's API).
2. Background & problem
The default authentication posture is the single most important structural security issue in ZUI
today, because it turns operations that would otherwise require a login into unauthenticated,
network-reachable ones. Two halves:
and real-time socket to any anonymous client on the network.
admin/zwave,with no forced change — and the UI even advertises these defaults.
A guiding principle: addressing only the default-off posture without eliminating the default
credentials would just replace "no auth" with "admin/zwave" — both must be solved together.
The building blocks already exist — scrypt password hashing, JWT sessions, a fail-closed session
secret, rate limiting, and reverse-proxy support — so this is mostly a defaults-and-onboarding
problem plus a few targeted crypto-hardening fixes (§7), not a build-from-scratch effort.
3. Goals / non-goals
Goals
defer that authentication to the host.
second login on top of the host's.
out.
Non-goals
@zwave-js/serverWebSocket (the socket the HA integration usestoday) — see §8, this needs upstream work.
4. Deployment contexts
Any solution must work across all four, which pull in different directions:
0.0.0.0exposed)Home Assistant is the primary concrete instance of the "embedded" context:
ingress: true). HA authenticates the user and proxiesevery request from the fixed internal IP
172.30.32.2. Per HA's ingress contract theseconnections are pre-authenticated, so the add-on must not require its own auth for them.
X-Ingress-Path— ZUI must honor it so assets resolveunder the sub-path. HA also forwards the user identity via
X-Remote-User-Id/-Name/-Display-Name, but ZUI needs these only for display/audit, never for the auth decision.5. Design decisions
D1 — Default authentication posture
Authentication is always on. The existing
gateway.authEnabledsetting is removed — thereis no in-app toggle. The only way to disable auth is an explicit env var,
DANGEROUSLY_DISABLE_AUTH=1, logged loudly on every startup, for local development ordeliberately-trusted networks.
setting; still flexible for users behind their own authenticating proxy.
D2 — Migration for existing installs
Flip everyone on upgrade. Auth becomes enabled for all installs; any install still on the
shipped default credential is forced to set a real admin password at next login. Users who
deliberately ran without auth must set
DANGEROUSLY_DISABLE_AUTH=1(D1).out (see §6).
D3 — First-run credential on a fresh standalone install
Forced setup wizard. On first run with no users, ZUI serves a guarded "setup required" state
that forces creating the admin account before any protected route is reachable. Env-provided
credentials (
DEFAULT_USERNAME+DEFAULT_PASSWORD) remain an opt-in headless bootstrap that skipsthe wizard. No
admin/zwavecredential ever ships.D4 — Authentication when embedded behind an authenticating host (e.g. HA add-on)
When a request reaches ZUI from a configured trusted reverse proxy, ZUI skips its own
authentication for that request — the proxy (HA's ingress) has already authenticated the user.
This is controlled by a new env var,
AUTH_TRUSTED_PROXY(a sibling ofTRUST_PROXY), set tothe proxy's address —
172.30.32.2for HA ingress; it accepts one or more IPs/CIDRs and applies toboth the HTTP API and the Socket.IO connection. The HA add-on sets it automatically; standalone
installs leave it unset. Such requests are treated as fully authorized (ZUI is single-user), so no
token, shared "system identity", or trust of
X-Remote-User-*is needed for the auth decision; theuser's own admin credential is untouched and stays fully user-managed.
The match is keyed on the connection's real peer IP, never a forwarded header (see §7). Other
source IPs are not blocked — they simply get normal authentication. So direct access to ZUI's web port (
8091, if the user publishesit) still requires a login or API token; the HA add-on leaves that port unmapped by default
(
ports: { 8091/tcp: null }, while ingress still reaches the container port internally), so out ofthe box the only route in is the pre-authenticated ingress path.
to implement (a peer-IP check on both transports); the user's own credential is untouched.
can reach the port it fronts (see §7); standalone reverse-proxy setups must firewall the app port
accordingly.
D5 — Programmatic API access
Add long-lived, named, revocable API tokens — CSPRNG-generated, hash-only storage, shown once —
for programmatic clients (scripts, external integrations, a future HA integration consuming ZUI's
API). A token authenticates on both transports: the
Authorization: Bearerheader (orx-access-token) for the REST API, and the Socket.IO handshakeauthfield (browsers can't setheaders on a WebSocket). Both auth paths must hash-match the opaque token, not only verify session
JWTs as they do today.
1-day expiry).
D6 — Credential recovery (standalone)
Reset the store. Standalone installs recover a lost admin password by deleting the users file,
which drops back to the first-run setup wizard (it must not re-seed any default credential). The
reset must also invalidate all API tokens — a credential reset that left existing tokens valid
would let a leaked token outlive the credential it was bound to. Embedded deployments need no
recovery path: the host authenticates every request, so ZUI never depends on a stored password
there.
surface).
users.
6. Release & migration
ZUI ships all of the above in a single major release — no phased rollout. Because auth becomes
mandatory (D1/D2), downstream applications must be updated before they can run on the new major:
AUTH_TRUSTED_PROXY=172.30.32.2(D4).(D5) or set credentials.
This is a breaking change — flag it prominently in the changelog. Ship the add-on update
together with the release so HA users get the frictionless ingress path the moment auth becomes
mandatory, avoiding a window of double-login (or broken-access) pain.
7. Security considerations
AUTH_TRUSTED_PROXYcheck (D4) must match thereal socket peer IP, never
X-Forwarded-Foror any client-supplied header — those arespoofable. The trusted proxy must also be the only thing that can reach the port it fronts (HA
ingress guarantees this; standalone reverse-proxy setups must firewall the app port).
Socket.IO connection must refuse access.
verifyPswmust compare derived keys with a timing-safecomparison (
crypto.timingSafeEqual), not===.verifyJWTmust pass an explicitalgorithmsallow-list tojwt.verify(on both the REST and Socket.IO paths), so a token can't be presented under an unexpected
algorithm.
revocable; a credential reset invalidates all tokens (D6).
8. Out of scope
@zwave-js/serverWebSocket (port 3000, the socket the HA integration connectsto today) is unauthenticated and binds all interfaces by default. Adding auth there needs an
upstream
@zwave-js/serverchange plus HA-core support, and is distinct from the web UI / REST APIthis RFC secures. Recommend documenting network isolation (bind it to loopback when co-located)
and tracking auth upstream separately.
All reactions