nixcfg/systems/x86_64-linux/mx/nextcloud-claude-bot/module.nix
Harald Hoyer d5967cf392 feat(nix): improve Nextcloud Claude Bot security and user setup
- Set `User` and `Group` for the bot service to enhance security and isolation.
- Added system user and group for `claude-bot` with defined home directory.
- Modified secrets ownership to align with the new bot user.
2026-02-03 16:14:21 +01:00

143 lines
3.9 KiB
Nix

{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.nextcloud-claude-bot;
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
fastapi
uvicorn
httpx
]);
botModule = pkgs.runCommand "nextcloud-claude-bot-module" {} ''
mkdir -p $out
cp ${./bot.py} $out/nextcloud_claude_bot.py
'';
in {
options.services.nextcloud-claude-bot = {
enable = mkEnableOption "Nextcloud Talk Claude Bot";
port = mkOption {
type = types.port;
default = 8085;
description = "Port for the webhook listener";
};
host = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Host to bind to";
};
nextcloudUrl = mkOption {
type = types.str;
example = "https://cloud.example.com";
description = "Base URL of your Nextcloud instance";
};
botSecretFile = mkOption {
type = types.path;
description = "Path to file containing the bot secret (shared with Nextcloud)";
};
claudePath = mkOption {
type = types.path;
default = "${pkgs.claude-code}/bin/claude";
description = "Path to claude CLI binary";
};
allowedUsers = mkOption {
type = types.listOf types.str;
default = [];
example = [ "harald" "admin" ];
description = "Nextcloud usernames allowed to talk to the bot (empty = all)";
};
maxTokens = mkOption {
type = types.int;
default = 4096;
description = "Max tokens for Claude response";
};
timeout = mkOption {
type = types.int;
default = 120;
description = "Timeout in seconds for Claude CLI";
};
systemPrompt = mkOption {
type = types.nullOr types.str;
default = null;
example = "Du bist ein hilfreicher Assistent.";
description = "Optional system prompt for Claude";
};
};
config = mkIf cfg.enable {
systemd.services.nextcloud-claude-bot = {
description = "Nextcloud Talk Claude Bot";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment = {
HOME = "/var/lib/nextcloud-claude-bot";
BOT_HOST = cfg.host;
BOT_PORT = toString cfg.port;
NEXTCLOUD_URL = cfg.nextcloudUrl;
CLAUDE_PATH = cfg.claudePath;
ALLOWED_USERS = concatStringsSep "," cfg.allowedUsers;
MAX_TOKENS = toString cfg.maxTokens;
TIMEOUT = toString cfg.timeout;
SYSTEM_PROMPT = cfg.systemPrompt or "";
PYTHONPATH = botModule;
};
serviceConfig = {
Type = "simple";
ExecStart = "${pythonEnv}/bin/uvicorn nextcloud_claude_bot:app --host ${cfg.host} --port ${toString cfg.port}";
Restart = "always";
RestartSec = 5;
User = "claude-bot";
Group = "claude-bot";
# Security hardening
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = "read-only";
PrivateTmp = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
MemoryDenyWriteExecute = false; # Python needs this
LockPersonality = true;
# Bot secret
LoadCredential = "bot-secret:${cfg.botSecretFile}";
# Claude CLI needs home for config
StateDirectory = "nextcloud-claude-bot";
};
};
users.users.claude-bot = {
isSystemUser = true;
group = "claude-bot";
home = "/var/lib/nextcloud-claude-bot";
};
users.groups.claude-bot = {};
# Nginx reverse proxy config (optional, if you want external access)
# services.nginx.virtualHosts."cloud.example.com".locations."/claude-bot/" = {
# proxyPass = "http://${cfg.host}:${toString cfg.port}/";
# };
};
}