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>
This commit is contained in:
archipelago
2026-08-06 14:24:51 -04:00
co-authored by Claude Opus 5
parent dfe027a5f3
commit ab69400956
3 changed files with 224 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
# 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`.
@@ -0,0 +1,113 @@
# Resume — 2026-08-05 (app gate, releases .122.125)
Paste the block at the bottom into a new session.
## Where things stand
- **v1.7.124-alpha is SHIPPED** (signed with the NEW root, published, verified).
- **Signed catalog is LIVE** carrying two hotfixes made after .124:
the repaired bitcoin start script and the fedimint 8175 removal.
Last commit: `4ace62fa`.
- **Release-root rotation is COMPLETE.** .122 was the last release signed with
the old key; .123/.124 and all catalogs use the new one. No override needed.
## Two bugs I introduced in .124 (both fixed, both instructive)
1. **Bitcoin vanished from every node.** I put a `#` comment INSIDE the
manifest's folded YAML scalar (`>-`), where `#` is not a comment — it
reaches the shell, and folding joins lines with spaces so it commented out
the `if ... then` while the more-indented `echo` survived, leaving an orphan
`fi`. Container exited instantly; app detection is container-based so the
app disappeared. **Guard added:** `scripts/check-manifest-shell.py` runs
`sh -n` over every embedded manifest script and rejects `#` in these
scalars; wired into `tests/release/run.sh`.
2. **Fedimint crash-looped.** I declared port 8175 on the `fedimint` app so the
gate could name it — but 8175 is served by the separate `archy-fedimint-ui`
companion. The orchestrator then tried to publish 8175 from fedimintd,
collided, and `start_container` failed forever. Removed. **Rule: never
declare a port on an app whose container does not actually serve it.**
Also: I published an UNSIGNED catalog at one point, which nodes correctly
reject — they silently keep their old cached copy. **Always verify
`'signature' in catalog` on the live URL after publishing.**
## OPEN TASKS
1. **indeedhub crash-loop — NOT mine, needs a real fix.** `indeedhub-minio` is
**absent** on `.38` and `.88`, so nginx fails with
`host not found in upstream "minio"` and both `indeedhub` and
`indeedhub-api` exit(1). The stack member never gets created. Look at
`api/rpc/package/stacks.rs` + `dependencies.rs`.
2. **Verify `.38` refetched the signed catalog** and bitcoin-knots starts.
`.88` already did (signed: True, script fixed).
3. **Deploy the .125 build to archi-dev-box for operator confirmation.**
Binary is built at `core/target/release/archipelago` with: app-login page
using the sidebar **A mark** (`favico-black-v2.svg`) not the wordmark;
page pinned to `100svh` + `position:fixed` so mobile stays centred and the
keyboard overlays instead of scrolling; install-version modal icon uses
`object-contain` so non-square icons are not cropped. **Operator has not
seen these yet.**
4. **Cut v1.7.125-alpha** once confirmed. Sign with the **NEW** mnemonic.
## Traps that cost time today
- `create-release.sh` says "sign, then re-run" — **re-running regenerates the
manifest and DESTROYS the signature**, and its clean-tree check blocks
anyway. Do steps 7/8 by hand: `git add` version+changelog+manifest →
commit `chore: release vX``git tag -a vX` → push main → **push the tag
explicitly** → `git ls-remote --tags` to prove it → `publish-release-assets.sh`.
- The release gate's `cargo-test-weekly` times out on the **compile** after any
version bump. Pre-warm: `CARGO_INCREMENTAL=0 cargo test --manifest-path
core/Cargo.toml -p archipelago --no-run`.
- The frontend version check fails until the in-app **What's New** block for
that version exists (`neode-ui/src/views/settings/AccountInfoSection.vue`) —
that string is what it greps for.
- `generate-app-catalog.py` writes `APP_LAUNCH_PORTS` one-per-line; rustfmt
packs it, so run `cargo fmt` after any catalog sync or the gate fails.
- **Manifest changes reach nodes via the SIGNED CATALOG, not the binary.** A
manifest hotfix needs only a catalog re-sign — no release.
## Fleet
SSH: `sshpass -p 'ThisIsWeb54321!' ssh archipelago@<ip>` (note the `!`; `@`
is older and still works on some). RPC/node password differs per node — the
`!` one failed RPC login on `.38`.
- `100.69.68.39` archi-dev-box — dev target
- `100.82.34.38` archipelago-1
- `100.70.96.88` austin-sapien
- `100.64.204.114` .228 shorty-s — **in real use, treat carefully**
**Force a catalog refresh on a node:** Settings → App Updates → Check for
updates, or `sudo rm -f /var/lib/archipelago/app-catalog.json && sudo
systemctl restart archipelago`.
**All fleet nodes were repaired** from `Restart=on-failure`
`Restart=always`; a node with the old value stays DEAD after an in-process
update (the updater exits cleanly and systemd reads that as success).
`bootstrap::ensure_restart_policy()` now self-heals it.
---
## PASTE THIS INTO THE NEW SESSION
Resume the archy work from 2026-08-05. Read
`.planning/RESUME-2026-08-05-appgate-fixes.md` and the memory notes
`project_fleet_ota_restart_policy_incident` and
`project_v1_7_121_shipped_appgate` first.
v1.7.124-alpha is shipped and the signed catalog is live with two hotfixes
(bitcoin start script, fedimint 8175). Four things are open, in order:
1. Fix the indeedhub crash-loop: `indeedhub-minio` is absent on .38 and .88 so
nginx fails on upstream "minio" and indeedhub + indeedhub-api exit(1). This
one is pre-existing, not from the port work.
2. Verify .38 refetched the signed catalog and bitcoin-knots starts (.88
already did).
3. Deploy the built .125 binary + frontend to archi-dev-box (100.69.68.39) so
I can confirm the app-login page (A mark, mobile centring, keyboard
behaviour) and the install-modal icon.
4. Then cut v1.7.125-alpha — I sign with the new mnemonic.
Do not re-run create-release.sh after signing; it destroys the signature —
do the commit/tag/publish steps by hand as the resume doc describes.
+5
View File
@@ -0,0 +1,5 @@
{
"workflow": {
"_auto_chain_active": false
}
}