From f733a5238cbb9836773f5ed565a2a4850cf7e006 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 19 Jul 2026 11:34:28 -0300 Subject: [PATCH 1/2] tailscale and user groups --- executables/claude-code.sh | 12 +++ executables/opencode.sh | 2 +- firejail.nix | 49 ---------- home/user.nix | 53 ++++++---- microvm/sandbox.nix | 7 ++ networking.nix | 11 ++- tailscale.nix | 196 ++++++++++--------------------------- 7 files changed, 113 insertions(+), 217 deletions(-) create mode 100644 executables/claude-code.sh diff --git a/executables/claude-code.sh b/executables/claude-code.sh new file mode 100644 index 0000000..646145b --- /dev/null +++ b/executables/claude-code.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +SSH_TARGET="sandbox@vsock%3" + +if [[ "$PWD" == /home/user* ]] || [[ "$PWD" == /etc/nixos* ]]; then + project="${PWD/\/home\/user/\/home\/sandbox}" +else + project="$(ssh "$SSH_TARGET" 'mktemp -d -p /tmp claude-XXXXXX')" +fi + +exec ssh -t "$SSH_TARGET" claude-code "$project" "$@" diff --git a/executables/opencode.sh b/executables/opencode.sh index 8735693..8bae728 100644 --- a/executables/opencode.sh +++ b/executables/opencode.sh @@ -3,7 +3,7 @@ set -euo pipefail SSH_TARGET="sandbox@vsock%3" -if [[ "$PWD" == /home/user* ]]; then +if [[ "$PWD" == /home/user* ]] || [[ "$PWD" == /etc/nixos* ]]; then project="${PWD/\/home\/user/\/home\/sandbox}" else project="$(ssh "$SSH_TARGET" 'mktemp -d -p /tmp opencode-XXXXXX')" diff --git a/firejail.nix b/firejail.nix index 7a95668..c618625 100644 --- a/firejail.nix +++ b/firejail.nix @@ -4,59 +4,10 @@ pkgs, ... }: -let - agentProfile = pkgs.writeText "agent.profile" '' - whitelist /nix/store - whitelist /nix/var - read-only /nix/store - whitelist ''${HOME}/.cache/nix - whitelist ''${HOME}/.local/state/nix - whitelist /etc/nix - whitelist /etc/nixos - whitelist /nix/var/nix/daemon-socket - whitelist ''${HOME}/dev - whitelist ''${HOME}/work - whitelist ''${HOME}/.config - - blacklist ''${HOME} - blacklist /etc/nixos/secrets - blacklist /etc/shadow - blacklist /etc/sudoers - blacklist /etc/sudoers.d - blacklist /root - - caps.drop all - nonewprivs - noroot - seccomp - - no3d - nodvd - notv - nou2f - novideo - - - ignore net none - - machine-id - disable-mnt - private-dev - private-tmp - private-cache - ''; -in { programs.firejail = { enable = true; wrappedBinaries = { - claude = { - executable = "${pkgs.claude-code}/bin/claude"; - profile = agentProfile; - extraArgs = [ - "--private=~/.local/state/claude-home" - ]; - }; chromium-sandbox = { executable = "${pkgs.chromium}/bin/chromium"; profile = "${pkgs.firejail}/etc/firejail/chromium-browser.profile"; diff --git a/home/user.nix b/home/user.nix index afe9204..cf62c79 100644 --- a/home/user.nix +++ b/home/user.nix @@ -58,25 +58,38 @@ # "Xft.antialias" = 1; # "Xft.rgba" = "rgb"; # }; - - systemd.user.services.waypipe-socket = { + systemd.user.services.screenshot-latest = { Unit = { - Description = "start waypipe client"; - After = [ "niri.service" ]; - Requires = [ "niri.service" ]; - }; - Install = { - WantedBy = [ "graphical-session.target" ]; + Description = "Update latest.png symlink"; }; Service = { - ExecStart = "${pkgs.waypipe}/bin/waypipe --socket /tmp/waypipe.sock client"; - ExecStartPost = "${pkgs.acl}/bin/setfacl -m g:sandbox:rw /tmp/waypipe.sock"; - ExecStopPost = "${pkgs.coreutils}/bin/rm /tmp/waypipe.sock"; - RuntimeDirectory = "waypipe"; - Type = "simple"; - Restart = "on-failure"; + Type = "oneshot"; + ExecStart = + let + screenshotDir = "${config.home.homeDirectory}/pictures/screenshots"; + script = pkgs.writeShellScript "screenshot-latest-update" '' + latest=$(${pkgs.coreutils}/bin/ls -t "${screenshotDir}"/*.png 2>/dev/null | head -1) + if [ -n "$latest" ]; then + ${pkgs.coreutils}/bin/ln -sf "$latest" "${screenshotDir}/latest.png" + fi + ''; + in + "${script}"; }; }; + + systemd.user.paths.screenshot-latest = { + Unit = { + Description = "Watch screenshot directory for new files"; + }; + Install = { + WantedBy = [ "default.target" ]; + }; + Path = { + PathChanged = "${config.home.homeDirectory}/pictures/screenshots"; + }; + }; + sops.secrets."user/gpg/legacy_fnzr" = { }; home.activation.importGpgKey = config.lib.dag.entryAfter [ "writeBoundary" ] '' if [[ -f "${config.sops.secrets."user/gpg/legacy_fnzr".path}" ]]; then @@ -258,7 +271,7 @@ }; programs = { - opencode.enable = true; + # opencode.enable = true; dank-material-shell.enable = true; ssh = { enable = true; @@ -364,11 +377,13 @@ home.packages = with pkgs; [ xrdb - # (writeShellScriptBin "agent" '' - # machinectl shell agent@ ${waypipe}/bin/waypipe --socket /run/waypipe.sock server fish - # '') (writeShellApplication { - name = "opencode-sandbox"; + name = "claude-code"; + runtimeInputs = [ openssh ]; + text = builtins.readFile ../executables/claude-code.sh; + }) + (writeShellApplication { + name = "opencode"; runtimeInputs = [ openssh ]; text = builtins.readFile ../executables/opencode.sh; }) diff --git a/microvm/sandbox.nix b/microvm/sandbox.nix index cd530ed..3efba48 100644 --- a/microvm/sandbox.nix +++ b/microvm/sandbox.nix @@ -92,6 +92,7 @@ in fd ripgrep podman-compose + claude-code ]; environment.persistence."/.persist" = { @@ -212,6 +213,12 @@ in source = "/home/user/work"; mountPoint = "/home/sandbox/work"; } + { + proto = "virtiofs"; + tag = "nixos"; + source = "/etc/nixos"; + mountPoint = "/etc/nixos"; + } { proto = "virtiofs"; tag = "screenshots"; diff --git a/networking.nix b/networking.nix index 72cb6ee..410dbae 100644 --- a/networking.nix +++ b/networking.nix @@ -5,7 +5,10 @@ ... }: { - imports = [ ./wireguard.nix ]; + imports = [ + ./wireguard.nix + ./tailscale.nix + ]; sops.secrets = { "wg0/conf".sopsFile = ./secrets/vpn.yaml; "wg-br0/conf".sopsFile = ./secrets/vpn.yaml; @@ -13,6 +16,12 @@ "wg-uk0/conf".sopsFile = ./secrets/vpn.yaml; }; + services.tailscale-netns = { + enable = true; + instances.work = { + }; + }; + networking = { hostName = hostname; networkmanager.enable = false; diff --git a/tailscale.nix b/tailscale.nix index af1693e..02a4f42 100644 --- a/tailscale.nix +++ b/tailscale.nix @@ -10,137 +10,24 @@ with lib; let cfg = config.services.tailscale-netns; - instanceOptions = - { name, ... }: - { - options = { - authKeyFile = mkOption { - type = types.nullOr types.path; - default = null; - description = "Path to file containing a Tailscale auth key."; - }; - - hostname = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - Tailscale hostname for this instance. - Defaults to "-". - ''; - }; - - advertiseRoutes = mkOption { - type = types.listOf types.str; - default = [ ]; - description = "Subnet routes to advertise (e.g. [ \"10.0.0.0/24\" ])."; - }; - - dns = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - Bootstrap DNS server for the namespace before Tailscale is connected. - Defaults to `1.1.1.1`. Only used for the initial coordination server - connection; once Tailscale is up it manages DNS inside the namespace. - ''; - }; - - advertiseExitNode = mkOption { - type = types.bool; - default = false; - description = "Whether to advertise this node as an exit node."; - }; - - acceptRoutes = mkOption { - type = types.bool; - default = false; - description = "Whether to accept routes advertised by other nodes."; - }; - - exitNode = mkOption { - type = types.nullOr types.str; - default = null; - description = "Tailscale IP or hostname of exit node to use."; - }; - - extraUpFlags = mkOption { - type = types.listOf types.str; - default = [ ]; - description = "Extra flags passed to `tailscale up`."; - }; - - extraDaemonFlags = mkOption { - type = types.listOf types.str; - default = [ ]; - description = "Extra flags passed to `tailscaled`."; - }; - }; + instanceOptions = { + options.extraDaemonFlags = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Extra flags passed to `tailscaled`."; }; - - mkUpFlags = - instanceCfg: - let - hostFlag = optional ( - instanceCfg.hostname != null - ) "--hostname=${escapeShellArg instanceCfg.hostname}"; - routeFlags = map (r: "--advertise-routes=${escapeShellArg r}") instanceCfg.advertiseRoutes; - exitFlag = optional instanceCfg.advertiseExitNode "--advertise-exit-node"; - acceptFlag = optional instanceCfg.acceptRoutes "--accept-routes"; - exitNodeFlag = optional ( - instanceCfg.exitNode != null - ) "--exit-node=${escapeShellArg instanceCfg.exitNode}"; - in - hostFlag ++ routeFlags ++ exitFlag ++ acceptFlag ++ exitNodeFlag ++ instanceCfg.extraUpFlags; - - mkAutoconnectScript = - name: instanceCfg: - pkgs.writers.writeBash "tailscale-autoconnect-${name}" '' - set -euo pipefail - - TS="${pkgs.tailscale}/bin/tailscale" - SOCKET="/run/tailscale-${name}/tailscaled.sock" - TS_ARGS="--socket $SOCKET" - - get_state() { - $TS $TS_ARGS status --json --peers=false 2>/dev/null | ${pkgs.jq}/bin/jq -r '.BackendState // "NoState"' - } - - last_state="" - while state="$(get_state)"; do - if [[ "$state" != "$last_state" ]]; then - case "$state" in - NeedsLogin|NeedsMachineAuth|Stopped) - echo "[tailscale-netns:${name}] server needs authentication, sending auth key" - $TS $TS_ARGS up \ - --auth-key "$(cat ${instanceCfg.authKeyFile})" \ - ${escapeShellArgs (mkUpFlags instanceCfg)} - ;; - Running) - echo "[tailscale-netns:${name}] tailscale is running" - ${pkgs.systemd}/bin/systemd-notify --ready - exit 0 - ;; - NoState) - echo "[tailscale-netns:${name}] tailscaled not ready yet" - ;; - *) - echo "[tailscale-netns:${name}] waiting for Running (state=$state)" - ;; - esac - fi - last_state="$state" - sleep 1 - done - ''; + }; mkNetnsSetup = index: name: instanceCfg: let ns = "ts-${name}ns"; - hostIP = "10.201.${toString index}.1/24"; + hostAddr = "10.201.${toString index}.1"; + hostIP = "${hostAddr}/24"; nsIP = "10.201.${toString index}.2/24"; vethHost = "veth-ts-${name}"; vethNS = "veth-ts-${name}-ns"; + fallbackLines = concatMapStrings (n: " echo nameserver ${n}\n") config.networking.nameservers; in { "netns@ts-${name}" = { @@ -153,12 +40,42 @@ let set -euo pipefail ${pkgs.coreutils}/bin/mkdir -p /etc/netns/${ns} - # Build resolv.conf for the netns. Skip loopback addresses - # (systemd-resolved stub at 127.0.0.53 is not reachable from the netns). - NS_DNS="${if instanceCfg.dns != null then instanceCfg.dns else "1.1.1.1"}" { - ${pkgs.gawk}/bin/awk '$1 == "nameserver" && $2 !~ /^127\./ && $2 != "::1" {print}' /etc/resolv.conf - echo "nameserver $NS_DNS" + dns_found=0 + if ${pkgs.systemd}/bin/resolvectl dns 2>/dev/null; then + saved_ifs="$IFS" + IFS=$'\n' + for line in $(${pkgs.systemd}/bin/resolvectl dns 2>/dev/null); do + case "$line" in + *:*) + server=$(echo "$line" | ${pkgs.gnused}/bin/sed 's/^[^:]*: //') + case "$server" in + 127.*|::1|"") ;; + *) echo "nameserver $server"; dns_found=1 ;; + esac + ;; + esac + done + IFS="$saved_ifs" + fi + + if [ "$dns_found" -eq 0 ]; then + while IFS= read -r rline; do + case "$rline" in + nameserver*) + ip=$(echo "$rline" | ${pkgs.gnused}/bin/sed 's/^nameserver //') + case "$ip" in + 127.*|::1) ;; + *) echo "$rline"; dns_found=1 ;; + esac + ;; + esac + done < /etc/resolv.conf + fi + ${optionalString (fallbackLines != "") '' + if [ "$dns_found" -eq 0 ]; then + ${fallbackLines} fi + ''} } > /etc/netns/${ns}/resolv.conf ${pkgs.iproute2}/bin/ip netns add ${ns} @@ -171,6 +88,7 @@ let ${pkgs.iproute2}/bin/ip -n ${ns} addr add ${nsIP} dev ${vethNS} ${pkgs.iproute2}/bin/ip -n ${ns} link set ${vethNS} up ${pkgs.iproute2}/bin/ip -n ${ns} link set lo up + ${pkgs.iproute2}/bin/ip -n ${ns} route add default via ${hostAddr} ${pkgs.iptables}/bin/iptables -t nat -C POSTROUTING -s ${nsIP} -j MASQUERADE 2>/dev/null \ || ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -s ${nsIP} -j MASQUERADE @@ -231,10 +149,7 @@ in description = "Tailscale instances configured in separate network namespaces."; example = literalExpression '' { - work = { - authKeyFile = "/run/secrets/ts-work/authkey"; - acceptRoutes = true; - }; + work = {}; } ''; }; @@ -247,25 +162,12 @@ in index: elem: let name = elem.name; - instanceCfg = elem.value; - units = mkNetnsSetup index name instanceCfg; + units = mkNetnsSetup index name elem.value; in [ - units."netns@ts-${name}" - units."tailscaled-${name}" + { name = "netns@ts-${name}"; value = units."netns@ts-${name}"; } + { name = "tailscaled-${name}"; value = units."tailscaled-${name}"; } ] - ++ optional (instanceCfg.authKeyFile != null) { - "tailscaled-autoconnect-${name}" = { - description = "Tailscale autoconnect for instance ${name}"; - after = [ "tailscaled-${name}.service" ]; - requires = [ "tailscaled-${name}.service" ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "notify"; - }; - script = "${mkAutoconnectScript name instanceCfg}"; - }; - } ) (attrsToList cfg.instances) ) ); From c461f04c13f4b18df56bad45990e9a4450e27be3 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 25 Jul 2026 09:16:53 -0300 Subject: [PATCH 2/2] 2026-07-25 --- configuration.nix | 12 ++++++------ kernel/default.nix | 1 + microvm/sandbox.nix | 13 ++++++++++--- networking.nix | 1 + 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/configuration.nix b/configuration.nix index 1fd0b01..0ea2240 100644 --- a/configuration.nix +++ b/configuration.nix @@ -9,18 +9,18 @@ ... }: { + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ]; + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + sops.defaultSopsFile = ./secrets/home.yaml; sops.age.keyFile = "/.persist/root/.config/sops/age/keys.txt"; sops.secrets."root/ssh/desktop" = { path = "/root/.ssh/desktop"; mode = "0600"; }; - nix.settings.experimental-features = [ - "nix-command" - "flakes" - ]; - - nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; boot.initrd.systemd.enable = true; boot.loader.systemd-boot.enable = true; diff --git a/kernel/default.nix b/kernel/default.nix index 6edd722..61a0777 100644 --- a/kernel/default.nix +++ b/kernel/default.nix @@ -56,6 +56,7 @@ in keepEnv = true; persist = true; setEnv = [ + "HOME=/root" "EDITOR" "TERM" ]; diff --git a/microvm/sandbox.nix b/microvm/sandbox.nix index 3efba48..fa830af 100644 --- a/microvm/sandbox.nix +++ b/microvm/sandbox.nix @@ -26,6 +26,13 @@ in homeManagerModule ]; + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ]; + nix.nixPath = [ "nixpkgs=${pkgs.path}" ]; + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + sops.defaultSopsFile = ./secrets/sandbox.yaml; sops.age.keyFile = "/.persist/secrets/age-keys.txt"; sops.secrets."root-password-hash" = { @@ -187,17 +194,17 @@ in { mountPoint = "/var"; image = "var.img"; - size = 256; + size = 2 * 1024; } { mountPoint = "/nix/.rw-store"; image = "nix-store-overlay.img"; - size = 512; + size = 2 * 1024; } { mountPoint = "/.persist"; image = "persist.img"; - size = 1; + size = 2 * 1024; } ]; shares = [ diff --git a/networking.nix b/networking.nix index 410dbae..9e24f21 100644 --- a/networking.nix +++ b/networking.nix @@ -19,6 +19,7 @@ services.tailscale-netns = { enable = true; instances.work = { + extraDaemonFlags = [ "--accept-routes=false" ]; }; };