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

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`.