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>
107 lines
5.8 KiB
Markdown
107 lines
5.8 KiB
Markdown
# 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 spec** — `apps/<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`.
|