Skip to content

Feature: optional Tor onion service for remote dashboard access — auth-gated, default off #343

Description

@VijitSingh97

Summary

Publish the dashboard as a Tor onion (hidden) service so an operator can reach it remotely over Tor, without port-forwarding, a VPN, or exposing the host on the clearnet. This is the most on-brand remote-access path for a Tor-first stack — the supported successor to the (closed / not planned) Tailscale investigation in #262, building on the (closed) auth work in #8.

Today the dashboard is localhost-only by design: it binds 127.0.0.1:8000 (build/dashboard/mining_dashboard/main.py), fronted by Caddy on ${HOST_IP:-127.0.0.1} (pithead, Caddyfile render). The three Tor hidden services in build/tor/torrc.template cover only the mining P2P legs (Monero 18080, Tari 18189, p2pool 37888) — none map to the dashboard. docs/configuration.md states remote access is left to the operator.

Work list — everything to tackle for "dashboard over Tor" (folded here 2026-07-03)

This issue is the single tracker for shipping the onion dashboard. Ordered by dependency; the rationale for each item is in the sections below. Nothing here needs a separate issue — check the boxes as they land.

Status (2026-07-03): the onion feature + its full auth surface are implemented in PR #344 (draft, needs bench validation). R1/R2, A1–A6, D1 done there. The P1/P2 proxy-hardening prerequisites are split into their own bench-validated PR — tracked in #345.

Ship-blocking prerequisites — pre-existing bridge hardening, now tracked in #345 (orthogonal to the onion; the fix touches the #180 subnet core + #122 SSRF guard, so it gets its own validated PR):

Onion routing + plumbing (the real build — see "Routing gap" / "Opt-in plumbing gap"):

