From 6c0fc366b31616cde6cf0b519f12dd3cef5dd63c Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 6 Aug 2026 09:17:10 -0400 Subject: [PATCH] fix(appgate): classify ports from the catalog even for on-node-built apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The port map deferred to DISK manifests for any app with a build source — which is exactly the four companion UIs (lnd-ui, bitcoin-ui, electrs-ui, fips-ui). Their disk manifests reach nodes only via the frontend runtime payload or a per-node repo checkout, and in the v1.7.125 rollout both proved stale or entirely absent: one node had no checkout at all, others restored an older payload over apps/ at every boot. Result: session_passthrough never reached the gate, so the node's own screens 401'd on every data call, and on nodes whose UI rebuilt from a stale context the app held its port UNGATED. Classification now uses a ports-only overlay that accepts build-source manifests (install/orchestration still defers to disk — unchanged). The signed catalog is the freshest, operator-signed source, and the gate's address binds fail safely against a container publishing differently (logged CANNOT PROTECT), so this can only tighten policy, never expose. Co-Authored-By: Claude Fable 5 --- core/archipelago/src/appgate/identity.rs | 13 ++++++--- core/archipelago/src/container/app_catalog.rs | 27 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/core/archipelago/src/appgate/identity.rs b/core/archipelago/src/appgate/identity.rs index 8bfc0142..240e160d 100644 --- a/core/archipelago/src/appgate/identity.rs +++ b/core/archipelago/src/appgate/identity.rs @@ -147,11 +147,18 @@ pub fn build_port_map() -> PortMap { let mut seen_apps: std::collections::HashSet = std::collections::HashSet::new(); for (app_id, value) in crate::container::app_catalog::catalog_manifest_values() { + // Ports-only overlay: unlike the install path, classification also + // accepts BUILD-SOURCE manifests. The on-node-built companion UIs + // are exactly the apps whose gate policy (session_passthrough, + // auth: gated) must arrive reliably, and their disk manifests + // proved stale or absent fleet-wide in the v1.7.125 rollout. The + // gate's binds fail safely on conflict with a differently-published + // container, so a fresher catalog can only tighten, never expose. let Some(manifest) = - crate::container::app_catalog::catalog_manifest_overlay(&app_id, value) + crate::container::app_catalog::catalog_manifest_ports_overlay(&app_id, value) else { - // Unparseable/invalid/build-source → the orchestrator falls back - // to disk for this app, so classification must too. + // Unparseable/invalid → the orchestrator falls back to disk for + // this app, so classification must too. continue; }; if seen_apps.insert(app_id) { diff --git a/core/archipelago/src/container/app_catalog.rs b/core/archipelago/src/container/app_catalog.rs index 63b46b30..94c02e93 100644 --- a/core/archipelago/src/container/app_catalog.rs +++ b/core/archipelago/src/container/app_catalog.rs @@ -256,6 +256,33 @@ pub fn catalog_manifest_overlay( Some(m) } +/// Like [`catalog_manifest_overlay`] but WITHOUT the build-source refusal — +/// for PORT CLASSIFICATION only, never for install/orchestration. +/// +/// The on-node-built companion UIs (lnd-ui, bitcoin-ui, electrs-ui, fips-ui) +/// are exactly the apps whose port policy (auth/bind/session_passthrough) +/// must reach the gate reliably, yet their build sources made the overlay +/// defer to DISK manifests — whose only delivery paths (frontend runtime +/// payload, per-node repo copies) proved stale or absent across the fleet in +/// the v1.7.125 rollout: nodes served ungated UIs or 401-dead panels until +/// hand-fixed. The signed catalog is fresher and operator-signed; and the +/// gate's address binds fail safely on conflict with a container that +/// publishes differently (logged as CANNOT PROTECT), so classifying from the +/// catalog cannot open anything the running container hasn't already opened. +pub fn catalog_manifest_ports_overlay( + app_id: &str, + value: serde_json::Value, +) -> Option { + let m: archipelago_container::manifest::AppManifest = serde_json::from_value(value).ok()?; + if m.app.id != app_id { + return None; + } + if m.validate().is_err() { + return None; + } + Some(m) +} + /// The catalog's default/latest version string for an app (the top-level /// `version` field), if covered. Used to decide whether an install-time /// selection should pin (older) or track-latest (default).