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, whata05956c4had 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 ofa05956c4either way. Compile-checked clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0a1d314ffa
commit
c966395eb9
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user