Files
archy/.planning/APP-PORT-AUTH-GATE.md
T
archipelagoandClaude Opus 5 ab69400956 docs: commit the app-gate design + 2026-08-05 resume notes
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>
2026-08-06 14:24:51 -04:00

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:P only (PublishPort=127.0.0.1:P:<container>), so it is no longer reachable from any interface.
  • The gate binds P on every external address — LAN IP, Tailscale IP, fips0 ULA — and on 127.0.0.2:P for Tor. 127.0.0.2 is a distinct loopback address, so it does not collide with the app on 127.0.0.1:P, and it means no app needs a second port number. torrc changes to HiddenServicePort 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

  1. Read the session cookie. Cookies are host-scoped and port-agnostic, so the session minted on the dashboard is presented to <host>:P automatically — this is the same mechanism umbrel's "proxy token" relies on. (Scheme still matters: a Secure cookie will not travel to a plain-HTTP app port. See open questions.)
  2. Valid session → proxy to 127.0.0.1:P, passing through Upgrade so WebSockets work.
  3. 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>:P also logs you into the dashboard, exactly as umbrel behaves.
  4. Non-browser clients get 401 with 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 specapps/<id>/manifest.yml, scripts/container-specs.sh, scripts/first-boot-containers.sh. Changing PublishPort in 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 PublishPort drifts 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_loop in server.rs already 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

  1. 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?
  2. TLS/scheme. The daemon serves plain HTTP with nginx terminating TLS in front. If the dashboard is HTTPS and app ports are HTTP, a Secure session 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

  1. Gate module + login page + proxy, behind an env opt-in.
  2. Prove on one HTTP app on .228, across all four transports.
  3. Dedupe the container-spec declarations.
  4. Roll to all HTTP apps; declare the non-HTTP exceptions.
  5. Repoint torrc at 127.0.0.2.