diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index f361163ca63..5e306de7dad 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -193,6 +193,7 @@ ./security/lock-kernel-modules.nix ./security/misc.nix ./security/oath.nix + ./security/pass.nix ./security/pam.nix ./security/pam_usb.nix ./security/pam_mount.nix diff --git a/nixos/modules/security/pass.nix b/nixos/modules/security/pass.nix new file mode 100644 index 00000000000..a2141d56d16 --- /dev/null +++ b/nixos/modules/security/pass.nix @@ -0,0 +1,165 @@ +{ pkgs, lib, config, ... }: +let + inherit (builtins) dirOf head listToAttrs match split; + inherit (lib) types; + inherit (config.security) pass; + escapeUnitName = name: + lib.concatMapStrings (s: if lib.isList s then "-" else s) + (split "[^a-zA-Z0-9_.\\-]+" name); +in +{ +options.security.pass = { + store = lib.mkOption { + type = types.path; + description = '' + Default path to the password-store of the orchestrating system. + ''; + # Does not copy the entire password-store into the Nix store, + # only the keys actually used will be. + apply = toString; + }; + secrets = lib.mkOption { + default = {}; + type = types.attrsOf (types.submodule ({name, config, ...}: { + options = { + gpg = lib.mkOption { + type = types.path; + default = builtins.path { + path = pass.store + "/${name}.gpg"; + name = "${escapeUnitName name}.gpg"; + }; + description = '' + The path to the gnupg-encrypted secret. + It will be copied into the Nix store of the orchestrating and of the target system. + It must be decipherable by an OpenPGP key within gnupgHome, + whose passhrase is on the target system into passphraseFile. + Defaults to the name of the secret, prefixed by passwordStore + and suffixed by .gpg. + ''; + }; + gnupgHome = lib.mkOption { + type = types.str; + default = "/root/.gnupg"; + description = '' + The directory on the target system to the gnupg home + used to decrypt the secret. + ''; + }; + passphraseFile = lib.mkOption { + type = types.str; + default = "/root/key.pass"; + description = '' + The directory on the target system to a file containing + the password of an OpenPGP key in gnupgHome, + to which gpg secret is encrypted to. + ''; + }; + mode = lib.mkOption { + type = types.str; + default = "400"; + description = '' + Permission mode of the secret path. + ''; + }; + user = lib.mkOption { + type = types.str; + default = "root"; + description = '' + Owner of the secret path. + ''; + }; + group = lib.mkOption { + type = types.str; + default = "root"; + description = '' + Group of the secret path. + ''; + }; + pipe = lib.mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Shell command taking the deciphered secret on its standard input + and which must put on its standard output + the actual material to be installed. + This allows to decorate the secret with non-secret bits. + ''; + }; + path = lib.mkOption { + type = types.str; + default = name; + apply = p: if match "^/.*" p == null then "/run/pass-secrets/"+p+"/file" else p; + description = '' + The path on the target system where the secret is installed to. + Any non-absolute path is relative to /run/pass-secrets. + Default to the name of the secret. + ''; + }; + service = lib.mkOption { + type = types.str; + default = "secret-" + escapeUnitName name + ".service"; + description = '' + The name of the systemd service. + Useful to put constraints like after or wants + into services requiring this secret. + ''; + }; + postStart = lib.mkOption { + type = types.lines; + default = ""; + example = "systemctl reload nginx.service"; + description = '' + Commands to run after new secrets go live. Typically + the web server and other servers using secrets need to + be reloaded. + ''; + }; + }; + })); + }; +}; +config = lib.mkIf (pass.secrets != {}) { + systemd.services = + lib.mapAttrs' (target: secret: + lib.nameValuePair (lib.removeSuffix ".service" secret.service) { + description = "Install secret ${secret.path}"; + script = '' + set -o pipefail + set -eux + decrypt() { + { + ${pkgs.gnupg}/bin/gpg --batch --pinentry-mode loopback \ + --passphrase-file '${secret.passphraseFile}' \ + --decrypt '${secret.gpg}' \ + ${lib.optionalString (secret.pipe != null) (" | "+secret.pipe)} + } | + install -D -m '${secret.mode}' -o '${secret.user}' -g '${secret.group}' /dev/stdin \ + '${secret.path}' + } + while ! decrypt; do sleep $((1 + ($RANDOM % 10))); done + ''; + postStart = lib.optionalString (secret.postStart != "") '' + set -eux + ${secret.postStart} + ''; + restartIfChanged = true; + restartTriggers = [ + secret.passphraseFile + secret.gpg + ]; + serviceConfig = { + Type = "oneshot"; + Environment = "GNUPGHOME=${secret.gnupgHome}"; + PrivateTmp = true; + RemainAfterExit = true; + WorkingDirectory = dirOf secret.gnupgHome; + } // lib.optionalAttrs (match "^/.*" target == null) { + RuntimeDirectory = lib.removePrefix "/run/" (dirOf secret.path); + RuntimeDirectoryMode = "711"; + RuntimeDirectoryPreserve = false; + }; + } + ) pass.secrets; +}; +meta.maintainers = with lib.maintainers; [ julm ]; +}