fix(appgate): classify ports from the catalog even for on-node-built apps

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 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-06 09:17:10 -04:00
co-authored by Claude Fable 5
parent 83d7234824
commit 6c0fc366b3
2 changed files with 37 additions and 3 deletions
+10 -3
View File
@@ -147,11 +147,18 @@ pub fn build_port_map() -> PortMap {
let mut seen_apps: std::collections::HashSet<String> = 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) {
@@ -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<archipelago_container::manifest::AppManifest> {
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).