fix(security): never auto-publish the wallet UI proxies as Tor onions

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:<the app's host port>. 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) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-02 16:37:53 -04:00
co-authored by Claude Opus 5
parent 0a1d314ffa
commit c966395eb9
2 changed files with 36 additions and 3 deletions
+12 -3
View File
@@ -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.
+24
View File
@@ -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 {