Goal: one panel that answers "how do I reach Raspy from here?" and lets an admin turn remote access on. It's a networking dashboard plus controls.
Admin-only (in manifest._ADMIN_ONLY): it shows addresses, controls public
exposure, and holds a stored secret. Its router refuses non-admins. It never runs
as root and never installs provider binaries — it detects them and links to docs.
A single dashboard object:
local— every non-loopback interface IP (IPv4 + IPv6), each classified (lan/tailscale/link-local) and given a ready linkhttp://<host>:<port>(IPv6 bracketed). Discovered with stdlib socket tricks, nonetifacesdependency.public— the internet-facing IPv4/IPv6 via a keyless lookup (Cloudflare trace). Informational: only reachable if the port is actually forwarded/exposed.tailscale— installed?, backend state, connected?, the assigned IP(s) + MagicDNS name as access links, the login account email (login_namefromtailscale status --json'sUser[Self.UserID].LoginName), display name, tailnet, and whether Tailscale SSH is advertised. Tailscale is a machine-level VPN already running on the box — Raspy discovers the address, it doesn't assign it.cloudflare— installed?, configured? (token present), running?, and the public hostname/link when known.
The link port is the spine's configured settings.port.
cloudflare/token— store the tunnel token (encrypted at rest, per-attachment Fernet key; never returned to the client).cloudflare/up/cloudflare/down— supervisecloudflared tunnel run --token …as a tracked child;upis idempotent (kills the old child first), and the spine's shutdown stops it so no tunnel is orphaned.tailscale/up— bring Tailscale up; if it needs an interactive login the daemon's login URL (AuthURL/ scraped) is surfaced for the UI to show. No auth key is taken or stored — the normal flow is a browser login.tailscale/down— disconnect (keeps the node key).tailscale/logout— full logout (forgets the account/node key).tailscale/ssh— enable/disable Tailscale SSH (tailscale set --ssh[=false]).tailscale/exit-node— route this box's traffic through a tailnet peer, or clear it (set --exit-node=<name|ip>/--exit-node=). The status object lists the available exit-node peers + the active one.tailscale/advertise-exit-node— offer/stop offering this node as an exit node (set --advertise-exit-node[=false]).tailscale/update— update the tailscale client (update --yes).tailscale/netcheck,tailscale/ping— read-only diagnostics that return their text output for the UI to show. No root, so no sudo prompt.
The status object also reports the client version/update_available, the active
exit_node, whether we're advertising_exit_node, and the list of exit_nodes.
Every mutating Tailscale action above honours the sudo-password flow (see Security boundary): it runs unprivileged first and only escalates with an explicit, user-supplied password when the command reports it needs root.
- Not installed: status reports
installed: falseand the UI shows an install link; every action endpoint returns 503 with a clear message. - Installed but not configured / logged out: Cloudflare shows
configured: false(Connect disabled until a token is saved); Tailscale shows the login button and, afterup, the login URL. - A failing probe never blanks the dashboard:
local,public, andtailscaleare gathered independently and each falls back to an empty/neutral value on error.
- The Cloudflare token is written only to the account data dir, encrypted, and never echoed back.
- Bringing a tunnel up / changing Tailscale is a mutating admin action behind the normal auth + CSRF gate.
- A Tailscale action that needs root (
up/down/logout/set --ssh) never escalates silently. The endpoint first runs the command unprivileged; if it fails for lack of root it returns 428 withdetail: needs-root. The UI then pops a sudo-password prompt (SudoPrompt) and retries with the password in the request body. The password is sent only over the encrypted channel (core/channel), handed tosudo -S -k -p ''on stdin (never in argv, which is world-readable via/proc), used once, and never stored or logged. A wrong password returns 403; the prompt stays open to re-try.