feat(webui): signed-in read-only Console preview behind the edge proxy (#1568, row 74)
serve takes --public-host NAME --auth-header HEADER --allow-user ID, all three or none. With them, a request whose Host is NAME must carry HEADER equal to ID or gets 403; a request that carries HEADER with any other Host gets 403, so a proxy that rewrites Host can't reach the loopback path with a signed-in request; every POST is refused read-only. A repeated Host gets 400 in both modes. Host and header names compare case-insensitively (Traefik sends X-Authentik-Uid). The README states the proxy contract: the edge forwards Host unchanged and strips client copies of HEADER. Author: Dewey. Round 2 candidate manifest 17b6d7f62d106a23d99441eefa46e34ab2e9bd1442a1d02e219f126a82a8668d (base17ea0dbc, no drift on the 4 paths through60c8dcf9). Approved by Filbert (comment 27485) and Darkwing (comment 27486) on #1568. Their wording notes on the README (first signed-in request; the tunnel sentence) go to row 76, which owns the README next. Sage gate rerun on a worktree ofa55915f0with the patch: manifest OK; webui 61/61, control-board 124/124, cli 113/113; test-auth 15, conductor 17, config 24, extension-package 18, foundation 44, queue 27, release 14, task 98, all 0 failed. test-discord failed one timing test (engine: a timed-out run outlives the grace, 280 ms at load 30) on the sequential run and passed 66/66 on a solo rerun of the same worktree; the patch touches no discord file. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -16,12 +16,78 @@ Open http://127.0.0.1:7330/. Optional `--port N` and
|
||||
`--business ID` picks the business the bus views read; without it they read
|
||||
the running bus host's business (`mosaic bus start <business>`).
|
||||
Port 0 picks a free port. Ctrl-C stops each foreground server. No daemon,
|
||||
installation, account, authentication or deployment is added.
|
||||
installation, account, login or deployment is added. The preview mode below
|
||||
checks one header a sign-in proxy sets; it signs nobody in itself.
|
||||
|
||||
Only HTTP loopback board origins are accepted. The WebUI binds to IPv4 loopback
|
||||
by default, rejects nonlocal Host and cross-origin requests, sends no CORS
|
||||
by default, rejects a repeated Host (400), nonlocal Host and cross-origin
|
||||
requests, sends no CORS
|
||||
headers, refuses redirects and accepts only JSON object POST bodies up to 4096
|
||||
bytes. Do not expose either unauthenticated server through a public proxy.
|
||||
bytes. Do not put the board behind a public proxy. Put the Console behind one
|
||||
only in the preview mode below, with a proxy that meets its contract.
|
||||
|
||||
## Preview behind a sign-in proxy (row 74, #1568)
|
||||
|
||||
```sh
|
||||
node packages/webui/src/cli.mjs serve --port 7332 \
|
||||
--public-host NAME --auth-header HEADER --allow-user ID
|
||||
```
|
||||
|
||||
The three flags go together; one or two refuse at start, before any config
|
||||
is read. NAME is a DNS name, no scheme or port. HEADER is a header name other
|
||||
than Host or Origin; Node lowercases incoming header names, so it matches
|
||||
without regard to case (`X-Example-Uid` on the flag finds `x-example-uid`).
|
||||
ID is the one user id, 1 to 256 visible ASCII characters without a comma,
|
||||
and the value compare is exact. The Console still binds to loopback only. It
|
||||
names no identity provider and has no login code: the proxy signs the user
|
||||
in and limits sign-in to the right group.
|
||||
|
||||
In preview mode:
|
||||
|
||||
- A request whose Host is exactly NAME must carry HEADER once, with a value
|
||||
equal to ID. Missing, different or repeated answers 401 `sign-in required`
|
||||
before any route runs, so no bus read or board call is made. An Origin,
|
||||
when sent, must be `https://NAME`.
|
||||
- A request whose Host is loopback on the Console's port (`127.x`,
|
||||
`localhost` or `[::1]`) is checked as before and needs no HEADER, so the
|
||||
operator's browser on the host still works. Any other Host is 403.
|
||||
- A request that carries HEADER with any Host but NAME answers 403 `proxy
|
||||
Host refused`. The operator's browser never sends HEADER; a proxy that
|
||||
rewrote Host does, and it fails on its first request instead of reaching
|
||||
the loopback path unchecked.
|
||||
- More than one Host header answers 400 before any other check, in this
|
||||
mode or not. Node keeps the first, so a loopback Host followed by
|
||||
`Host: NAME` would otherwise skip the sign-in check.
|
||||
- Every method other than GET and HEAD answers 403 `read-only`, whatever the
|
||||
Host. `/api/seen` and `/api/reply` never reach the board, so Seen and Reply
|
||||
fail in the page with that refusal.
|
||||
- The GET routes are unchanged: the bus views, the Queue, Business and
|
||||
Settings reads, the board panel and the assets. If the board is down, its
|
||||
panel shows the existing error and the bus views still answer.
|
||||
|
||||
What the proxy must do. The Console trusts HEADER only because the proxy
|
||||
controls it:
|
||||
|
||||
- It terminates TLS for NAME and is the only thing that reaches the port
|
||||
from off the host.
|
||||
- It strips HEADER from every client request and sets it only after the
|
||||
identity provider signs the user in.
|
||||
- It forwards only requests whose Host is NAME, and forwards that Host
|
||||
unchanged. nginx rewrites Host to the upstream by default; set
|
||||
`proxy_set_header Host $host;` (or the literal NAME). Caddy and Traefik
|
||||
keep the client's Host by default.
|
||||
- Any loopback Host on the Console's port, `localhost` included, is the
|
||||
operator's path and skips the header check, so a proxy that forwarded a
|
||||
client's `Host: 127.0.0.1:7332` would let anyone through without signing
|
||||
in. A tunnel whose local end uses the same port number sends such a Host
|
||||
and reaches the Console unchecked.
|
||||
- Every hop on this host that reaches the loopback port is bound by this
|
||||
contract too, not only the edge.
|
||||
|
||||
The local exposure doesn't change ("Data and boundaries"): any process on
|
||||
the host can still connect to the port and send `Host: NAME` with HEADER and
|
||||
ID, or just a loopback Host, and read what a reader capability reads. The
|
||||
user id is not a secret; the sign-in at the edge is what keeps others out.
|
||||
|
||||
## Use
|
||||
|
||||
|
||||
Reference in New Issue
Block a user