feat(appgate): apps with their own login can skip the node login
Demo images / Build & push demo images (push) Successful in 3m33s

Some apps carry a complete account system and are broken by an upstream
challenge: git clients speak basic-auth (not browser cookies), and a
BTCPay checkout link handed to a customer must open for that customer.
Both were behind the gate's login page — the "non-browser clients need an
access token" gap disclosed in five consecutive releases.

- New manifest port policy `auth: open`: the daemon still fronts the port
  exactly like `gated` (loopback pin, external binds, frame-header fixes,
  app-down retry page, Tor upstream) but serves it without the login
  challenge. Requires auth_rationale, same burden of proof as `none`.
  Gitea 3001 and BTCPay 23000 declare it.
- Runtime operator override per app (security.set-app-gate → app-configs/
  <id>.json "gateEnabled"), surfaced as Settings → app → Access control.
  Wins over the manifest in both directions and applies on the next
  request — no restart, and it works today on catalog-covered apps whose
  signed manifest still says `gated`.
- The gate resolves policy per-request from the live port map, so a
  toggle takes effect without waiting for the 60s rebind sweep. "Off"
  never releases the port: gated apps are loopback-pinned, so releasing
  would strand them, not open them.
- security.app-gate-status now reports gate_enabled + any override.
- New guard test pins the `auth: open` set (both entries reviewed); the
  `auth: none` count moves 25 → 26, absorbing pre-existing drift from the
  phoenixd onboarding (loopback JSON API with its own generated password).
- Docs: the manifest spec's ports row documented only host/container/
  protocol — bind, auth, auth_rationale and session_passthrough were
  undocumented. Added a full "Ports & the app gate" section plus a
  developer-guide entry telling app authors to enforce their own auth
  regardless, since the operator can flip the gate either way.

Verified live on archi-dev-box from an external address: gated → 401 gate
page; override off → Gitea 200 own page, BTCPay 302 to its own login,
git-over-HTTP info/refs 200; override on → 401 again; clear → default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-16 11:40:07 -04:00
co-authored by Claude Fable 5
parent 9b789a64ad
commit 58cdea5e79
15 changed files with 537 additions and 7 deletions
+52 -1
View File
@@ -29,7 +29,7 @@ reusable manifest primitive.
| `dependencies` | list | — | `- storage: "10GB"`, `- { app_id: bitcoin, version: … }`, or a bare string. |
| `resources` | ResourceLimits | — | `cpu_limit` (int), `memory_limit` (e.g. `"512m"`), `disk_limit`. |
| `security` | SecurityPolicy | — | See [Security](#security). |
| `ports` | list of PortMapping | — | `- { host: 8080, container: 80, protocol: tcp }`. |
| `ports` | list of PortMapping | — | See [Ports & the app gate](#ports--the-app-gate). |
| `volumes` | list of Volume | — | See [Volumes](#volumes). |
| `files` | list of GeneratedFile | — | Config files written before create: `{ path, content, overwrite }`. `path` must sit under a declared bind mount. |
| `environment` | list of string | — | `- KEY=value` pairs (static). |
@@ -82,6 +82,57 @@ Validation (enforced at `AppManifest::validate()`):
`secret_env`/`generated_secrets` names must be bare filenames.
- Hook steps are validated against the hook allow-list (below).
## Ports & the app gate
```yaml
ports:
- host: 3001 # host port your app is reachable on
container: 3000
protocol: tcp # default tcp
bind: 127.0.0.1 # empty = all interfaces
auth: gated # session | none | local | gated | open
auth_rationale: "…" # REQUIRED for auth: none and auth: open
session_passthrough: false
```
| Field | Notes |
|-------|-------|
| `bind` | Host address for the publish. Empty = all interfaces. List the same host/container pair twice with different binds to serve several addresses. |
| `auth` | The port's authentication policy — see below. **Absent means "no instruction"**: the daemon reports on the port but never changes how it is published. |
| `auth_rationale` | Why the port is safe without the gate's login. Required for `none` and `open`, rejected elsewhere. |
| `session_passthrough` | Forward the node session cookie to the app on authorised requests. First-party companion UIs only; the gate otherwise strips its own credential. Only meaningful on `auth: gated`. |
### `auth` policies
- **`session`** (default when declared): the app gate authenticates every
connection — node session cookie or an app-scoped bearer token.
- **`gated`**: the app publishes on loopback **only** (`bind: 127.0.0.1`) and
the daemon owns the external addresses: it binds them, authenticates every
connection, fixes frame-blocking headers so the app embeds in the
dashboard, serves a retrying page while the app is down, and fronts the
Tor onion for the port. This is the migrated end state for most apps.
- **`open`**: same daemon takeover as `gated` — loopback pin, external
binds, header fixes, retry page, Tor — but **no dashboard login
challenge**. For apps that carry a complete login of their own and are
broken by an upstream challenge: Gitea (git clients speak basic-auth, not
cookies), BTCPay (checkout pages must be reachable by anonymous payers).
Requires `auth_rationale`.
- **`none`**: the gate does not touch the port at all. Only for protocols
that authenticate themselves (LND macaroons, TLS client certs) or where a
login page is meaningless (p2p gossip). Requires `auth_rationale`.
- **`local`**: host-local by intent — the gate must never bind or expose
this port anywhere (e.g. Bitcoin RPC).
### Runtime override
The manifest sets the **default**. The node operator can flip any
gate-fronted app between `gated` and `open` behaviour at runtime from
**Settings → app → Access control** (RPC `security.set-app-gate`), stored
per-app on the node (`app-configs/<id>.json`, key `gateEnabled`). The
override wins over the manifest in both directions and applies on the next
request — your app cannot assume the gate is or isn't in front of it, so it
must always enforce its own authorization for sensitive operations.
## Volumes
```yaml