From c966395eb9fbb58e629c8636b1d41e9181cec0f3 Mon Sep 17 00:00:00 2001 From: archipelago Date: Sun, 2 Aug 2026 16:37:53 -0400 Subject: [PATCH] fix(security): never auto-publish the wallet UI proxies as Tor onions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found while checking whether the /lnd-connect-info leak (a05956c4) was also reachable over Tor. It was not — but only by accident, and the accident was one app id away from failing. auto_add_tor_service() creates a hidden service for a freshly installed app, mapping onion:80 -> 127.0.0.1:. It skips the node's own service and is_protocol_service() — which names the DAEMONS (bitcoin, bitcoin-knots, electrs, electrumx, lnd) but NOT their UI sidecars. lnd-ui and bitcoin-ui are real, installable app ids (apps/lnd-ui, apps/bitcoin-ui) whose host ports are 18083 and 8334: exactly the two ports that served the admin macaroon and the credential-injecting Bitcoin RPC proxy. So nothing structural prevented either from acquiring a GLOBAL onion as a silent side effect of being installed — re-exposing worldwide, and persistently, what a05956c4 had just closed to mesh/LAN peers. Verified on a live node that this has not fired (services.json maps lnd to 8080 and holds no *-ui entry, and the running torrc contains neither port), so this closes a latent hole rather than an active one. The gate gets its own named predicate rather than an addition to is_protocol_service, because the two express different things: is_protocol_service says "this speaks a wire protocol, not HTTP", while never_auto_onioned says "this fronts the node's money and must not be published unasked". Conflating them would have made the fix look like a classification tweak. This gates only the AUTOMATIC path. An operator who deliberately enables Tor for one of these apps still can — that is an informed choice, not a silent default. Both endpoints are session-gated at the backend as of a05956c4 either way. Compile-checked clean. Co-Authored-By: Claude Opus 5 (1M context) --- core/archipelago/src/api/rpc/tor/handlers.rs | 15 +++++++++--- core/archipelago/src/api/rpc/tor/mod.rs | 24 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/core/archipelago/src/api/rpc/tor/handlers.rs b/core/archipelago/src/api/rpc/tor/handlers.rs index d45caa5f..ba3f004f 100644 --- a/core/archipelago/src/api/rpc/tor/handlers.rs +++ b/core/archipelago/src/api/rpc/tor/handlers.rs @@ -370,13 +370,22 @@ impl RpcHandler { /// Best-effort auto-exposure of a freshly installed app as a Tor hidden /// service. Skips protocol services (bitcoin/lnd keep their explicit - /// flows), the node's own service, apps that already have one, and apps - /// with no resolvable web port. Runs detached after install — it never - /// fails the caller, it only logs. + /// flows), the node's own service, the credential-bearing UI proxies in + /// [`never_auto_onioned`], apps that already have one, and apps with no + /// resolvable web port. Runs detached after install — it never fails the + /// caller, it only logs. pub(in crate::api::rpc) async fn auto_add_tor_service(&self, app_id: &str) { if app_id == "archipelago" || is_protocol_service(app_id) { return; } + if never_auto_onioned(app_id) { + info!( + app = app_id, + "Skipping auto Tor service — this app fronts wallet/node RPC and \ + must not gain a global onion as a side effect of being installed" + ); + return; + } let config_dir = self.config.data_dir.join("tor-config"); // The scanner may still be deriving the launch address on slower // nodes; retry for up to ~5 minutes before giving up quietly. diff --git a/core/archipelago/src/api/rpc/tor/mod.rs b/core/archipelago/src/api/rpc/tor/mod.rs index c0882343..00a01447 100644 --- a/core/archipelago/src/api/rpc/tor/mod.rs +++ b/core/archipelago/src/api/rpc/tor/mod.rs @@ -422,6 +422,30 @@ pub(in crate::api::rpc) fn is_protocol_service(name: &str) -> bool { ) } +/// Apps that must never gain a Tor hidden service *automatically*. +/// +/// These are credential-bearing reverse proxies in front of the node's money: +/// `lnd-ui` (host port 18083) serves `/lnd-connect-info` and fronts LND, and +/// `bitcoin-ui` (host port 8334) proxies Bitcoin Core RPC with the RPC +/// password injected on the caller's behalf. Both are session-gated at the +/// backend as of the 2026-08-02 fix, but an onion is a *global, persistent* +/// door, and neither should acquire one merely as a side effect of being +/// installed. +/// +/// [`is_protocol_service`] does not cover them: it names the daemons (`lnd`, +/// `bitcoin-knots`), not their UI sidecars. Without this list, +/// `auto_add_tor_service` would publish `onion:80 -> 127.0.0.1:18083` and +/// `-> :8334` the moment either app installs — which is exactly the surface +/// that leaked the LND admin macaroon, re-exposed worldwide instead of just +/// to mesh/LAN peers. +/// +/// This gates only the automatic path. An operator who deliberately enables +/// Tor for one of these apps still can; that is an informed choice, not a +/// silent default. +pub(in crate::api::rpc) fn never_auto_onioned(name: &str) -> bool { + matches!(name, "lnd-ui" | "bitcoin-ui") +} + // ─── Config I/O ────────────────────────────────────────────────── fn tor_data_dir() -> String {