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 {