fix(security): two gate bugs that would have made the rollout a no-op
Both found while setting up the on-node test, and both fail silently in the same direction — the gate reports success while protecting nothing, which is the exact failure the module was written to prevent. 1. Loopback-pinned ports were skipped entirely. `identity.rs` dropped any port whose manifest sets `bind: 127.0.0.1`, reasoning that a loopback publish is not externally reachable. But `listener.rs` requires loopback-pinning as the PRECONDITION for gating — while an app holds 0.0.0.0:<port> the kernel will not let the gate bind that port at all. So the two contradicted each other: pinning an app, the one action that lets the gate take over, was also what removed it from the gated set. Completing the entire migration would have gated nothing, and GateStatus would have reported zero unprotected ports while doing it. `bind` cannot carry this decision, because two unrelated intentions produce an identical loopback publish: Bitcoin's RPC 8332 is pinned so the LAN CANNOT reach it (fronting it would newly expose it on every host address, behind a login but exposed where it deliberately was not), whereas a migrated app is pinned precisely so the gate CAN. Inferring from `bind` breaks one or the other, so the intent is now declared: `PortAuth::Local` means the first case. The three ports that are host-local by intent (bitcoin-core/knots 8332, aiui 5180 — all already `bind: 127.0.0.1`) say so, and a loopback publish with `auth: session` stays gated. A test pins that property. 2. The port map was never refreshed. `AppGate::refresh()` existed, was documented as making catalog changes apply without a restart, and was called by nothing. The map was built once in `new()`, so an app installed while the daemon runs would never be gated — and would never appear in `unprotected` either, so the node would report itself fully enforced while serving a brand-new app to anyone who asked. The sweep now refreshes before classifying. Tests: 22/22 appgate, 73/73 archipelago-container. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cc9e19589c
commit
3716b6e9c3
@@ -530,6 +530,23 @@ pub enum PortAuth {
|
||||
/// (Bitcoin p2p gossip, mDNS). Requires `auth_rationale`: an exemption
|
||||
/// nobody can explain is an exemption nobody reviewed.
|
||||
None,
|
||||
/// Host-local by intent — the gate must not bind this port at all.
|
||||
///
|
||||
/// This exists because `bind: 127.0.0.1` is ambiguous on its own, and
|
||||
/// reading intent out of it would be wrong in both directions. Two
|
||||
/// unrelated situations produce an identical loopback publish:
|
||||
///
|
||||
/// * Bitcoin's RPC 8332 is loopback-pinned so that the LAN *cannot*
|
||||
/// reach it. Fronting it with the gate would newly expose it on every
|
||||
/// host address — behind a login, but exposed where it deliberately
|
||||
/// was not.
|
||||
/// * A gated app is loopback-pinned precisely *so that* the gate can
|
||||
/// take over its external addresses; that is the whole migration.
|
||||
///
|
||||
/// Inferring from `bind` would break one or the other, so the intent is
|
||||
/// declared. `Local` means the first case: never externally reachable,
|
||||
/// gate keeps its hands off.
|
||||
Local,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
Reference in New Issue
Block a user