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
|
the `networkmanager-openvpn` plugin ships system-side
|
||||||
(`networking.networkmanager.plugins`, mkDefault) so the openvpn type is
|
(`networking.networkmanager.plugins`, mkDefault) so the openvpn type is
|
||||||
available — import type is chosen by file extension.
|
available — import type is chosen by file extension.
|
||||||
- **Tailscale:** status (read-only, no privilege) + `up`/`down` + **exit-node**
|
- **Tailscale:** status (read-only) + `up`/`down` + **exit-node** selection. It
|
||||||
selection. It lives outside NetworkManager, so the menu drives the
|
lives outside NetworkManager, so the menu drives the `tailscale` CLI
|
||||||
`tailscale` CLI directly, **self-gated** on the CLI being present
|
directly, **self-gated** on the CLI being present (= `nomarchy.services.tailscale`,
|
||||||
(= `nomarchy.services.tailscale`). Privileged actions go through a terminal +
|
which makes the login user the **operator** via `extraSetFlags`). So
|
||||||
`sudo` (the snapshot-tool pattern) — no operator wiring.
|
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
|
Shape: a `nomarchy-menu vpn` rofi submenu under **System** — NM
|
||||||
VPN/WireGuard connections shown ● active / ○ inactive and toggled on select
|
VPN/WireGuard connections shown ● active / ○ inactive and toggled on select
|
||||||
(networkmanager-group users need no sudo), Import via the Files/`fd` picker
|
(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
|
— a from-scratch WireGuard keypair/peer editor is too much rofi surface, so
|
||||||
creation is deferred to `nm-connection-editor`. Eval + build green; **pending
|
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
|
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
|
session with real configs). Remaining (optional): richer exit-node display
|
||||||
toggles skip the sudo terminal; richer exit-node display (country/city).
|
(country/city).
|
||||||
- **Theme parity with legacy:** summer-day/night now carry their legacy
|
- **Theme parity with legacy:** summer-day/night now carry their legacy
|
||||||
bar layouts as `waybar.jsonc` whole-swaps (adapted: dead legacy script
|
bar layouts as `waybar.jsonc` whole-swaps (adapted: dead legacy script
|
||||||
modules dropped, Nerd-Fonts-v2 codepoints remapped to FontAwesome/v3,
|
modules dropped, Nerd-Fonts-v2 codepoints remapped to FontAwesome/v3,
|
||||||
|
|||||||
@@ -446,9 +446,10 @@ ${themeRows}
|
|||||||
# up/down (networkmanager-group users need no sudo).
|
# up/down (networkmanager-group users need no sudo).
|
||||||
# · Import a WireGuard .conf or OpenVPN .ovpn (nmcli import; OpenVPN needs the
|
# · Import a WireGuard .conf or OpenVPN .ovpn (nmcli import; OpenVPN needs the
|
||||||
# networkmanager-openvpn plugin, shipped system-side).
|
# networkmanager-openvpn plugin, shipped system-side).
|
||||||
# · Tailscale (self-gated on the CLI being present = nomarchy.services.tailscale):
|
# · 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
|
# which makes the login user the operator): status is read-only; up/down/
|
||||||
# terminal + sudo (the snapshot-tool pattern), no operator wiring needed.
|
# 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.
|
# Secrets stay in NetworkManager's / Tailscale's own store — no new manager.
|
||||||
nomarchy-vpn = pkgs.writeShellScriptBin "nomarchy-vpn" ''
|
nomarchy-vpn = pkgs.writeShellScriptBin "nomarchy-vpn" ''
|
||||||
set -u
|
set -u
|
||||||
@@ -485,12 +486,19 @@ ${themeRows}
|
|||||||
}
|
}
|
||||||
|
|
||||||
ts_state() { tailscale status --json 2>/dev/null | jq -r '.BackendState // "Unknown"'; }
|
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() {
|
ts_exit_node() {
|
||||||
node=$( { printf 'none\n'; tailscale exit-node list 2>/dev/null | awk 'NR>1 && $2 {print $2}'; \
|
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
|
printf '%s\n' "$BACK"; } | rofi -dmenu -p "Exit node" ) || return
|
||||||
{ [ -z "$node" ] || [ "$node" = "$BACK" ]; } && return
|
{ [ -z "$node" ] || [ "$node" = "$BACK" ]; } && return
|
||||||
[ "$node" = none ] && node=""
|
[ "$node" = none ] && node=""
|
||||||
${cfg.terminal} -e sudo tailscale set --exit-node="$node"
|
ts_priv set --exit-node="$node"
|
||||||
}
|
}
|
||||||
tailscale_menu() {
|
tailscale_menu() {
|
||||||
while :; do
|
while :; do
|
||||||
@@ -502,8 +510,10 @@ ${themeRows}
|
|||||||
} | rofi -dmenu -p "Tailscale ($st)" ) || return
|
} | rofi -dmenu -p "Tailscale ($st)" ) || return
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
"$BACK"|"") return ;;
|
"$BACK"|"") return ;;
|
||||||
Connect) ${cfg.terminal} -e sudo tailscale up ;;
|
# Authed-but-down → bring it up inline; otherwise it needs an
|
||||||
Disconnect) ${cfg.terminal} -e sudo tailscale down ;;
|
# 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 ;;
|
"Exit node…") ts_exit_node ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ in
|
|||||||
{
|
{
|
||||||
options.nomarchy.services = {
|
options.nomarchy.services = {
|
||||||
tailscale.enable = lib.mkEnableOption ''
|
tailscale.enable = lib.mkEnableOption ''
|
||||||
Tailscale, the mesh VPN — ships the daemon; authenticate once with
|
Tailscale, the mesh VPN — ships the daemon and makes the login user its
|
||||||
`sudo tailscale up`'';
|
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.enable = lib.mkEnableOption ''
|
||||||
Syncthing continuous file sync, running as the login user — add
|
Syncthing continuous file sync, running as the login user — add
|
||||||
@@ -125,6 +126,12 @@ in
|
|||||||
config = lib.mkMerge [
|
config = lib.mkMerge [
|
||||||
(lib.mkIf cfg.tailscale.enable {
|
(lib.mkIf cfg.tailscale.enable {
|
||||||
services.tailscale.enable = true;
|
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 {
|
(lib.mkIf cfg.syncthing.enable {
|
||||||
|
|||||||
@@ -48,7 +48,8 @@
|
|||||||
# latestKernel = true; # newest kernel for very-new hardware (drivers not yet in the default)
|
# 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.syncthing.enable = true; # file sync — GUI at http://127.0.0.1:8384
|
||||||
# nomarchy.services.podman.enable = true; # rootless containers (docker → podman)
|
# nomarchy.services.podman.enable = true; # rootless containers (docker → podman)
|
||||||
# nomarchy.services.flatpak.enable = true; # Flatpak + the Flathub remote
|
# nomarchy.services.flatpak.enable = true; # Flatpak + the Flathub remote
|
||||||
|
|||||||
Reference in New Issue
Block a user