Code Monkey home page Code Monkey logo

scalpel's Introduction

Scalpel

Minimally invasive safe secret provisioning to Nix-generated service config files.

The issue

NixOS has some fairly nice secrets provisioning with packages like sops-nix or agenix. Secrets are decrypted at activation time and will not end up in your store where they may be accessible to anyone.

Unfortunately, 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.

Scalpel 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.

Prerequisites

You should already have secrets provisioning set up using, e.g., sops-nix or agenix. Please refer to these projects to get going.

Interlude - extendModules

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:

Given 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.:

    newsys = sys.extendModules {
        modules = [ ... ];
        specialArgs = { prev = sys; };
    };

What 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:

    environment.etc."test.cfg".text = ''${prev.config.environment.etc."test.cfg".text} more text here afterwards'';

This can be used to extract the names of configuration files from systemd service configurations and later inject different names back into them.

Usage example

In the example, we will securely provision bridge-passwords for Mosquitto.

1. Create config with placeholder secrets

Create your Mosquitto config as usual. But use placeholders sandwiched between !! to name your secrets.

  services.mosquitto = {
    enable = true;
    listeners = [
      {
        address = "127.0.0.1";
      }
    ];

    bridges.br1 = {
      addresses = [ { address = "127.0.0.2"; } ];
      topics = [ "# in" ];
      settings = {
        remote_password = "!!BR1_PASSWORD!!";
      };
    };

    bridges.br2 = {
      addresses = [ { address = "127.0.0.3"; } ];
      topics = [ "# in" ];
      settings = {
        remote_password = "!!BR2_PASSWORD!!";
      };
    };
  };

Also, you will configure your favorite secrets provisioning tool here to ensure that the secrets are later available at runtime:

  sops.secrets.br1passwd = {};
  sops.secrets.br2passwd = {};