Auth hardening (fail-closed at config-render — see "Security hardening" #1#8):

Docs:

Related — raise the stakes but do NOT block a read-only onion (cross-refs, not folded work): the host-mutation cluster #33 (config editor / canonical channel contract), #59 (upgrade), #338 (two-way control), #77 (wizard), #235 (miner badge). For these, the second factor that matters is per-action out-of-band approval, not login 2FA — see "Two-factor via Telegram" below. Keep #235 pull-only (no dashboard→miner write path).

Why this is not just "add a HiddenServicePort"

Scoping correction (auth review, 2026-07-03). An earlier draft of this issue called the Tor wiring "trivial (~one block)." A review of the current code says otherwise: the torrc block looks like one line, but there is no reachable target IP to point it at, and no opt-in plumbing to render it conditionally. Those two are the real build; the auth hardening below is a set of smaller, focused edits on top. See "Routing gap" and "Opt-in plumbing gap" before estimating this.

Routing gap — the biggest piece (no bridge from Tor to the dashboard today)

The naive block is not one line, because the target IP doesn't exist:

HiddenServiceDir /var/lib/tor/dashboard/
HiddenServicePort 80 <caddy-or-dashboard-ip>:80   # ← there is no such IP reachable from the tor container

The tor container lives on the bridge network (172.28.0.25). Caddy and the dashboard are host-networked and bind loopback — Caddy on ${HOST_IP:-127.0.0.1}, the dashboard on 127.0.0.1:8000 (docker-compose.yml, main.py). A HiddenServicePort 80 127.0.0.1:80 inside the tor container resolves to the tor container's own loopback, not the host's Caddy — so it reaches nothing. The three existing onions (Monero/Tari/p2pool) only work because their targets are bridge services (.26/.27/.28); the dashboard is not one. docs/privacy.md states it outright: "the host-networked dashboard and caddy aren't on the bridge."

So a bridge has to be built first. Two options:

Either way, the onion must front Caddy (which carries the auth), never the dashboard's raw 127.0.0.1:8000 (which has none).

Opt-in plumbing gap (torrc is static today)

build/tor/torrc.template is always-on and the tor entrypoint only substitutes __NETWORK_PREFIX__ via one sed (build/tor/entrypoint.sh). There is no dashboard.onion.enabled key and no mechanism to render a hidden-service block conditionally. Making the onion default-off needs new plumbing — pithead passing a conditional env the entrypoint expands, or pithead rendering the block itself. This doesn't exist yet.

The safety story

The remaining work is the safety story. The dashboard ships no auth by default (docs/configuration.md: "anyone who can reach the host can open it"). Optional HTTP basic-auth exists via dashboard.auth.password (Caddy). Publishing an unauthenticated control panel as an onion means anyone with the .onion address gets the panel — including start/stop failover controls via docker-control. So:

  • Enabling the onion MUST force authentication. Refuse to render the hidden service unless dashboard.auth.password is set (or the stronger option below). Fail-closed at config-render time in pithead, with a loud error — not a runtime surprise.
  • Prefer Tor client authorization (v3 onion client-auth) over (or in addition to) HTTP basic-auth: the service is invisible/unreachable without the client key, which is a much stronger posture than a guessable password behind a published address.
  • The onion serves plain HTTP inside the Tor tunnel (Tor provides the transport encryption); no need for Caddy tls internal on the onion vhost. Confirm the Caddy config change stays localhost-only for the LAN path and adds the onion vhost separately.

Proposed shape

Egress note (not a leak)

This is inbound access over Tor — it does not change the egress posture reviewed for #160. The dashboard's own outbound (XvB, update check, healthchecks) already routes over Tor socks5h. No new clearnet surface.

Related


Security hardening (requirements, not nice-to-haves)

Publishing an unauthenticated-by-default control panel at a stable .onion is a real attack-surface change. The auth that exists today (dashboard.auth.password, #8: opt-in, bcrypt-hashed into the Caddyfile, wraps all routes incl. /api/state) is validated only for length 8–128, no double-quotes (pithead ~L1808) — password and 12345678 pass. That floor is fine for a LAN panel; it is not fine for a published onion. Enforce the following, fail-closed at config-render time:

  1. Strong password enforcement. When the onion is enabled, require more than length ≥8: minimum ≥16 chars (or a passphrase), and reject a denylist of common/weak passwords (top-N list) and obvious patterns. Better default: auto-generate a strong random password when the onion is turned on and no strong one is set, and print it once — same pattern as generate_node_password (pithead ~L1489). Do not let a published onion come up behind a weak secret.

  2. Prefer Tor v3 client authorization as the primary gate. (Already noted above.) A client-auth'd onion is unreachable without the client key — the service doesn't even respond, which defeats brute force and address-scanning entirely. Treat basic_auth as defense-in-depth behind client-auth, not as the only lock. If client-auth is configured, still require the password too.

  3. Brute-force reality: per-IP throttling is meaningless over Tor. All requests arrive from the Tor daemon (no source IP), so rate-limit-by-IP buys nothing. This is the core reason client-auth (Set pool in config, more info in dashboard #2) — not a password + rate limiter — is the correct primary defense. If basic_auth is the only gate, document that it is online-brute-forceable and lean on password strength (Simpler way to deploy #1).

  4. The onion vhost needs its own auth directive. Today's basic_auth block is attached to the https://$HOST_IP site (pithead ~L2247). A new http://<onion> vhost that omits it would serve the panel with no login at all. Render the auth block into the onion vhost too, or refuse to render the vhost — cover it with a test.

  5. Onion key hygiene. v3 only. Dedicated HiddenServiceDir with 0700 perms, a separate key from the mining-service onions — never reuse. The dashboard onion private key is now sensitive: it's already inside the Tor data dir that backup protects (good), but call it out so it isn't logged, added to telemetry, or shipped in a backup that leaves the host.

  6. Treat the .onion hostname as a capability/secret. Anyone who learns it can attempt access (and, without client-auth, reach the login). Don't print it into shared logs, the update-check, or anything that egresses; surface it only to the local operator (e.g. a pithead onion-address command).

  7. Exempt the onion vhost from the plain-HTTP warning. pithead ~L1822 warns when dashboard.auth is set but dashboard.secure is false. Inside a Tor tunnel the transport is already encrypted, so plain HTTP on the onion vhost is fine — that warning must not fire (or hard-block) for the onion path.

  8. Rotation. Provide a way to rotate the onion address (regenerate HiddenServiceDir) and the password. A leaked .onion is otherwise permanent.

Keep the existing input sanitization (no double-quotes / printable-only, so the secret can't break or inject into the generated Caddyfile) and the bcrypt hashing — both already correct; extend, don't replace.

Already in the tree — reuse, don't rebuild (auth review, 2026-07-03)

  • bcrypt pipeline + stable-hash fingerprint (caddy_hash_password_b64, the sha256 DASHBOARD_AUTH_PW_FP mechanism) — reuse verbatim for the onion's forced password.
  • generate_node_password (pithead ~L1489) — reuse for the auto-generate-strong-password default (hardening Simpler way to deploy #1).
  • Onion-address surfacing loop (pithead status, the env_get onion loop ~L746) — a dashboard onion slots straight in (hardening Infrastructure and Documentation Overhaul #6).
  • Backup already captures TOR_DATA_DIR (pithead ~L901) — the new onion key rides along and survives restore; no extra work (hardening Add automated high-performance XMRig provisioning kit #5).
  • socat loopback-bridge (build/p2pool/entrypoint.sh ~L30) — the template for the routing gap above.

Net: the auth primitive is sound and reusable. The two genuinely new chunks are the routing bridge and Tor v3 client-auth (nothing like it exists — grep finds zero HiddenServiceAuthorizeClient/authorized_clients). Hardening #1/#4/#7 are small edits to pithead's render.

Two-factor via Telegram: where it helps, where it's theatre (auth review, 2026-07-03)

Question raised during review: should dashboard login require a second confirmation via the Telegram bot (#121/#341)? Verdict: gating login with Telegram is mostly security theatre in this design; out-of-band Telegram confirmation on dangerous actions is genuinely valuable. Do the second, not the first.

Why Telegram-2FA-at-login is weak here

  1. Redundant behind the control we already recommend. This issue's primary gate is Tor v3 client-auth (hardening Set pool in config, more info in dashboard #2): the onion doesn't even respond without the client key, so brute-force and address-scanning are dead at the transport layer. A login second factor stops a threat that can no longer reach the service — near-zero marginal risk reduction.
  2. Where it's not redundant, it's the wrong fix. If client-auth is skipped and a basic_auth-only onion is published, the password is online-brute-forceable (per-IP throttling is meaningless over Tor — hardening Add support for querying workers using avahi #3). Telegram 2FA raises that bar, but the correct answer is to turn on client-auth, not to compensate for a weak transport with a bolt-on second factor.
  3. Same-device collapse defeats the whole use case. The core scenario for this feature is "alert fires → open the dashboard on my phone." If the dashboard and the Telegram approval live on the same phone, the two "factors" are one device — a stolen/compromised phone owns both. In the primary usage pattern the independence that makes 2FA meaningful is gone.
  4. Telegram is a poor cryptographic second factor. Accounts are phone-number-bound (SIM-swap), cloud chats aren't E2E by default, and the account itself is phishable. The factor's strength collapses to "how well is your Telegram account secured" — for many users an SMS channel, weaker than a TOTP app or hardware key.
  5. It couples login to a third party's uptime — during an incident. The remote dashboard exists for incident response. Telegram down / no signal / bot rate-limited ⇒ locked out of your own dashboard exactly when you need it. That's an availability regression against the feature's purpose. (It also contradicts the Tor-first posture — Telegram is a clearnet SaaS; the bot already routes over Tor per Route Telegram bot traffic over Tor (privacy: don't expose host IP to Telegram) #340, but login-blocking on it is a hard dependency.)

Where out-of-band Telegram approval is the real thing — the actions, not the login

The elevated risk in this stack is not reading the dashboard (the web surface is read-only today — only GET / + GET /api/state, no app-level mutation). It's the host-mutation actions the roadmap adds: #33 (change payout wallet / pithead apply), #59 (pithead upgrade), #338 (restart/apply). A login gate protects the session; a hijacked session then does everything.

Per-action out-of-band approval is transaction-signing, not session-gating. The Telegram message states exactly what is about to change — "Change payout wallet to 4A…xyz — approve? (deny in 60s)" — so even a fully hijacked dashboard session cannot silently redirect earnings or upgrade the host without a human seeing and approving the specific change. That is where the money is, it degrades safely (Telegram down ⇒ you can't do the dangerous thing, but reading still works), and it's already the direction #33/#59/#338 point ("confirmation on destructive changes"). This belongs in those issues, not as a login gate here.

Recommendation

Metadata

Metadata

Assignees

No one assigned

    Labels

    dashboardMining dashboard web UIenhancementNew feature or requestsecuritySecurity-sensitive issue or hardening

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions