-
-
Notifications
You must be signed in to change notification settings - Fork 156
feat(nixos): add NixOS module for isolated system user #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
jeanlucthumm
wants to merge
34
commits into
openclaw:main
Choose a base branch
from
jeanlucthumm:userseg
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
50ff5de
fix: resolve ~ paths in systemd service config
das-monki 006317d
fix: bundle extensions dir
das-monki d8fc64b
ci: auto-update version strings in update-pins.sh
das-monki b608a06
feat: add aarch64-linux support
das-monki e46f5a2
CLAUDE.md
jeanlucthumm 4742475
PR.md
jeanlucthumm 43c0bee
feat(nixos): add option definitions for system service module
jeanlucthumm e745cda
feat(nixos): add main module with systemd hardening
jeanlucthumm 9e022e4
feat(flake): export nixosModules.clawdbot
jeanlucthumm b9cfedd
test(nixos): add VM integration test for system service module
jeanlucthumm c9e262d
fix(nixos): remove hardcoded ReadOnlyPaths that may not exist
jeanlucthumm 4c2fef3
feat(nixos): add oauthCredentialsDir for Claude CLI OAuth support
jeanlucthumm 49dfb48
test(nixos): add OAuth bind-mount verification to VM test
jeanlucthumm f900fea
fix(nixos): allow AF_NETLINK for Node.js networkInterfaces()
jeanlucthumm d79442a
fix(nixos): default gateway.auth.mode to none for system service
jeanlucthumm 91bf1c9
refactor(nixos): don't default gateway.auth.mode, let users configure
jeanlucthumm e1adff7
feat(nixos): add gateway.auth options for upstream auth requirement
jeanlucthumm b6e50f3
fix(nixos): relax syscall filter for clipboard module
jeanlucthumm dc6f1b1
fix(nixos): relax syscall filter for Node.js compatibility
jeanlucthumm 2d1ce3a
fix(nixos): inherit top-level provider settings in instances
jeanlucthumm d6d8df9
fix(nixos): disable ProtectHome when OAuth credentials in /home
jeanlucthumm 7956567
refactor(nixos): replace oauthCredentialsDir with oauthTokenFile
jeanlucthumm 84ec091
docs: update PR.md with implementation status
jeanlucthumm 8d20c22
.gitignore
jeanlucthumm 3de8594
fix(test): remove obsolete oauthCredentialsDir test
jeanlucthumm 6ecb958
refactor(nixos): remove unimplemented documents/skills options
jeanlucthumm 5c804b7
feat(nixos): implement documents and skills support
jeanlucthumm a7c0547
refactor: remove unused config option from both modules
jeanlucthumm 34145ef
feat(nixos): add assertions for auth configuration
jeanlucthumm 0c2f6c1
refactor(nixos): remove duplicate instances option definition
jeanlucthumm 323de2d
docs(nixos): clarify CLAWDIS_* env var comment
jeanlucthumm f2ee825
docs: update PR.md with documents-skills.nix
jeanlucthumm cf52bef
docs: update clawdbot to moltbot references
jeanlucthumm efe698e
fix: update remaining clawdbot references to openclaw in documents-sk…
jeanlucthumm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| result | ||
| .claude/settings.local.json |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # AGENTS.md — nix-moltbot | ||
|
|
||
| Single source of truth for product direction: `README.md`. | ||
|
|
||
| Documentation policy: | ||
| - Keep the surface area small. | ||
| - Avoid duplicate "pointer‑only" files. | ||
| - Update `README.md` first, then adjust references. | ||
|
|
||
| Defaults: | ||
| - Nix‑first, no sudo. | ||
| - Declarative config only. | ||
| - Batteries‑included install is the baseline. | ||
| - Breaking changes are acceptable pre‑1.0.0 (move fast, keep docs accurate). | ||
| - NO INLINE SCRIPTS EVER. | ||
| - NEVER send any message (iMessage, email, SMS, etc.) without explicit user confirmation: | ||
| - Always show the full message text and ask: "I'm going to send this: <message>. Send? (y/n)" | ||
|
|
||
| Moltbot packaging: | ||
| - The gateway package must include Control UI assets (run `pnpm ui:build` in the Nix build). | ||
|
|
||
| Golden path for pins (yolo + manual bumps): | ||
| - Hourly GitHub Action **Yolo Update Pins** runs `scripts/update-pins.sh`, which: | ||
| - Picks latest upstream moltbot SHA with green non-Windows checks | ||
| - Rebuilds gateway to refresh `pnpmDepsHash` | ||
| - Regenerates `nix/generated/moltbot-config-options.nix` from upstream schema | ||
| - Updates app pin/hash, commits, rebases, pushes to `main` | ||
| - Manual bump (rare): `GH_TOKEN=... scripts/update-pins.sh` (same steps as above). Use only if yolo is blocked. | ||
| - To verify freshness: `git pull --ff-only` and check `nix/sources/moltbot-source.nix` vs `git ls-remote https://github.com/moltbot/moltbot.git refs/heads/main`. | ||
| - If upstream is moving fast and tighter freshness is needed, trigger yolo manually: `gh workflow run "Yolo Update Pins"`. | ||
|
|
||
| Philosophy: | ||
|
|
||
| The Zen of ~~Python~~ Moltbot, ~~by~~ shamelessly stolen from Tim Peters | ||
|
|
||
| Beautiful is better than ugly. | ||
| Explicit is better than implicit. | ||
| Simple is better than complex. | ||
| Complex is better than complicated. | ||
| Flat is better than nested. | ||
| Sparse is better than dense. | ||
| Readability counts. | ||
| Special cases aren't special enough to break the rules. | ||
| Although practicality beats purity. | ||
| Errors should never pass silently. | ||
| Unless explicitly silenced. | ||
| In the face of ambiguity, refuse the temptation to guess. | ||
| There should be one-- and preferably only one --obvious way to do it. | ||
| Although that way may not be obvious at first unless you're Dutch. | ||
| Now is better than never. | ||
| Although never is often better than *right* now. | ||
| If the implementation is hard to explain, it's a bad idea. | ||
| If the implementation is easy to explain, it may be a good idea. | ||
| Namespaces are one honking great idea -- let's do more of those! | ||
|
|
||
| Nix file policy: | ||
| - No inline file contents in Nix code, ever. | ||
| - Always reference explicit file paths (keep docs as real files in the repo). | ||
| - No inline scripts in Nix code, ever (use repo scripts and reference their paths). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| # PR: Add NixOS module for isolated system user | ||
|
|
||
| ## Issue | ||
|
|
||
| https://github.com/moltbot/nix-moltbot/issues/22 | ||
|
|
||
| Upstream issue: https://github.com/moltbot/moltbot/issues/2341 | ||
|
|
||
| ## Goal | ||
|
|
||
| Add a NixOS module (`nixosModules.moltbot`) that runs the gateway as an isolated system user instead of the personal user account. | ||
|
|
||
| ## Security Motivation | ||
|
|
||
| Currently the gateway runs as the user's personal account, giving the LLM full access to SSH keys, credentials, personal files, etc. Running as a dedicated locked-down user contains the blast radius if the LLM is compromised. | ||
|
|
||
| ## Status: Working | ||
|
|
||
| Tested and deployed successfully. The service runs with full systemd hardening. | ||
|
|
||
| ## Implementation | ||
|
|
||
| ### Files | ||
|
|
||
| - `nix/modules/nixos/moltbot.nix` - Main module | ||
| - `nix/modules/nixos/options.nix` - Option definitions | ||
| - `nix/modules/nixos/documents-skills.nix` - Documents and skills installation | ||
|
|
||
| ### Features | ||
|
|
||
| - Dedicated `moltbot` system user with minimal privileges | ||
| - System-level systemd service with hardening: | ||
| - `ProtectHome=true` | ||
| - `ProtectSystem=strict` | ||
| - `PrivateTmp=true`, `PrivateDevices=true` | ||
| - `NoNewPrivileges=true` | ||
| - `CapabilityBoundingSet=""` (no capabilities) | ||
| - `SystemCallFilter=@system-service` | ||
| - `RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK` | ||
| - Full namespace/kernel protection | ||
| - Multi-instance support via `instances.<name>` | ||
| - Credential loading from files at runtime (wrapper script) | ||
|
|
||
| ### Credential Management | ||
|
|
||
| Uses `providers.anthropic.oauthTokenFile` - a long-lived token from `claude setup-token`. | ||
|
|
||
| ```nix | ||
| services.moltbot = { | ||
| enable = true; | ||
| providers.anthropic.oauthTokenFile = config.age.secrets.moltbot-token.path; | ||
| providers.telegram = { | ||
| enable = true; | ||
| botTokenFile = config.age.secrets.telegram-token.path; | ||
| allowFrom = [ 12345678 ]; | ||
| }; | ||
| }; | ||
| ``` | ||
|
|
||
| The deprecated `anthropic:claude-cli` profile (which tried to sync OAuth from `~/.claude/`) was not implemented - upstream deprecated it in favor of `setup-token` flow. | ||
|
|
||
| ### Gateway Auth | ||
|
|
||
| Upstream now requires gateway authentication. Options: | ||
|
|
||
| - `gateway.auth.tokenFile` / `gateway.auth.passwordFile` - load from file | ||
| - `instances.<name>.configOverrides.gateway.auth` - inline in config (for non-sensitive cases) | ||
|
|
||
| ## Notes | ||
|
|
||
| - Node.js JIT requires `SystemCallFilter=@system-service` (can't use `~@privileged`) | ||
| - `AF_NETLINK` needed for `os.networkInterfaces()` in Node.js |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| # NixOS VM integration test for openclaw module | ||
| # | ||
| # Tests that: | ||
| # 1. Service starts successfully | ||
| # 2. User/group are created | ||
| # 3. State directories exist with correct permissions | ||
| # 4. Hardening prevents reading /home (ProtectHome=true) | ||
| # | ||
| # Run with: nix build .#checks.x86_64-linux.nixos-module -L | ||
| # Or interactively: nix build .#checks.x86_64-linux.nixos-module.driverInteractive && ./result/bin/nixos-test-driver | ||
|
|
||
| { pkgs, openclawModule }: | ||
|
|
||
| pkgs.testers.nixosTest { | ||
| name = "openclaw-nixos-module"; | ||
|
|
||
| nodes.server = { pkgs, ... }: { | ||
| imports = [ openclawModule ]; | ||
|
|
||
| # Use the gateway-only package to avoid toolset issues | ||
| services.openclaw = { | ||
| enable = true; | ||
| package = pkgs.openclaw-gateway; | ||
| # Dummy token for testing - service won't be fully functional but will start | ||
| providers.anthropic.oauthTokenFile = "/run/openclaw-test-token"; | ||
| gateway.auth.tokenFile = "/run/openclaw-gateway-token"; | ||
| }; | ||
|
|
||
| # Create dummy token files for testing | ||
| system.activationScripts.openclawTestTokens = '' | ||
| echo "test-oauth-token" > /run/openclaw-test-token | ||
| echo "test-gateway-token" > /run/openclaw-gateway-token | ||
| chmod 600 /run/openclaw-test-token /run/openclaw-gateway-token | ||
| ''; | ||
|
|
||
| # Create a test file in /home to verify hardening | ||
| users.users.testuser = { | ||
| isNormalUser = true; | ||
| home = "/home/testuser"; | ||
| }; | ||
|
|
||
| system.activationScripts.testSecrets = '' | ||
| mkdir -p /home/testuser | ||
| echo "secret-data" > /home/testuser/secret.txt | ||
| chown testuser:users /home/testuser/secret.txt | ||
| chmod 600 /home/testuser/secret.txt | ||
| ''; | ||
| }; | ||
|
|
||
| testScript = '' | ||
| start_all() | ||
|
|
||
| with subtest("Service starts"): | ||
| server.wait_for_unit("openclaw-gateway.service", timeout=60) | ||
|
|
||
| with subtest("User and group exist"): | ||
| server.succeed("id openclaw") | ||
| server.succeed("getent group openclaw") | ||
|
|
||
| with subtest("State directories exist with correct ownership"): | ||
| server.succeed("test -d /var/lib/openclaw") | ||
| server.succeed("test -d /var/lib/openclaw/workspace") | ||
| server.succeed("stat -c '%U:%G' /var/lib/openclaw | grep -q 'openclaw:openclaw'") | ||
|
|
||
| with subtest("Config file exists"): | ||
| server.succeed("test -f /var/lib/openclaw/openclaw.json") | ||
|
|
||
| with subtest("Hardening: cannot read /home"): | ||
| # The service should not be able to read files in /home due to ProtectHome=true | ||
| # We test this by checking the service's view of the filesystem | ||
| server.succeed( | ||
| "nsenter -t $(systemctl show -p MainPID --value openclaw-gateway.service) -m " | ||
| "sh -c 'test ! -e /home/testuser/secret.txt' || " | ||
| "echo 'ProtectHome working: /home is hidden from service'" | ||
| ) | ||
|
|
||
| with subtest("Service is running as openclaw user"): | ||
| server.succeed( | ||
| "ps -o user= -p $(systemctl show -p MainPID --value openclaw-gateway.service) | grep -q openclaw" | ||
| ) | ||
|
|
||
| # Note: We don't test the gateway HTTP response because we don't have an API key | ||
| # The service will be running but not fully functional without credentials | ||
|
|
||
| server.log(server.succeed("systemctl status openclaw-gateway.service")) | ||
| server.log(server.succeed("journalctl -u openclaw-gateway.service --no-pager | tail -50")) | ||
| ''; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit odd that the name of the file on-disk is
CLAUDE.mdwhile the name of the file in it's own heading is "AGENTS.md"There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just copy pasted it so I can use it with Claude Code.
I haven't seen a good way to have both coexist in a repo, might delete before merge but open to suggestions.