feat(tailscale): operator-set login user — VPN menu skips the sudo terminal
nomarchy.services.tailscale now makes the login user the Tailscale operator (services.tailscale.extraSetFlags = ["--operator=<username>"], 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user