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