From eebf3f61596570d663e60922039d6797dbadeec3 Mon Sep 17 00:00:00 2001 From: Kris Date: Sat, 14 Mar 2026 17:02:37 +0200 Subject: [PATCH] feat: add openclaw sandboxed user, sudo policy, fs restrictions, docker proxy, watchdog --- modules/openclaw-docker-env.nix | 6 +++ modules/openclaw-docker.nix | 32 +++++++++++++ modules/openclaw-fs.nix | 29 ++++++++++++ modules/openclaw-sudo.nix | 17 +++++++ modules/openclaw-user.nix | 15 ++++++ modules/openclaw-watchdog.nix | 82 +++++++++++++++++++++++++++++++++ 6 files changed, 181 insertions(+) create mode 100644 modules/openclaw-docker-env.nix create mode 100644 modules/openclaw-docker.nix create mode 100644 modules/openclaw-fs.nix create mode 100644 modules/openclaw-sudo.nix create mode 100644 modules/openclaw-user.nix create mode 100644 modules/openclaw-watchdog.nix diff --git a/modules/openclaw-docker-env.nix b/modules/openclaw-docker-env.nix new file mode 100644 index 0000000..23b18e9 --- /dev/null +++ b/modules/openclaw-docker-env.nix @@ -0,0 +1,6 @@ +{ ... }: +{ + environment.variables = { + DOCKER_HOST = "tcp://127.0.0.1:2375"; + }; +} diff --git a/modules/openclaw-docker.nix b/modules/openclaw-docker.nix new file mode 100644 index 0000000..e7d8a15 --- /dev/null +++ b/modules/openclaw-docker.nix @@ -0,0 +1,32 @@ +{ pkgs, ... }: +{ + virtualisation.oci-containers.containers.docker-socket-proxy = { + image = "tecnativa/docker-socket-proxy:latest"; + autoStart = true; + volumes = [ "/var/run/docker.sock:/var/run/docker.sock" ]; + environment = { + CONTAINERS = "1"; + IMAGES = "1"; + NETWORKS = "1"; + VOLUMES = "1"; + INFO = "1"; + POST = "1"; + BUILD = "1"; + COMMIT = "0"; + CONFIGS = "0"; + DISTRIBUTION = "0"; + EXEC = "0"; + GRPC = "0"; + PLUGINS = "0"; + SECRETS = "0"; + SERVICES = "0"; + SESSION = "0"; + SWARM = "0"; + SYSTEM = "0"; + TASKS = "0"; + AUTH = "0"; + ALLOW_RESTARTS = "1"; + }; + ports = [ "127.0.0.1:2375:2375" ]; + }; +} diff --git a/modules/openclaw-fs.nix b/modules/openclaw-fs.nix new file mode 100644 index 0000000..5573390 --- /dev/null +++ b/modules/openclaw-fs.nix @@ -0,0 +1,29 @@ +{ ... }: +{ + fileSystems = + let + bindRO = src: { + device = src; + fsType = "none"; + options = [ "bind" "ro" ]; + }; + bindHide = src: { + device = "tmpfs"; + fsType = "tmpfs"; + options = [ "size=0" "mode=000" ]; + }; + in + { + "/home/openclaw/private/AT Protocol" = bindHide "/private/AT Protocol"; + "/home/openclaw/private/cloudflared" = bindHide "/private/cloudflared"; + "/home/openclaw/private/vaultwarden" = bindHide "/private/vaultwarden"; + "/home/openclaw/protected" = bindHide "/protected"; + }; + + systemd.tmpfiles.rules = [ + "d /home/openclaw/private 0750 openclaw openclaw -" + "d /home/openclaw/protected 0000 root root -" + "f /home/openclaw/private/tangled.env 0000 root root -" + "f /home/openclaw/private/cloudflared.pem 0000 root root -" + ]; +} diff --git a/modules/openclaw-sudo.nix b/modules/openclaw-sudo.nix new file mode 100644 index 0000000..d6617d4 --- /dev/null +++ b/modules/openclaw-sudo.nix @@ -0,0 +1,17 @@ +{ + security.sudo.extraRules = [ + { + users = [ "openclaw" ]; + commands = [ + { + command = "/run/current-system/sw/bin/cat"; + options = [ "NOPASSWD" ]; + } + { + command = "/run/current-system/sw/bin/docker"; + options = [ "NOPASSWD" ]; + } + ]; + } + ]; +} diff --git a/modules/openclaw-user.nix b/modules/openclaw-user.nix new file mode 100644 index 0000000..1473b9d --- /dev/null +++ b/modules/openclaw-user.nix @@ -0,0 +1,15 @@ +{ pkgs, ... }: +{ + users.users.openclaw = { + isSystemUser = false; + isNormalUser = true; + home = "/home/openclaw"; + createHome = true; + group = "openclaw"; + extraGroups = [ "docker" ]; + shell = pkgs.bash; + description = "OpenClaw agent sandboxed user"; + }; + + users.groups.openclaw = { }; +} diff --git a/modules/openclaw-watchdog.nix b/modules/openclaw-watchdog.nix new file mode 100644 index 0000000..ccff3d3 --- /dev/null +++ b/modules/openclaw-watchdog.nix @@ -0,0 +1,82 @@ +{ pkgs, ... }: +{ + systemd.services.openclaw-watchdog = { + description = "Post-rebuild health watchdog"; + after = [ "network.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "/etc/openclaw/nixos-rollback.sh check"; + }; + onFailure = [ "nixos-rollback.service" ]; + }; + + systemd.services.nixos-rollback = { + description = "Autonomous NixOS rollback"; + serviceConfig = { + Type = "oneshot"; + ExecStart = "/etc/openclaw/nixos-rollback.sh rollback"; + }; + }; + + environment.etc."openclaw/nixos-rollback.sh" = { + mode = "0750"; + text = '' + #!/usr/bin/env bash + set -euo pipefail + + WEBHOOK="$(cat /run/secrets/discord-webhook 2>/dev/null || echo "")" + UNITS=("sshd" "docker" "bluesky-pds" "cloudflared") + HOSTNAME="$(hostname)" + + notify() { + [ -z "$WEBHOOK" ] && return + curl -s -X POST "$WEBHOOK" \ + -H "Content-Type: application/json" \ + -d "{\"content\": \"$1\"}" + } + + check_units() { + for unit in "''${UNITS[@]}"; do + if ! systemctl is-active --quiet "$unit"; then + return 1 + fi + done + return 0 + } + + check_ssh() { + timeout 5 bash -c 'echo > /dev/tcp/127.0.0.1/22' 2>/dev/null + } + + do_check() { + for i in $(seq 1 6); do + sleep 10 + if check_units && check_ssh; then + notify "**[$HOSTNAME] NixOS switch healthy** — all units OK after rebuild." + exit 0 + fi + done + exit 1 + } + + do_rollback() { + notify "**[$HOSTNAME] ROLLBACK TRIGGERED** — health check failed. Rolling back..." + if nixos-rebuild switch --rollback; then + sleep 15 + if check_units && check_ssh; then + notify "**[$HOSTNAME] Rollback successful** — previous generation restored." + else + notify "**[$HOSTNAME] URGENT — rollback also failed.** Manual intervention needed." + fi + else + notify "**[$HOSTNAME] URGENT — rollback command failed.** Manual intervention needed." + fi + } + + case "''${1:-check}" in + check) do_check ;; + rollback) do_rollback ;; + esac + ''; + }; +}