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 ];
+}