2. Create a derived system to add secret-provisioning module
  nixosConfigurations = let
    base_sys = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        sops-nix.nixosModules.sops
        ./example/system.nix
      ];
    };
  in {
    exampleContainer = base_sys.extendModules {
      modules = [ 
        self.nixosModules.scalpel
        ./example/secrets.nix 
      ];
      specialArgs = { prev = base_sys; };
    };
3. Write transformation rules for config file, replace service config

This 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:

let
  start = "${prev.config.systemd.services.mosquitto.serviceConfig.ExecStart}";
  mosquitto_cfgfile = builtins.head (builtins.match ".*-c ([^[:space:]]+)" "${start}");
in
  (...)

Now, create a transformator to replace the secret placeholders in this file:

  scalpel.trafos."mosquitto.conf" = {
    source = mosquitto_cfgfile;
    matchers."BR1_PASSWORD".secret = config.sops.secrets.br1passwd.path;
    matchers."BR2_PASSWORD".secret = config.sops.secrets.br2passwd.path;
    owner = "mosquitto";
    group = "mosquitto";
    mode = "0440";
  };

Finally, replace the configuraton file with the newly created one:

  systemd.services.mosquitto.serviceConfig.ExecStart = lib.mkForce (
    builtins.replaceStrings [ "${mosquitto_cfgfile}" ] [ "${config.scalpel.trafos."mosquitto.conf".destination} "] "${start}"
  );

In 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.

Run the example as a NixOS container

WARNING: THIS CONTAINER USES PUBLICALLY KNOWN PRIVATE KEYS. DO NOT USE THEM IN YOUR DEPLOYMENTS. EVER.

To quickly test the example, you can run it as a NixOS container after cloning the Flake.

sudo nixos-container create em --flake .#exampleContainer
sudo nixos-container start em
sudo machinectl shell em

Inside the container, we can see the changes in action:

$ systemctl cat mosquitto | grep ExecStart
ExecStart=/nix/store/jd00fshpzdc8mm1gqf2x8s7pkb8yb8nj-mosquitto-2.0.14/bin/mosquitto -c /run/scalpel/mosquitto.conf

$ ls -la /run/scalpel/
-r--r-----  1 mosquitto mosquitto 373 Jun 18 17:10 mosquitto.conf

$ cat /run/scalpel/mosquitto.conf
[...]
connection br1
addresses 127.0.0.2:1883
topic # in
remote_password secretbridge1password
connection br2
addresses 127.0.0.3:1883
topic # in
remote_password moresecretbridge2

Beta Warning

This 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.

Acknowledgements

Thanks to the creators of sops-nix and 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.

scalpel's People

Contributors

polygon avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

scalpel's Issues

BUG: `rm: refusing to remove '.' or '..' directory: skipping '/run/scalpel/.`

Hey, thank you very much for providing a way to customize configuration files that require some sops-managed passwords!
This is exactly what I've been looking for! ❤️
Though, I am having some issues and am relatively new to NixOS so sorry for not being able to provide a minimal example.

  1. When I execute the container there is no issue
  2. But when I call it via my flake configuration, I get the following issues:
rm: refusing to remove '.' or '..' directory: skipping '/run/scalpel/.`
rm: refusing to remove '.' or '..' directory: skipping '/run/scalpel/..`
Activation script snippet 'scalpelCreateStore' failed (1)

Looking at the code, the issue seems to come from

config = mkIf (cfg.trafos != { }) {
system.activationScripts.scalpelCreateStore = {
text = ''
echo "[scalpel] Ensuring existance of ${cfg.secretsDir}"
mkdir -p ${cfg.secretsDir}
grep -q "${cfg.secretsDir} ramfs" /proc/mounts || mount -t ramfs none "${cfg.secretsDir}" -o nodev,nosuid,mode=0751
echo "[scalpel] Clearing old secrets from ${cfg.secretsDir}"
rm -rf ${cfg.secretsDir}/{*,.*}
'';
deps = [ "specialfs" ];
};

Where the shell is safeguarding recursively deleting . and ..

I've cloned this repository and changed it to:

system.activationScripts.scalpelCreateStore = { 
       text = '' 
         echo "[scalpel] Ensuring existance of ${cfg.secretsDir}" 
         mkdir -p ${cfg.secretsDir} 
         grep -q "${cfg.secretsDir} ramfs" /proc/mounts || mount -t ramfs none "${cfg.secretsDir}" -o nodev,nosuid,mode=0751 
  
         echo "[scalpel] Clearing old secrets from ${cfg.secretsDir}" 
         find . -name . -o -prune -exec rm -rf -- {} +
       ''; 
       deps = [ "specialfs" ]; 
     }; 

With inspiration from: https://unix.stackexchange.com/a/77313

Which allowed me to run the flake without any issues. :)
I would be happy to open a PR if you think this change would fix it.

How to implement in Nix Flakes?

Hey I'm trying to add scalpel into my dotfiles config but I'm unsure how to implement the extendModules hook into my particular flake, which is based on Misterio77's start config. This is what my flake.nix looks like below

{
  # BASED ON https://github.com/Misterio77/nix-config/
  description = "My NixOS Configurations for multiple machines";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

    hardware.url = "github:nixos/nixos-hardware";
    impermanence.url = "github:nix-community/impermanence";
    nix-colors.url = "github:misterio77/nix-colors";

    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    nh = {
      url = "github:viperml/nh";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    firefox-addons = {
      url = "gitlab:rycee/nur-expressions?dir=pkgs/firefox-addons";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    nixpkgs-howdy.url = "github:fufexan/nixpkgs/howdy";

  };

  outputs = { self, nixpkgs, home-manager, ... }@inputs:
    let
      inherit (self) outputs;
      lib = nixpkgs.lib // home-manager.lib;
      # Supported systems for your flake packages, shell, etc.
      systems = [
        "aarch64-linux"
        "i686-linux"
        "x86_64-linux"
        "aarch64-darwin"
        "x86_64-darwin"
      ];
      # This is a function that generates an attribute by calling a function you pass to it, with each system as an argument
      forEachSystem = f: lib.genAttrs systems (sys: f pkgsFor.${sys});
      pkgsFor = nixpkgs.legacyPackages;
    in {
      inherit lib;
      # Reusable nixos modules you might want to export
      # These are usually stuff you would upstream into nixpkgs
      nixosModules = import ./modules/nixos;
      # Reusable home-manager modules you might want to export
      # These are usually stuff you would upstream into home-manager
      homeManagerModules = import ./modules/home-manager;
      #templates = import ./templates;

      # Your custom packages and modifications, exported as overlays
      overlays = import ./overlays { inherit inputs outputs; };

      # Your custom packages
      # Acessible through 'nix build', 'nix shell', etc
      packages = forEachSystem (pkgs: import ./pkgs { inherit pkgs; });
      # Devshell for bootstrapping
      # Acessible through 'nix develop' or 'nix-shell' (legacy)
      devShells = forEachSystem (pkgs: import ./shell.nix { inherit pkgs; });
      # Formatter for your nix files, available through 'nix fmt'
      # Other options beside 'alejandra' include 'nixpkgs-fmt'
      formatter = forEachSystem (pkgs: pkgs.nixpkgs-fmt);

      #wallpapers = import ./home/misterio/wallpapers;

      # NixOS configuration entrypoint
      # Available through 'nixos-rebuild --flake .#your-hostname'
      nixosConfigurations = {
        # Main desktop
        ryzennova = lib.nixosSystem {
          modules = [ ./hosts/ryzennova ];
          specialArgs = { inherit inputs outputs; };
        };
        # Personal laptop
        yoganova = lib.nixosSystem {
          modules = [ ./hosts/yoganova ];
          specialArgs = { inherit inputs outputs; };
        };
      };

      # Standalone home-manager configuration entrypoint
      # Available through 'home-manager --flake .#your-username@your-hostname'
      homeConfigurations = {
        # Desktops
        "novaviper@ryzennova" = lib.homeManagerConfiguration {
          modules = [ ./home/novaviper/ryzennova.nix ];
          pkgs = pkgsFor.x86_64-linux;
          extraSpecialArgs = { inherit inputs outputs; };
        };
        # Laptops
        "novaviper@yoganova" = lib.homeManagerConfiguration {
          modules = [ ./home/novaviper/yoganova.nix ];
          pkgs = pkgsFor.x86_64-linux;
          extraSpecialArgs = { inherit inputs outputs; };
        };
      };
    };
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.