From 5f42e5d90b5a195fc1007390d950dddd4de42056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Tue, 28 Apr 2026 16:33:32 -0700 Subject: [PATCH] refactor(glyph): run obsidian services as shared system user Move obsidian-sync off the 'mu' user account onto a dedicated 'obsidian' system user (/var/lib/obsidian). obsidian-vault-mcp runs as the same user, so it has native read/write access to vault files without ACLs or permission hacks. Co-Authored-By: Claude Sonnet 4.6 --- hosts/glyph/services/default.nix | 12 +++- modules/nixos/llm/default.nix | 1 + modules/nixos/llm/obsidian-vault-mcp.nix | 73 ++++++++++++++++++++++++ modules/nixos/obsidian-sync.nix | 25 +++++--- 4 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 modules/nixos/llm/obsidian-vault-mcp.nix diff --git a/hosts/glyph/services/default.nix b/hosts/glyph/services/default.nix index 5848c3e2..f7ca4f8e 100644 --- a/hosts/glyph/services/default.nix +++ b/hosts/glyph/services/default.nix @@ -72,8 +72,8 @@ age.secrets.obsidian-auth-token = { file = ./../secrets/obsidian-auth-token.age; mode = "400"; - owner = "mu"; - group = "users"; + owner = "obsidian"; + group = "obsidian"; }; rc.obsidian-sync = { @@ -133,6 +133,10 @@ enable = true; authTokenFile = config.age.secrets.graphite-auth-token.path; }; + services.obsidian-vault-mcp = { + enable = true; + inherit (config.rc.obsidian-sync) vaultPath; + }; services.mcpjungle = { enable = true; servers.basic-memory = { @@ -155,6 +159,10 @@ url = "http://127.0.0.1:8094/mcp"; description = "Graphite CLI for stacked PRs and code review"; }; + servers.obsidian-vault = { + url = "http://127.0.0.1:8097/mcp"; + description = "Read and write files in the Obsidian vault"; + }; servers.context7 = { url = "https://mcp.context7.com/mcp"; description = "Up-to-date library documentation and code examples"; diff --git a/modules/nixos/llm/default.nix b/modules/nixos/llm/default.nix index 38428cec..9da488dc 100644 --- a/modules/nixos/llm/default.nix +++ b/modules/nixos/llm/default.nix @@ -6,5 +6,6 @@ ./kagi.nix ./mcp-nixos.nix ./mcpjungle.nix + ./obsidian-vault-mcp.nix ]; } diff --git a/modules/nixos/llm/obsidian-vault-mcp.nix b/modules/nixos/llm/obsidian-vault-mcp.nix new file mode 100644 index 00000000..b86ec5cc --- /dev/null +++ b/modules/nixos/llm/obsidian-vault-mcp.nix @@ -0,0 +1,73 @@ +{ + config, + pkgs, + lib, + ... +}: let + cfg = config.services.obsidian-vault-mcp; + + startScript = pkgs.writeShellScript "obsidian-vault-mcp-start" '' + exec ${lib.getExe pkgs.mcp-proxy} \ + --host ${cfg.host} \ + --port ${toString cfg.port} \ + --transport streamablehttp \ + -- ${lib.getExe pkgs.mcp-server-filesystem} ${lib.escapeShellArg cfg.vaultPath} + ''; +in { + options.services.obsidian-vault-mcp = { + enable = lib.mkEnableOption "Obsidian vault filesystem MCP server (stdio→HTTP bridge)"; + + port = lib.mkOption { + type = lib.types.port; + default = 8097; + description = "Port for the streamable HTTP transport."; + }; + + host = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "Address to bind the HTTP server to."; + }; + + vaultPath = lib.mkOption { + type = lib.types.str; + description = "Absolute path to the Obsidian vault directory."; + }; + + openFirewall = lib.mkEnableOption "opening firewall port for obsidian-vault-mcp"; + }; + + config = lib.mkIf cfg.enable { + systemd.services.obsidian-vault-mcp = { + description = "Obsidian Vault Filesystem MCP Server"; + after = ["network-online.target"]; + wants = ["network-online.target"]; + wantedBy = ["multi-user.target"]; + + serviceConfig = { + ExecStart = "${startScript}"; + # Runs as the shared obsidian user so it has full read/write access + # to vault files created by obsidian-sync. + User = "obsidian"; + Group = "obsidian"; + WorkingDirectory = "/var/lib/obsidian"; + Restart = "on-failure"; + RestartSec = 5; + + # Hardening + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "strict"; + ReadWritePaths = [cfg.vaultPath "/var/lib/obsidian"]; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictSUIDSGID = true; + }; + }; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [cfg.port]; + }; +} diff --git a/modules/nixos/obsidian-sync.nix b/modules/nixos/obsidian-sync.nix index bcaa406b..fadc5c76 100644 --- a/modules/nixos/obsidian-sync.nix +++ b/modules/nixos/obsidian-sync.nix @@ -2,18 +2,17 @@ config, lib, pkgs, - username, ... }: let cfg = config.rc.obsidian-sync; - homeDir = config.users.users.${username}.home; + stateDir = "/var/lib/obsidian"; in { options.rc.obsidian-sync = { enable = lib.mkEnableOption "Obsidian headless vault sync"; vaultPath = lib.mkOption { type = lib.types.str; - default = "${homeDir}/vault"; + default = "${stateDir}/vault"; description = "Local path to the vault directory."; }; @@ -24,9 +23,16 @@ in { }; config = lib.mkIf cfg.enable { + users.users.obsidian = { + isSystemUser = true; + group = "obsidian"; + home = stateDir; + }; + users.groups.obsidian = {}; + systemd.tmpfiles.rules = [ - "d ${cfg.vaultPath} 0755 ${username} users -" - "d ${homeDir}/.config/obsidian-headless 0700 ${username} users -" + "d ${cfg.vaultPath} 0755 obsidian obsidian -" + "d ${stateDir}/.config/obsidian-headless 0700 obsidian obsidian -" ]; systemd.services.obsidian-sync = { @@ -36,9 +42,10 @@ in { wants = ["network-online.target"]; serviceConfig = { - User = username; - Group = "users"; - WorkingDirectory = homeDir; + User = "obsidian"; + Group = "obsidian"; + WorkingDirectory = stateDir; + StateDirectory = "obsidian"; EnvironmentFile = cfg.authTokenFile; @@ -53,7 +60,7 @@ in { NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; - ReadWritePaths = [cfg.vaultPath "${homeDir}/.config/obsidian-headless"]; + ReadWritePaths = [cfg.vaultPath "${stateDir}/.config/obsidian-headless"]; }; }; };