Scalpel: Minimally invasive secrets provisioning to generated config files

Run-time provisioning of secrets with sops-nix or agenix is a nice feature. However, it usually is problematic for services that require secrets to be provided in their config-file with no alternative means of receiving them. Forking the NixOS-module or even the upstream package to make it compatible with run-time provisioned secrets might be a lot of work and you suddenly have to maintain a fork, not ideal.

Scalpel is a set of tools and a workflow that provides minimally invasive secure secrets-provisioning to config files by using the recent extendModules feature of NixOS. The basic workflow is:

  1. Configure the service normally, put placeholders where your secrets go
  2. Create a transformator that will exchange the placeholders with the actual secrets and put the file in a ramdisk. Transformators run at activation time.
  3. Use extendModules to obtain the service config-file path from the nix-store (to be provided to transformator) and replace it with the ramdisk path

The goals are:

  • Provide high-level of security for services that require their secrets as part of config-files
  • No secrets at evaluation time, no danger of secrets ending up in the nix-store
  • Does not require a fork of the module or upstream project
  • Continue to reap benefits from module or upstream updates with minimal worry of breakage

There is more explanation and an example provided in the repository README.

This should be considered beta and a proof-of-concept. Comments towards security and pull-requests are welcome.


Hey polygon, extendModules author here :wave:

That’s an interesting application of the feature! I should note though that it has a significant evaluation overhead, as you’re evaluating part of a second system configuration. It’s a bit of a sledgehammer really, so I feel that a more subtle solution may be more appropriate for a tool called scalpel :wink:

Something like lib/modules: Add mkTransform by roberth · Pull Request #155903 · NixOS/nixpkgs · GitHub would qualify; a feature that lets a definition “peek below” its priority, to transform the value that it overrides.
I rejected it because modules shouldn’t force configurations to be more complex by relying on such a feature.

Ultimately I believe the best solution is to improve the modules so that we don’t need an extra solution like scalpel.

Philosophizing aside, scalpel exists and the extendModules-based solution can probably be improved.

I heard you like modules, so I put extendModules in your module args, so you can extend modules while you extend modules.

extendModules isn’t just available on the result of evalModules (and the NixOS function). You can also use it within a module, as it is available as a module argument.
This has the potential to remove the need for the user (e.g. flake) to wire up the extendModules call themselves.
It’s not a trivial change though, as you’ll have to apply the scalpel configuration in the “base” configuration rather than the extended one. This is necessary because the base configuration is the one that’s going to be the final one. So to make it work this way, you’ll need a mechanism that enables the scalpel module behavior by default, but disables it in the extended configuration.
Suppose the extended config sets scalpel.replace.enable = false, then you could have a module argument that’s defined as _module.args.mkForceScalpel = if config.scalpel.replace.enable then mkForce else x: x;.

A change like this could make change the interface to be just a regular module (despite doing inception-esque stuff), which seems like a nice improvement for users.

If you want to go full inception you could hook the extra configuration into the options using (evalModules { }).type. I don’t think users need to set anything inside the extended config though, but I may be wrong.

1 Like

Could be this a potential use of GitHub - divnix/POP: Pure Object Prototypes

Which part of the problem would you apply it to?

POP would supposedly be flexible enough that the module system could be rebased onto it, but until that’s been done, I don’t see a use case for it here where it is readily applicable. I’d like to be proven wrong though!