From cc369e63856c1e8338cfe34eff73500e000cb3a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torjus=20H=C3=A5kestad?= Date: Tue, 3 Feb 2026 19:13:20 +0100 Subject: [PATCH] security: add connectionStringFile option for PostgreSQL secrets The connectionString option stores credentials in the world-readable Nix store. This adds connectionStringFile as an alternative that reads the connection string from a file at runtime, compatible with secret management tools like agenix or sops-nix. Changes: - Add database.connectionStringFile option (mutually exclusive with connectionString) - Read connection string from file at service start when configured - Add warning to connectionString documentation about Nix store visibility - Update README with examples for both approaches Co-Authored-By: Claude Opus 4.5 --- README.md | 24 +++++++++++++++++- nix/module.nix | 67 ++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 80 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 02dd4b7..05e2601 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,8 @@ A NixOS module is provided for running the MCP server as a systemd service. | `package` | package | from flake | Package to use | | `database.type` | enum | `"sqlite"` | `"sqlite"` or `"postgres"` | | `database.name` | string | `"nixos-options.db"` | SQLite database filename | -| `database.connectionString` | string | `""` | PostgreSQL connection URL | +| `database.connectionString` | string | `""` | PostgreSQL connection URL (stored in Nix store) | +| `database.connectionStringFile` | path | `null` | Path to file with PostgreSQL connection URL (recommended for secrets) | | `indexOnStart` | list of string | `[]` | Revisions to index on service start | | `user` | string | `"nixos-options-mcp"` | User to run the service as | | `group` | string | `"nixos-options-mcp"` | Group to run the service as | @@ -189,6 +190,8 @@ A NixOS module is provided for running the MCP server as a systemd service. ### PostgreSQL Example +Using `connectionString` (stored in Nix store - suitable for testing or non-sensitive setups): + ```nix { services.nixos-options-mcp = { @@ -202,6 +205,25 @@ A NixOS module is provided for running the MCP server as a systemd service. } ``` +Using `connectionStringFile` (recommended for production with sensitive credentials): + +```nix +{ + services.nixos-options-mcp = { + enable = true; + database = { + type = "postgres"; + # File contains: postgres://user:secret@localhost/nixos_options?sslmode=disable + connectionStringFile = "/run/secrets/nixos-options-db"; + }; + indexOnStart = [ "nixos-unstable" ]; + }; + + # Example with agenix or sops-nix for secret management + # age.secrets.nixos-options-db.file = ./secrets/nixos-options-db.age; +} +``` + ## Development ```bash diff --git a/nix/module.nix b/nix/module.nix index 9d1e9f2..0e98369 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -3,8 +3,14 @@ let cfg = config.services.nixos-options-mcp; + # Determine database URL based on configuration + # For postgres with connectionStringFile, the URL is set at runtime via script + useConnectionStringFile = cfg.database.type == "postgres" && cfg.database.connectionStringFile != null; + databaseUrl = if cfg.database.type == "sqlite" then "sqlite://${cfg.dataDir}/${cfg.database.name}" + else if useConnectionStringFile + then "" # Will be set at runtime from file else cfg.database.connectionString; in { @@ -50,8 +56,26 @@ in description = '' PostgreSQL connection string (when using postgres backend). Example: "postgres://user:password@localhost/nixos_options?sslmode=disable" + + WARNING: This value will be stored in the Nix store, which is world-readable. + For production use with sensitive credentials, use connectionStringFile instead. ''; }; + + connectionStringFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + Path to a file containing the PostgreSQL connection string. + The file should contain just the connection string, e.g.: + postgres://user:password@localhost/nixos_options?sslmode=disable + + This is the recommended way to configure PostgreSQL credentials + as the file is not stored in the world-readable Nix store. + The file must be readable by the service user. + ''; + example = "/run/secrets/nixos-options-mcp-db"; + }; }; indexOnStart = lib.mkOption { @@ -78,8 +102,14 @@ in config = lib.mkIf cfg.enable { assertions = [ { - assertion = cfg.database.type == "sqlite" || cfg.database.connectionString != ""; - message = "services.nixos-options-mcp.database.connectionString must be set when using postgres backend"; + assertion = cfg.database.type == "sqlite" + || cfg.database.connectionString != "" + || cfg.database.connectionStringFile != null; + message = "services.nixos-options-mcp.database: when using postgres backend, either connectionString or connectionStringFile must be set"; + } + { + assertion = cfg.database.connectionString == "" || cfg.database.connectionStringFile == null; + message = "services.nixos-options-mcp.database: connectionString and connectionStringFile are mutually exclusive"; } ]; @@ -102,22 +132,39 @@ in after = [ "network.target" ] ++ lib.optional (cfg.database.type == "postgres") "postgresql.service"; - environment = { + environment = lib.mkIf (!useConnectionStringFile) { NIXOS_OPTIONS_DATABASE = databaseUrl; }; - preStart = lib.mkIf (cfg.indexOnStart != [ ]) '' - ${lib.concatMapStringsSep "\n" (rev: '' - echo "Indexing revision: ${rev}" - ${cfg.package}/bin/nixos-options index "${rev}" || true - '') cfg.indexOnStart} - ''; + path = [ cfg.package ]; + + script = let + indexCommands = lib.optionalString (cfg.indexOnStart != []) '' + ${lib.concatMapStringsSep "\n" (rev: '' + echo "Indexing revision: ${rev}" + nixos-options index "${rev}" || true + '') cfg.indexOnStart} + ''; + in + if useConnectionStringFile then '' + # Read database connection string from file + if [ ! -f "${cfg.database.connectionStringFile}" ]; then + echo "Error: connectionStringFile not found: ${cfg.database.connectionStringFile}" >&2 + exit 1 + fi + export NIXOS_OPTIONS_DATABASE="$(cat "${cfg.database.connectionStringFile}")" + + ${indexCommands} + exec nixos-options serve + '' else '' + ${indexCommands} + exec nixos-options serve + ''; serviceConfig = { Type = "simple"; User = cfg.user; Group = cfg.group; - ExecStart = "${cfg.package}/bin/nixos-options serve"; Restart = "on-failure"; RestartSec = "5s";