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..28a0af36ac5 --- /dev/null +++ b/nixos/modules/security/pass.nix @@ -0,0 +1,216 @@ +{ config, lib, pkgs, ... }: +let + inherit (builtins) dirOf head listToAttrs match split; + inherit (lib) types; + cfg = 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; + default = lib.maybeEnv "PASSWORD_STORE_DIR" ".password-store"; + 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; + }; + 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. + Set it to a temporary directory like /run/keys/key.pass + if you don't want it to persist accross reboot. + It can be customized per secret. + ''; + }; + 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. + It can be customized per secret. + ''; + }; + secrets = lib.mkOption { + description = "Available secrets."; + default = {}; + example = { + "/root/.ssh/id_ed25519" = {}; + "knot/tsig/example.org/acme.conf" = { + user = "knot"; + }; + "lego/example.org/rfc2136" = { + pipe = " + cat - + cat 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 = cfg.gnupgHome; + description = '' + Custom gnupgHome for this secret. + ''; + }; + passphraseFile = lib.mkOption { + type = types.str; + default = cfg.passphraseFile; + description = '' + Custom passphraseFile for this secret. + ''; + }; + 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; + apply = x: if x == null then null else pkgs.writeShellScript "pipe" x; + description = '' + Shell script taking the deciphered secret on its standard input + and which must put on its standard output + the actual secret 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. + ''; + }; + postStop = lib.mkOption { + type = types.lines; + default = ""; + example = ''shred -u "$secret_file"''; + description = '' + Commands to run after stopping the service. + Typically removing a persistent secret. + For convenience the path of the secret + is provided in the shell variable secret_file. + ''; + }; + }; + })); + }; +}; +config = lib.mkIf (cfg.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} + ''; + postStop = lib.optionalString (secret.postStop != "") '' + secret_file='${secret.path}' + set -eux + ${secret.postStop} + ''; + 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; + }; + } + ) cfg.secrets; +}; +meta.maintainers = with lib.maintainers; [ julm ]; +}