{"id":13781746,"url":"https://github.com/polygon/scalpel","last_synced_at":"2025-06-23T19:08:32.581Z","repository":{"id":37471402,"uuid":"504685467","full_name":"polygon/scalpel","owner":"polygon","description":"Minimally invasive safe secret provisioning to Nix-generated service config files","archived":false,"fork":false,"pushed_at":"2022-06-19T12:22:43.000Z","size":19,"stargazers_count":123,"open_issues_count":6,"forks_count":7,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-05-23T11:30:35.521Z","etag":null,"topics":["age","agenix","flake","flakes","nix","nixos","scalpel","secrets-management","sops","sops-nix"],"latest_commit_sha":null,"homepage":"","language":"Nix","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/polygon.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-06-17T22:15:32.000Z","updated_at":"2025-05-16T12:43:50.000Z","dependencies_parsed_at":"2022-07-27T02:32:12.834Z","dependency_job_id":null,"html_url":"https://github.com/polygon/scalpel","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/polygon/scalpel","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/polygon%2Fscalpel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/polygon%2Fscalpel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/polygon%2Fscalpel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/polygon%2Fscalpel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/polygon","download_url":"https://codeload.github.com/polygon/scalpel/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/polygon%2Fscalpel/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261539316,"owners_count":23174136,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["age","agenix","flake","flakes","nix","nixos","scalpel","secrets-management","sops","sops-nix"],"created_at":"2024-08-03T18:01:28.907Z","updated_at":"2025-06-23T19:08:27.555Z","avatar_url":"https://github.com/polygon.png","language":"Nix","funding_links":[],"categories":["Integrations"],"sub_categories":[],"readme":"# Scalpel\n\nMinimally invasive safe secret provisioning to Nix-generated service config files.\n\n## The issue\n\nNixOS has some fairly nice secrets provisioning with packages like [sops-nix](https://github.com/Mic92/sops-nix/) or [agenix](https://github.com/ryantm/agenix). Secrets are decrypted at activation time and will not end up in your store where they may be accessible to anyone.\n\nUnfortunately, some services require secrets in their config files and don't support receiving secrets by other means, e.g., password files or environment variables. This could be solved by forking the module for the service and making it compatible, but that may require continuous effort to keep up with upstream changes. Submitting changes upstream to enhance the configuration possibilities is always a good idea but may not be viable for various reasons.\n\nScalpel provides tooling and a workflow based on `extendModules` to safely provision secrets to config files and then inject them into existing modules without having to fork them altogether.\n\n## Prerequisites\n\nYou should already have secrets provisioning set up using, e.g., [sops-nix](https://github.com/Mic92/sops-nix/) or [agenix](https://github.com/ryantm/agenix). Please refer to these projects to get going.\n\n## Interlude - `extendModules`\n\n`extendModules` is a fairly recent feature of NixOS. I'll leave the exact explanation to someone more knowledgeable in the guts of the Nix module system, but the way we are using it here is:\n\nGiven a NixOS system configuration `sys = nixpkgs.lib.nixosSystem { ... }` we can derive a new configuration from it by calling `extendModules`. The interesting part is that we can now use `sys` and all the values inside of it in the new modules, e.g.:\n\n```\n    newsys = sys.extendModules {\n        modules = [ ... ];\n        specialArgs = { prev = sys; };\n    };\n```\n\nWhat makes this so interesting is that you can replace a value in a module while taking reference to its previous value. Something that would previously end you up in an infinite recursion:\n\n```\n    environment.etc.\"test.cfg\".text = ''${prev.config.environment.etc.\"test.cfg\".text} more text here afterwards'';\n```\n\nThis can be used to extract the names of configuration files from systemd service configurations and later inject different names back into them.\n\n## Usage example\n\nIn the example, we will securely provision bridge-passwords for Mosquitto.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e1. Create config with placeholder secrets\u003c/b\u003e\u003c/summary\u003e\n\nCreate your Mosquitto config as usual. But use placeholders sandwiched between `!!` to name your secrets.\n\n```\n  services.mosquitto = {\n    enable = true;\n    listeners = [\n      {\n        address = \"127.0.0.1\";\n      }\n    ];\n\n    bridges.br1 = {\n      addresses = [ { address = \"127.0.0.2\"; } ];\n      topics = [ \"# in\" ];\n      settings = {\n        remote_password = \"!!BR1_PASSWORD!!\";\n      };\n    };\n\n    bridges.br2 = {\n      addresses = [ { address = \"127.0.0.3\"; } ];\n      topics = [ \"# in\" ];\n      settings = {\n        remote_password = \"!!BR2_PASSWORD!!\";\n      };\n    };\n  };\n```\n\nAlso, you will configure your favorite secrets provisioning tool here to ensure that the secrets are later available at runtime:\n\n```\n  sops.secrets.br1passwd = {};\n  sops.secrets.br2passwd = {};\n```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e2. Create a derived system to add secret-provisioning module\u003c/b\u003e\u003c/summary\u003e\n\n```\n  nixosConfigurations = let\n    base_sys = nixpkgs.lib.nixosSystem {\n      system = \"x86_64-linux\";\n      modules = [\n        sops-nix.nixosModules.sops\n        ./example/system.nix\n      ];\n    };\n  in {\n    exampleContainer = base_sys.extendModules {\n      modules = [ \n        self.nixosModules.scalpel\n        ./example/secrets.nix \n      ];\n      specialArgs = { prev = base_sys; };\n    };\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e3. Write transformation rules for config file, replace service config\u003c/b\u003e\u003c/summary\u003e\n\nThis is the part that is specific to each service. You will need to do some investigation to figure out how the configuration is passed to the service. Firstly, extract the path of the generated config file:\n\n```\nlet\n  start = \"${prev.config.systemd.services.mosquitto.serviceConfig.ExecStart}\";\n  mosquitto_cfgfile = builtins.head (builtins.match \".*-c ([^[:space:]]+)\" \"${start}\");\nin\n  (...)\n```\n\nNow, create a transformator to replace the secret placeholders in this file:\n\n```\n  scalpel.trafos.\"mosquitto.conf\" = {\n    source = mosquitto_cfgfile;\n    matchers.\"BR1_PASSWORD\".secret = config.sops.secrets.br1passwd.path;\n    matchers.\"BR2_PASSWORD\".secret = config.sops.secrets.br2passwd.path;\n    owner = \"mosquitto\";\n    group = \"mosquitto\";\n    mode = \"0440\";\n  };\n```\n\nFinally, replace the configuraton file with the newly created one:\n\n```\n  systemd.services.mosquitto.serviceConfig.ExecStart = lib.mkForce (\n    builtins.replaceStrings [ \"${mosquitto_cfgfile}\" ] [ \"${config.scalpel.trafos.\"mosquitto.conf\".destination} \"] \"${start}\"\n  );\n```\n\u003c/details\u003e\n\nIn this example, we only modified `systemd.services.mosquitto.serviceConfig.ExecStart` without forking the original service at all. This makes the change very minimally invasive and this config should remain compatible to most changes in the module of the service. The full example is provided in this flake as well.\n\n## Run the example as a NixOS container\n\nWARNING: THIS CONTAINER USES PUBLICALLY KNOWN PRIVATE KEYS. DO NOT USE THEM IN YOUR DEPLOYMENTS. EVER.\n\nTo quickly test the example, you can run it as a NixOS container after cloning the Flake.\n\n```\nsudo nixos-container create em --flake .#exampleContainer\nsudo nixos-container start em\nsudo machinectl shell em\n```\n\nInside the container, we can see the changes in action:\n\n```\n$ systemctl cat mosquitto | grep ExecStart\nExecStart=/nix/store/jd00fshpzdc8mm1gqf2x8s7pkb8yb8nj-mosquitto-2.0.14/bin/mosquitto -c /run/scalpel/mosquitto.conf\n\n$ ls -la /run/scalpel/\n-r--r-----  1 mosquitto mosquitto 373 Jun 18 17:10 mosquitto.conf\n\n$ cat /run/scalpel/mosquitto.conf\n[...]\nconnection br1\naddresses 127.0.0.2:1883\ntopic # in\nremote_password secretbridge1password\nconnection br2\naddresses 127.0.0.3:1883\ntopic # in\nremote_password moresecretbridge2\n```\n\n## Beta Warning\n\nThis module should be considered a Proof of Concept. It works, but I am sure that there are possible improvements security wise. Use it at your own risk. On another note, I am very happy to receive comments and pull requests for improvements.\n\n## Acknowledgements\n\nThanks to the creators of [sops-nix](https://github.com/Mic92/sops-nix/) and [agenix](https://github.com/ryantm/agenix) for their fantastic work and sending me down this rabbit hole. A lot of the Scalpel module system was ~~blatantly ripped off~~ inspired by the modules provided from these projects.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpolygon%2Fscalpel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpolygon%2Fscalpel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpolygon%2Fscalpel/lists"}