From 431af618cc5d7b1660647ba220bf598b813d6ec9 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Tue, 23 Jun 2026 11:26:15 +0100 Subject: [PATCH] =?UTF-8?q?feat(tailscale):=20operator-set=20login=20user?= =?UTF-8?q?=20=E2=80=94=20VPN=20menu=20skips=20the=20sudo=20terminal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nomarchy.services.tailscale now makes the login user the Tailscale operator (services.tailscale.extraSetFlags = ["--operator="], mkDefault), so `tailscale up/down/set` — and the System → VPN menu's Tailscale controls — run without sudo. The user is already in wheel, so the operator grant is no real new privilege, just skips the prompt. nomarchy-vpn's Tailscale actions are now operator-aware: a ts_priv helper runs the subcommand inline and only falls back to a sudo terminal if the operator grant is absent (downstream dropped it). Connect brings an authed-but-down node up inline; a node that still needs interactive login goes to a terminal so the auth URL is visible. Option description + template comment updated; ROADMAP follow-up closed. System + home builds green. Co-Authored-By: Claude Opus 4.8 --- docs/ROADMAP.md | 16 +++++++++------- modules/home/rofi.nix | 22 ++++++++++++++++------ modules/nixos/services.nix | 11 +++++++++-- templates/downstream/system.nix | 3 ++- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index da96562..2d786dc 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -108,11 +108,13 @@ how to override it. Items marked ✓ are shipped. the `networkmanager-openvpn` plugin ships system-side (`networking.networkmanager.plugins`, mkDefault) so the openvpn type is available — import type is chosen by file extension. - - **Tailscale:** status (read-only, no privilege) + `up`/`down` + **exit-node** - selection. It lives outside NetworkManager, so the menu drives the - `tailscale` CLI directly, **self-gated** on the CLI being present - (= `nomarchy.services.tailscale`). Privileged actions go through a terminal + - `sudo` (the snapshot-tool pattern) — no operator wiring. + - **Tailscale:** status (read-only) + `up`/`down` + **exit-node** selection. It + lives outside NetworkManager, so the menu drives the `tailscale` CLI + directly, **self-gated** on the CLI being present (= `nomarchy.services.tailscale`, + which makes the login user the **operator** via `extraSetFlags`). So + up/down/exit-node run **inline without sudo**, falling back to a sudo terminal + only if the operator grant is absent; the first interactive login uses a + terminal (the auth URL is visible). Shape: a `nomarchy-menu vpn` rofi submenu under **System** — NM VPN/WireGuard connections shown ● active / ○ inactive and toggled on select (networkmanager-group users need no sudo), Import via the Files/`fd` picker @@ -125,8 +127,8 @@ how to override it. Items marked ✓ are shipped. — a from-scratch WireGuard keypair/peer editor is too much rofi surface, so creation is deferred to `nm-connection-editor`. Eval + build green; **pending an on-machine check** (the nmcli import/up-down + Tailscale paths need a live - session with real configs). Remaining (optional): operator-set Tailscale so its - toggles skip the sudo terminal; richer exit-node display (country/city). + session with real configs). Remaining (optional): richer exit-node display + (country/city). - **Theme parity with legacy:** summer-day/night now carry their legacy bar layouts as `waybar.jsonc` whole-swaps (adapted: dead legacy script modules dropped, Nerd-Fonts-v2 codepoints remapped to FontAwesome/v3, diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index e6b6912..bdaaeb4 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -446,9 +446,10 @@ ${themeRows} # up/down (networkmanager-group users need no sudo). # · Import a WireGuard .conf or OpenVPN .ovpn (nmcli import; OpenVPN needs the # networkmanager-openvpn plugin, shipped system-side). - # · Tailscale (self-gated on the CLI being present = nomarchy.services.tailscale): - # status is read-only; up/down/exit-node need root, so they go via a - # terminal + sudo (the snapshot-tool pattern), no operator wiring needed. + # · Tailscale (self-gated on the CLI being present = nomarchy.services.tailscale, + # which makes the login user the operator): status is read-only; up/down/ + # exit-node run inline without sudo thanks to the operator grant, falling + # back to a sudo terminal if it's absent. First login goes to a terminal. # Secrets stay in NetworkManager's / Tailscale's own store — no new manager. nomarchy-vpn = pkgs.writeShellScriptBin "nomarchy-vpn" '' set -u @@ -485,12 +486,19 @@ ${themeRows} } ts_state() { tailscale status --json 2>/dev/null | jq -r '.BackendState // "Unknown"'; } + # Run a privileged tailscale subcommand. nomarchy.services.tailscale makes + # the login user the operator, so this runs inline (no sudo); if the operator + # grant is absent (downstream dropped it) it falls back to a sudo terminal. + ts_priv() { + if tailscale "$@" >/dev/null 2>&1; then notify-send "Tailscale" "tailscale $*" + else ${cfg.terminal} -e sudo tailscale "$@"; fi + } ts_exit_node() { node=$( { printf 'none\n'; tailscale exit-node list 2>/dev/null | awk 'NR>1 && $2 {print $2}'; \ printf '%s\n' "$BACK"; } | rofi -dmenu -p "Exit node" ) || return { [ -z "$node" ] || [ "$node" = "$BACK" ]; } && return [ "$node" = none ] && node="" - ${cfg.terminal} -e sudo tailscale set --exit-node="$node" + ts_priv set --exit-node="$node" } tailscale_menu() { while :; do @@ -502,8 +510,10 @@ ${themeRows} } | rofi -dmenu -p "Tailscale ($st)" ) || return case "$choice" in "$BACK"|"") return ;; - Connect) ${cfg.terminal} -e sudo tailscale up ;; - Disconnect) ${cfg.terminal} -e sudo tailscale down ;; + # Authed-but-down → bring it up inline; otherwise it needs an + # interactive login, so show it in a terminal (URL visible). + Connect) if [ "$st" = Stopped ]; then ts_priv up; else ${cfg.terminal} -e sudo tailscale up; fi ;; + Disconnect) ts_priv down ;; "Exit node…") ts_exit_node ;; esac done diff --git a/modules/nixos/services.nix b/modules/nixos/services.nix index 05a5a33..2a388a4 100644 --- a/modules/nixos/services.nix +++ b/modules/nixos/services.nix @@ -11,8 +11,9 @@ in { options.nomarchy.services = { tailscale.enable = lib.mkEnableOption '' - Tailscale, the mesh VPN — ships the daemon; authenticate once with - `sudo tailscale up`''; + Tailscale, the mesh VPN — ships the daemon and makes the login user its + operator, so `tailscale up/down/set` and the System → VPN menu work + without sudo. Authenticate once (the menu's Connect, or `tailscale up`)''; syncthing.enable = lib.mkEnableOption '' Syncthing continuous file sync, running as the login user — add @@ -125,6 +126,12 @@ in config = lib.mkMerge [ (lib.mkIf cfg.tailscale.enable { services.tailscale.enable = true; + # Let the login user drive tailscale (up/down/set — and so the VPN menu's + # Tailscale controls) without sudo. It's already in wheel, so the operator + # grant is no real new privilege, just skips the password prompt. The + # module's tailscaled-set unit applies this after the daemon starts. + # mkDefault so a downstream can drop or replace it (e.g. extraSetFlags = []). + services.tailscale.extraSetFlags = lib.mkDefault [ "--operator=${args.username}" ]; }) (lib.mkIf cfg.syncthing.enable { diff --git a/templates/downstream/system.nix b/templates/downstream/system.nix index 6ce4b4a..4cdb0b4 100644 --- a/templates/downstream/system.nix +++ b/templates/downstream/system.nix @@ -48,7 +48,8 @@ # latestKernel = true; # newest kernel for very-new hardware (drivers not yet in the default) # }; # - # nomarchy.services.tailscale.enable = true; # mesh VPN — then `sudo tailscale up` + # nomarchy.services.tailscale.enable = true; # mesh VPN — connect from System › VPN + # # (login user is operator, no sudo) # nomarchy.services.syncthing.enable = true; # file sync — GUI at http://127.0.0.1:8384 # nomarchy.services.podman.enable = true; # rootless containers (docker → podman) # nomarchy.services.flatpak.enable = true; # Flatpak + the Flathub remote