freeze-dried nix

note

this page builds off of 41666's soppy_wet_nix, hence the title
consider reading that first

your$reader nix is soppy, right? but what if we added a bit more snowflake to your$reader nix?

doll

cold

why

having gone through soppy_wet_nix, it$dolls .sops.yaml looked like
#.sops.yaml · yaml · 17 lines
keys: &all
  - &op_stella ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMr6odlUppWKyn79H31iPFdXCW8s0QgY92cvklsmeKVm stella@eulr
  - &machine_xylem ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFJGMeJUFcSu49onjpfMaS3iEknma/30ZhgJtQrRX1qV root@xylem

creation_rules:
  - path_regex: shared-secrets\.yaml$
    key_groups:
	  - age: *all
  - path_regex: host/xylem/secrets\.yaml$
    key_groups:
	  - age:
	    - *op_stella
	    - *machine_xylem
  - path_regex: user/stella/secrets\.yaml$
    key_groups:
	  - age:
	    - *op_stella
the highlighting lib it$doll uses rn breaks on yaml. sowwy
which. not the worst, it$code's pretty clear about its intents
but it$doll already has a way to declare [machines, machine keys, admins, admin keys] with nix!
#darpa.nix · nix · 31 lines
{
	email = "stella@lifeless.space";

	admins = {
		stella = {
			# home-manager setup when administrating a server
			hm = pkgs: {
				home.packages = with pkgs; [
					micro
					curl
					btop
				];

				home.stateVersion = "25.11";
			};
			shell = pkgs: pkgs.nushell;

			# public keys used for ssh login
			ssh_publickeys = [
				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMr6odlUppWKyn79H31iPFdXCW8s0QgY92cvklsmeKVm stella@eulr"
			];
		};
	};

	machines = {
		xylem = {
			age_publickey = "age1wp5pysaa0gasu0fu8z0vy84fl7wkxepj7l23wwqvapn8dc38svxqdl3vj3";
			admins = ["stella"];
		};
	};
}

what if they kissed

nix is a functional language and a build system, so it$doll took advantage of that and made it build .sops.yaml from that nix file
let's start with adding a packages output to the flake:
#flake.nix · nix · 23 lines
{
	inputs = {
		nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

		systems.url = "github:nix-systems/default";

		# *snip* (other inputs)
	};

	outputs = inputs: let
		lib = inputs.nixpkgs.lib;
		forAllSystems = fn: lib.genAttrs (import inputs.systems) (system: fn {
			inherit system;
			pkgs = inputs.nixpkgs.legacyPackages.${system};
		});
	in {
		# *snip* (nixos system)

		packages = forAllSystems ({ system, pkgs }: {
			# we'll fill this in a moment
		});
	};
}
and two packages to go in there
#flake.nix · nix · 8 lines
			# exports a single-file package that is the .sops.yaml config file
			sops-config = pkgs.writeText "sops.yaml" ''
				# something goes here...
			'';
			# wraps sops to use the above config package as the config
			sops = pkgs.writeShellScriptBin "sops" ''
				${pkgs.sops}/bin/sops --config ${inputs.self.packages.${system}.sops-config} $@
			'';
function reference
  • writeText name text -> derivation
    writes a text file with name name and contents text into the store and returns the store path
  • writeShellScriptBin name text -> derivation
    writes an executable bash script with name name and contents text in a directory $storepath/bin/$name into the store and returns the store path
now all we need to do is plumb the keys into their corresponding file paths
#flake.nix · nix · 31 lines
			# exports a single-file package that is the .sops.yaml (except it's actually json) config file
			sops-config = pkgs.writeText "sops.yaml" (builtins.toJSON {
				creation_rules = let
					darpa = import ./darpa.nix;
					regexes =
						# user secrets
						{
							"shared-secrets\\.yaml" = lib.flatten [
								(lib.mapAttrsToList (name: machine: machine.age_publickey) darpa.machines)
								(lib.mapAttrsToList (name: admin: admin.ssh_publickeys) darpa.admins)
							];
						}
						# host secrets
						// lib.mapAttrs' (name: machine: lib.nameValuePair
							"host/${name}/secrets\\.yaml"
							(lib.flatten [
								machine.age_publickey
								(map (name: darpa.admins.${name}.ssh_publickeys) machine.admins)
							])
						) darpa.machines;
				in lib.mapAttrsToList (regex: keys: {
					path_regex = regex;
					key_groups = [
						{ age = keys; }
					];
				}) regexes;
			});
			# wraps sops to use the above config package as the config
			sops = pkgs.writeShellScriptBin "sops" ''
				${pkgs.sops}/bin/sops --config ${inputs.self.packages.${system}.sops-config} $@
			'';
