Both had been sitting untracked in the working tree since 2026-08-05 — exactly the "finished work lost because it was never committed" failure CLAUDE.md's #1 process rule exists to prevent. APP-PORT-AUTH-GATE.md carries the gate's design rationale ("you cannot gate a socket you do not own") and, in its open questions, the TLS/scheme fork that still blocks the gated-app iframe login: if the dashboard is HTTPS and app ports are HTTP, a Secure session cookie is never sent. RESUME-2026-08-05-appgate-fixes.md carries the .122-.125 release trail, the two self-inflicted .124 bugs and their guards, and the open indeedhub crash-loop (indeedhub-minio absent on .38/.88). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.8 KiB
App-port authentication gate — design
Item 1 of RELEASE-1.7.121-TASKS.md. Opened 2026-08-04.
"if I'm logged out I can reach every app port on tailscale and LAN, this can not be allowed… it must present the login to access the app with an app icon of what you're accessing to confirm, and 2FA if present" — operator, 2026-08-03
"make sure we fix FIPS, Tor, everything … umbrel definitely shows a port when you go to tailscale IP or other + port but demands the node login and 2FA if activated" — operator, 2026-08-04
What we already built, and why it did not close this
The operator's recollection that FIPS and Tor were "done" is correct — but that work was about reachability, and about restricting the daemon's own API. Neither one ever authenticated an app port. Read together, each transport got a door and none got a lock:
| Layer | What exists today | What it protects |
|---|---|---|
server.rs:1271 is_peer_allowed_path |
Federated peers hitting the daemon port may only reach /health, /rpc/v1, /content, /blob/, /dwn/, /transport/inbox, /archipelago/* |
The daemon's API surface. Not app ports. |
fips/app_ports.rs APP_LAUNCH_PORTS |
35 app ports allowed through the fips0 firewall | Nothing — it opens them |
server.rs:1130 app_port_v6_relay_loop |
Daemon relays mesh v6 → v4 loopback for those same ports | Nothing — it bridges them |
api/rpc/tor/mod.rs:243 |
Per-app HiddenServicePort 80 → 127.0.0.1:<app port> |
Nothing — it publishes them to an onion |
container/quadlet.rs:261 |
PublishPort=0.0.0.0:{host}:{container} |
Nothing — it binds every interface |
So the app ports are reachable, by construction, over LAN, Tailscale, FIPS mesh and Tor,
and nothing on any of those paths checks a session. This is the same bug class as the
v1.7.120 /lnd-connect-info + /bitcoin-rpc/ leaks, but structural rather than
per-endpoint.
The rule this design is built on
You cannot gate a socket you do not own. Every previous fix added a check beside the listener, which is why each one only covered the transport it was written for. The gate has to be the listener.
Design
Port numbers do not change. For an app whose UI port is P:
- The app binds
127.0.0.1:Ponly (PublishPort=127.0.0.1:P:<container>), so it is no longer reachable from any interface. - The gate binds
Pon every external address — LAN IP, Tailscale IP, fips0 ULA — and on127.0.0.2:Pfor Tor.127.0.0.2is a distinct loopback address, so it does not collide with the app on127.0.0.1:P, and it means no app needs a second port number.torrcchanges toHiddenServicePort 80 127.0.0.2:P. - Upstream for the gate is always
127.0.0.1:P.
Because the gate owns the socket, LAN / Tailscale / FIPS / Tor are one code path. There is no per-transport work, and therefore no transport to forget.
Request handling
- Read the
sessioncookie. Cookies are host-scoped and port-agnostic, so the session minted on the dashboard is presented to<host>:Pautomatically — this is the same mechanism umbrel's "proxy token" relies on. (Scheme still matters: aSecurecookie will not travel to a plain-HTTP app port. See open questions.) - Valid session → proxy to
127.0.0.1:P, passing throughUpgradeso WebSockets work. - No/invalid session → serve the login page on the app port itself, naming the app
and showing its icon, POSTing back to the same origin. The gate verifies the password,
enforces TOTP when enabled, and sets the session cookie — so logging in at
<tailscale-ip>:Palso logs you into the dashboard, exactly as umbrel behaves. - Non-browser clients get
401with a JSON body rather than an HTML page.
What must NOT be gated
Non-HTTP ports cannot carry a cookie and must be declared, not discovered:
electrum 50002, bitcoin p2p 8333, LND gRPC 10009/9735. These need an explicit
manifest field (auth: none + rationale) so the exception list is a grep, and they are
a firewall/allowlist question, tracked separately.
Note api/rpc/tor/mod.rs:238-240 already special-cases lnd's 9735/10009 as
is_protocol_service — that distinction is the seed of the manifest field.
Deploy traps this walks into
- Three copies of every container spec —
apps/<id>/manifest.yml,scripts/container-specs.sh,scripts/first-boot-containers.sh. ChangingPublishPortin one leaves fresh installs broken while the node looks fixed. This is exactly what bit lnd-ui (item 4). Deduplicating these is arguably a prerequisite, not a follow-up. - Changing
PublishPortdrifts every app → one-time recreate fleet-wide. - The gate must rebind when addresses change (Tailscale up/down, DHCP, fips0 re-key).
Precedent exists:
peer_late_bind_loopinserver.rsalready does this for fips0. - Verify on the node, not from source. v1.7.120's headline bug was a fix that shipped in the binary and never reached the running container.
Open questions for the operator
- Machine clients. Umbrel's real-world failure mode: Home Assistant (or any API client) hitting an app's API has no cookie and breaks. Browser-only, or do we mint per-app long-lived tokens?
- TLS/scheme. The daemon serves plain HTTP with nginx terminating TLS in front. If the
dashboard is HTTPS and app ports are HTTP, a
Securesession cookie will not be sent — the gate would prompt for login every time. Either the gate serves TLS on app ports too, or app ports are HTTP-only on such nodes.
Sequencing
- Gate module + login page + proxy, behind an env opt-in.
- Prove on one HTTP app on .228, across all four transports.
- Dedupe the container-spec declarations.
- Roll to all HTTP apps; declare the non-HTTP exceptions.
- Repoint
torrcat127.0.0.2.