diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 06979a4df508f9..10c0e844c82f5d 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1709,6 +1709,8 @@ ./system/boot/timesyncd.nix ./system/boot/tmp.nix ./system/boot/uvesafb.nix + ./system/vars/module.nix + ./system/vars/on-machine-backend.nix ./system/etc/etc-activation.nix ./tasks/auto-upgrade.nix ./tasks/bcache.nix diff --git a/nixos/modules/system/vars/on-machine-backend.nix b/nixos/modules/system/vars/on-machine-backend.nix new file mode 100644 index 00000000000000..a1771b00ebc87a --- /dev/null +++ b/nixos/modules/system/vars/on-machine-backend.nix @@ -0,0 +1,68 @@ +# we use this vars backend as an example backend. +# this generates a script which creates the values at the expected path. +# this script has to be run manually (I guess after updating the system) to generate the required vars. +{ pkgs, lib, config, ... }: +let + cfg = config.vars.settings.on-machine; + sortedGenerators = lib.toposort (a: b: builtins.elem a.name b.dependencies) ( + lib.attrValues config.clan.core.vars.generators + ); +in +{ + options.vars.settings.on-machine = { + enable = lib.mkEnableOption "Enable on-machine vars backend"; + fileLocation = lib.mkOption { + type = lib.types.str; + default = "/etc/secrets"; + }; + }; + config = lib.mkIf cfg.enable { + vars.settings.fileModule = file: { + path = "${cfg.fileLocation}/${file.generator}/${file.name}"; + }; + environment.systemPackages = [ + (pkgs.writeShellApplication { + name = "generate-vars"; + text = '' + ${lib.concatStringsSep "\n" (gen: '' + all_files_missing=true + all_files_present=true + ${lib.concatStringsSep "\n" (file: '' + if test -e ${file.path} ; then + all_files_missing=false + else + all_files_present=false + fi + '') (lib.attrValues gen.files)} + if [ $all_files_missing = false ] && [ $all_files_present = false ] ; then + echo "Inconsistent state for generator ${gen.name}" + exit 1 + fi + if [ $all_files_present = true ] ; then + echo "All secrets for ${gen.name} are present" + elif [ $all_files_missing = true ] ; then + echo "Generating vars for ${gen.name}" + # TODO add inputs + # TODO add dependencies + out=$(mktemp -d) + trap 'rm -rf $out' EXIT + export $out + mkdir -p "$out" + ${gen.finalScript} + ${lib.concatStringsSep "\n" (file: '' + if ! test -e $out/${file.name} ; then + echo 'generator ${gen.name} failed to generate ${file.name}' + exit 1 + fi + '') (lib.attrValues gen.files)} + ${lib.concatStringsSep "\n" (file: '' + mv "$out"/'${file.name}' '${file.path}' + '') (lib.attrValues gen.files)} + rm -rf "$out" + fi + '') sortedGenerators} + ''; + }) + ]; + }; +} diff --git a/nixos/modules/system/vars/options.nix b/nixos/modules/system/vars/options.nix new file mode 100644 index 00000000000000..b82c5ca873c0eb --- /dev/null +++ b/nixos/modules/system/vars/options.nix @@ -0,0 +1,212 @@ +{ lib, ... }: +{ + options.vars = { + settings = { + fileModule = lib.mkOption { + type = lib.types.deferredModule; + internal = true; + description = '' + A module to be imported in every vars.files. submodule. + Used by backends to define the `path` attribute. + + Takes the file as an arument and returns maybe an attrset with should at least contain the `path` attribute. + Can be used to set other file attributes as well, like `value`. + ''; + default = { }; + }; + }; + generators = lib.mkOption { + description = '' + A set of generators that can be used to generate files. + Generators are scripts that produce files based on the values of other generators and user input. + Each generator is expected to produce a set of files under a directory. + ''; + default = { }; + type = lib.types.attrsOf ( + lib.types.submodule (generator: { + options = { + name = lib.mkOption { + type = lib.types.str; + description = '' + The name of the generator. + This name will be used to refer to the generator in other generators. + ''; + readOnly = true; + default = generator.config._module.args.name; + defaultText = "Name of the generator"; + }; + + dependencies = lib.mkOption { + description = '' + A list of other generators that this generator depends on. + The output values of these generators will be available to the generator script as files. + For example, the file 'file1' of a dependency named 'dep1' will be available via $in/dep1/file1. + ''; + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + files = lib.mkOption { + description = '' + A set of files to generate. + The generator 'script' is expected to produce exactly these files under $out. + ''; + type = lib.types.attrsOf ( + lib.types.submodule (file: { + options = { + name = lib.mkOption { + type = lib.types.str; + description = '' + name of the public fact + ''; + readOnly = true; + default = file.config._module.args.name; + defaultText = "Name of the file"; + }; + generator = lib.mkOption { + description = '' + The generator that produces the file. + This is the name of another generator. + ''; + type = lib.types.str; + default = generator.name; + }; + deploy = lib.mkOption { + description = '' + Whether the file should be deployed to the target machine. + + Enable this if the generated file is only used as an input to other generators. + ''; + type = lib.types.bool; + default = true; + }; + secret = lib.mkOption { + description = '' + Whether the file should be treated as a secret. + ''; + type = lib.types.bool; + default = true; + }; + path = lib.mkOption { + description = '' + The path to the file containing the content of the generated value. + This will be set automatically + ''; + type = lib.types.str; + }; + neededFor = lib.mkOption { + description = '' + This option determines when the secret will be decrypted and deployed to the target machine. + + By setting this to `activation`, the secret will be deployed prior to running `nixos-rebuild` or `nixos-install`. + By setting this to `user`, the secret will be deployed prior to users and groups are created, allowing + users' passwords to be managed by vars. The secret will be stored in `/run/secrets-for-users` and `owner` and `group` must be `root`. + ''; + type = lib.types.enum [ + "activation" + "users" + "services" + ]; + default = "services"; + }; + owner = lib.mkOption { + description = "The user name or id that will own the file."; + default = "root"; + }; + group = lib.mkOption { + description = "The group name or id that will own the file."; + default = "root"; + }; + mode = lib.mkOption { + type = lib.types.strMatching "^[0-7]{3}$"; + description = "The unix file mode of the file. Must be a 3-digit octal number."; + default = "400"; + }; + value = + lib.mkOption { + description = '' + The content of the generated value. + Only available if the file is not secret. + ''; + type = lib.types.str; + defaultText = "Throws error because the value of a secret file is not accessible"; + } + // lib.optionalAttrs file.config.secret { + default = throw "Cannot access value of secret file"; + }; + }; + }) + ); + }; + prompts = lib.mkOption { + description = '' + A set of prompts to ask the user for values. + Prompts are available to the generator script as files. + For example, a prompt named 'prompt1' will be available via $prompts/prompt1 + ''; + default = { }; + type = lib.types.attrsOf ( + lib.types.submodule (prompt: { + options = { + name = lib.mkOption { + description = '' + The name of the prompt. + This name will be used to refer to the prompt in the generator script. + ''; + type = lib.types.str; + default = prompt.config._module.args.name; + defaultText = "Name of the prompt"; + }; + description = lib.mkOption { + description = '' + The description of the prompted value + ''; + type = lib.types.str; + example = "SSH private key"; + default = prompt.config._module.args.name; + defaultText = "Name of the prompt"; + }; + type = lib.mkOption { + description = '' + The input type of the prompt. + The following types are available: + - hidden: A hidden text (e.g. password) + - line: A single line of text + - multiline: A multiline text + ''; + type = lib.types.enum [ + "hidden" + "line" + "multiline" + ]; + default = "line"; + }; + }; + }) + ); + }; + runtimeInputs = lib.mkOption { + description = '' + A list of packages that the generator script requires. + These packages will be available in the PATH when the script is run. + ''; + type = lib.types.listOf lib.types.package; + default = [ ]; + }; + script = lib.mkOption { + description = '' + The script to run to generate the files. + The script will be run with the following environment variables: + - $in: The directory containing the output values of all declared dependencies + - $out: The output directory to put the generated files + - $prompts: The directory containing the prompted values as files + The script should produce the files specified in the 'files' attribute under $out. + ''; + type = lib.types.either lib.types.str lib.types.path; + default = ""; + }; + }; + }) + ); + }; + }; +}