fix(security): gate classifies from the catalog overlay and releases withdrawn claims

Dev-box verification of the Tor/FIPS fixes caught a pre-existing split
brain: the orchestrator publishes containers from the signed catalog's
embedded manifests (origin-wins), but the gate classified ports from the
stale disk manifests — so it externally bound nbxplorer 32838, a port
the catalog declares auth: local and pins to loopback. Reachable behind
a login, but reachable where it deliberately was not.

- build_port_map now consults the catalog overlay first, via the same
  parse/validate/image-only filter the orchestrator uses (moved to
  app_catalog::catalog_manifest_overlay so the two cannot diverge again).
- GatedPort carries . The gated set still includes undeclared
  Session-default ports for challenge/audit, but every action that
  REDIRECTS traffic — the torrc 127.0.0.2 repoint, the FIPS relay
  stand-down, the Tor-upstream bind — now keys on the declaration.
- The sweep releases held claims whose port left the gated set, so a
  catalog refresh that withdraws a port (gated → local/none) takes
  effect without a daemon restart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-04 08:50:46 -04:00
co-authored by Claude Fable 5
parent e46af8cfe5
commit d2e4b00789
7 changed files with 293 additions and 144 deletions
+1
View File
@@ -1162,6 +1162,7 @@ async fn app_port_v6_relay_loop(mut shutdown_rx: tokio::sync::watch::Receiver<bo
// releasing the bind so the gate's next sweep claims it.
let gated: std::collections::HashSet<u16> = crate::appgate::identity::build_port_map()
.gated_ports()
.filter(|g| g.declared)
.map(|g| g.port)
.collect();
for &port in crate::fips::app_ports::APP_LAUNCH_PORTS {