function reference
  • writeText name text -> derivation
    writes a text file with name name and contents text into the store and returns the store path
  • toJSON value -> string
    converts a value to a json string
  • flatten value -> list
    flatten a value into a single list (nested lists are flattened too)
  • mapAttrsToList (name: value: return) attrset -> list
    map an attrset into a list, using the value the function returns, called for each attr with the name and value
  • mapAttrs' (name: value: { name = new_name; value = new_value; }) attrsef -> attrset
    map an attrset into another attrset, using the new_name and new_value the function returns, called for each attr with the name and value
  • nameValuePair name value -> { name = name; value = value; }
    simple utility function to create a name + value pair
  • map (value: return) list -> list
    map each value of a list, using the value the function returns, called for each value in the list

Her

Hey there, dolly$doll, I$Her noticed it$doll's using builtins.toJSON. Tell me$Her, why is this?
yes Goddess! nix doesn't have a way to generate normal yaml, but it does have lib.generators.toYAML, however:
#nixpkgs/lib/generators.nix · nix · 17 lines · source
	/**
		YAML has been a strict superset of JSON since 1.2, so we
		use toJSON. Before it only had a few differences referring
		to implicit typing rules, so it should work with older
		parsers as well.

		# Inputs

		Options

		: Empty set, there may be configuration options in the future

		Value

		: The value to be converted to YAML
	*/
	toYAML = {}: lib.strings.toJSON;

Her

I$Her see, thank it$doll for explaining that. Good doll~!
awwwwawwawaaawawaa!!!

using it

now we can get the actual config with nix build .#sops-config, which builds and symlinks the config to ./result!
or, directly invoke sops with nix run .#sops -- host/xylem/secrets.yaml!!
wawawa... thank for reading
final code
#flake.nix · nix · 53 lines
{
	inputs = {
		nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

		systems.url = "github:nix-systems/default";

		# *snip*
	};

	outputs = inputs: let
		lib = inputs.nixpkgs.lib;
		forAllSystems = fn: lib.genAttrs (import inputs.systems) (system: fn {
			inherit system;
			pkgs = inputs.nixpkgs.legacyPackages.${system};
		});
	in {
		# *snip*

		packages = forAllSystems ({ system, pkgs }: {
			# exports a single-file package that is the .sops.yaml (except it's actually json) config file
			sops-config = pkgs.writeText "sops.yaml" (builtins.toJSON {
				creation_rules = let
					darpa = import ./darpa.nix;
					regexes =
						# user secrets
						{
							"shared-secrets\\.yaml" = lib.flatten [
								(lib.mapAttrsToList (name: machine: machine.age_publickey) darpa.machines)
								(lib.mapAttrsToList (name: admin: admin.ssh_publickeys) darpa.admins)
							];
						}
						# host secrets
						// lib.mapAttrs' (name: machine: lib.nameValuePair
							"host/${name}/secrets\\.yaml"
							(lib.flatten [
								machine.age_publickey
								(map (name: darpa.admins.${name}.ssh_publickeys) machine.admins)
							])
						) darpa.machines;
				in lib.mapAttrsToList (regex: keys: {
					path_regex = regex;
					key_groups = [
						{ age = keys; }
					];
				}) regexes;
			});
			# wraps sops to use the above config package as the config
			sops = pkgs.writeShellScriptBin "sops" ''
				${pkgs.sops}/bin/sops --config ${inputs.self.packages.${system}.sops-config} $@
			'';
		});
	};
}

created:
updated:
built: