feat(security): declare which app ports may skip authentication

Groundwork for the app gate (item 1): before anything can enforce
authentication on app ports, the node has to know which ports are
*supposed* to be reachable without it.

`PortMapping` grows `auth` (PortAuth::Session | None, defaulting to
Session) and `auth_rationale`. The default is deliberately the protected
one. Every app port on this node answered with no credential at all over
LAN, Tailscale, Tor and the FIPS mesh alike — reproduced 2026-08-03 —
precisely because exposure was what you got by saying nothing. Inverting
the default means a new app is protected unless its manifest argues for
an exemption.

Validation makes the argument mandatory: `auth: none` without a
rationale is rejected, and so is a rationale without `auth: none` (that
combination means the author wrote an exemption and did not get one —
shipping it silently would leave them believing otherwise).

17 ports across 12 apps are declared exempt, each with its reason. They
are the ports that cannot sit behind an HTTP login page at all: Lightning
p2p (BOLT-8 noise), LND gRPC/REST and CLN gRPC (macaroon / mutual TLS —
Zeus and remote wallets dial these directly), Bitcoin p2p gossip,
electrum wire protocol, Wyoming voice streams, git-over-SSH, and the UDP
discovery protocols (mDNS, SSDP, STUN). Everything else — 39 published
ports — now defaults to gated.

Bitcoin's RPC 8332 is deliberately NOT exempted: it is already
`bind: 127.0.0.1`, so the gate never sees it, and claiming an exemption
it does not need would put a line in the audit list that means nothing.
If the loopback bind is ever dropped, it fails closed.

Two corpus tests keep this honest: every shipped manifest must parse
under the new rules, and the exempt set is pinned at 17 so any change to
the node's unauthenticated surface has to be a deliberate edit.

Tests: 73/73 archipelago-container, workspace builds clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-03 13:51:15 -04:00
co-authored by Claude Opus 5
parent 24ce8b39e8
commit 0c4826f8cc
13 changed files with 234 additions and 0 deletions
+3
View File
@@ -88,6 +88,9 @@ app:
- host: 8333
container: 8333
protocol: tcp
auth: none
auth_rationale: >-
Bitcoin p2p gossip. Peers are anonymous by design and speak the Bitcoin wire protocol, not HTTP.
volumes:
- type: bind
+3
View File
@@ -88,6 +88,9 @@ app:
- host: 8333
container: 8333
protocol: tcp
auth: none
auth_rationale: >-
Bitcoin p2p gossip. Peers are anonymous by design and speak the Bitcoin wire protocol, not HTTP.
volumes:
- type: bind
+6
View File
@@ -31,9 +31,15 @@ app:
- host: 9736
container: 9735
protocol: tcp # P2P (using 9736 to avoid conflict with LND)
auth: none
auth_rationale: >-
Lightning p2p. The BOLT-8 noise handshake authenticates and encrypts the channel itself.
- host: 9835
container: 9835
protocol: tcp # gRPC
auth: none
auth_rationale: >-
Core Lightning gRPC, authenticated by mutual TLS client certificates.
volumes:
- type: bind
+3
View File
@@ -45,6 +45,9 @@ app:
- host: 50001
container: 50001
protocol: tcp
auth: none
auth_rationale: >-
Electrum wire protocol over TCP. Electrum wallets speak it directly and cannot hold a session cookie.
volumes:
- type: bind
+3
View File
@@ -29,6 +29,9 @@ app:
- host: 2222
container: 22
protocol: tcp
auth: none
auth_rationale: >-
Git over SSH, authenticated by the user's own SSH keypair. Not HTTP, so the gate cannot serve a login page here.
volumes:
- type: bind
+6
View File
@@ -32,9 +32,15 @@ app:
- host: 9738
container: 9735
protocol: tcp # P2P
auth: none
auth_rationale: >-
Lightning p2p. The BOLT-8 noise handshake authenticates and encrypts the channel itself.
- host: 10010
container: 10009
protocol: tcp # gRPC
auth: none
auth_rationale: >-
LND gRPC, authenticated by macaroon over TLS. Remote wallets depend on reaching this directly.
- host: 8091
container: 8080
protocol: tcp # REST/Web UI
+9
View File
@@ -38,12 +38,21 @@ app:
- host: 9735
container: 9735
protocol: tcp
auth: none
auth_rationale: >-
Lightning p2p. The BOLT-8 noise handshake authenticates and encrypts the channel itself.
- host: 10009
container: 10009
protocol: tcp
auth: none
auth_rationale: >-
LND gRPC, authenticated by macaroon over TLS. Zeus and other remote wallets depend on reaching this directly.
- host: 18080
container: 8080
protocol: tcp
auth: none
auth_rationale: >-
LND REST, authenticated by macaroon over TLS. A browser login page would break Zeus and every non-browser wallet client.
volumes:
- type: bind
+3
View File
@@ -51,6 +51,9 @@ app:
- host: 3478
container: 3478
protocol: udp # STUN — must be UDP; tcp here breaks relay discovery
auth: none
auth_rationale: >-
STUN over UDP for NAT traversal; it must answer unauthenticated probes to do its job at all.
volumes:
- type: bind
+3
View File
@@ -40,6 +40,9 @@ app:
- host: 10400
container: 10400
protocol: tcp
auth: none
auth_rationale: >-
Wyoming voice protocol, a binary local-only stream consumed by Home Assistant; not HTTP and not browser-reachable.
volumes:
- type: bind
+3
View File
@@ -40,6 +40,9 @@ app:
- host: 10200
container: 10200
protocol: tcp
auth: none
auth_rationale: >-
Wyoming voice protocol, a binary local-only stream consumed by Home Assistant; not HTTP and not browser-reachable.
volumes:
- type: bind
+3
View File
@@ -48,6 +48,9 @@ app:
- host: 10300
container: 10300
protocol: tcp
auth: none
auth_rationale: >-
Wyoming voice protocol, a binary local-only stream consumed by Home Assistant; not HTTP and not browser-reachable.
volumes:
- type: bind
+6
View File
@@ -33,9 +33,15 @@ app:
- host: 5353
container: 5353
protocol: udp # mDNS/Bonjour
auth: none
auth_rationale: >-
mDNS is UDP multicast service discovery; gating it would break .local name resolution for every device on the LAN.
- host: 1900
container: 1900
protocol: udp # SSDP
auth: none
auth_rationale: >-
SSDP/UPnP discovery is UDP multicast — there is no HTTP request to gate and no client that could hold a session.
volumes:
- type: bind