1 diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
2 index f361163ca63..8c199f0fc25 100644
3 --- a/nixos/modules/module-list.nix
4 +++ b/nixos/modules/module-list.nix
6 ./security/lock-kernel-modules.nix
11 ./security/pam_usb.nix
12 ./security/pam_mount.nix
13 diff --git a/nixos/modules/security/gnupg.nix b/nixos/modules/security/gnupg.nix
15 index 00000000000..a7a3c752267
17 +++ b/nixos/modules/security/gnupg.nix
19 +{ config, lib, pkgs, ... }:
21 + inherit (builtins) dirOf match split;
22 + inherit (lib) types;
23 + cfg = config.security.gnupg;
24 + gnupgHome = "/var/lib/gnupg";
25 + escapeUnitName = name:
26 + lib.concatMapStrings (s: if lib.isList s then "-" else s)
27 + (split "[^a-zA-Z0-9_.\\-]+" name);
30 +options.security.gnupg = {
31 + store = lib.mkOption {
33 + default = "/root/.password-store";
35 + Default base path for the <literal>gpg</literal> option
36 + of the <link linkend="opt-security.gnupg.secrets">secrets</link>.
37 + Note that you may set it up with something like:
38 + <literal>builtins.getEnv "PASSWORD_STORE_DIR" + "/machines/example"</literal>.
40 + # Does not copy the entire password-store into the Nix store,
41 + # only the keys actually used will be.
44 + secrets = lib.mkOption {
45 + description = "Available secrets.";
48 + "/root/.ssh/id_ed25519" = {};
49 + "knot/tsig/example.org/acme.conf" = {
52 + "lego/example.org/rfc2136" = {
56 + RFC2136_NAMESERVER=ns.example.org:53
57 + RFC2136_TSIG_ALGORITHM=hmac-sha256.
58 + RFC2136_TSIG_KEY=acme_example_org
59 + RFC2136_PROPAGATION_TIMEOUT=1000
60 + RFC2136_POLLING_INTERVAL=30
61 + RFC2136_SEQUENCE_INTERVAL=30
62 + RFC2136_DNS_TIMEOUT=1000
68 + type = types.attrsOf (types.submodule ({name, config, ...}: {
70 + gpg = lib.mkOption {
72 + default = builtins.path {
73 + path = cfg.store + "/${name}.gpg";
74 + name = "${escapeUnitName name}.gpg";
77 + The path to the GnuPG-encrypted secret.
78 + It will be copied into the Nix store of the orchestrating and of the target system.
79 + It must be decipherable by an OpenPGP key within the GnuPG home <filename>/var/lib/gnupg/</filename>.
80 + Defaults to the name of the secret,
81 + prefixed by the path of the <link linkend="opt-security.gnupg.store">store</link>
82 + and suffixed by <filename>.gpg</filename>.
85 + mode = lib.mkOption {
89 + Permission mode of the secret <literal>path</literal>.
92 + user = lib.mkOption {
96 + Owner of the secret <literal>path</literal>.
99 + group = lib.mkOption {
103 + Group of the secret <literal>path</literal>.
106 + pipe = lib.mkOption {
107 + type = types.nullOr types.str;
109 + apply = x: if x == null then null else pkgs.writeShellScript "pipe" x;
111 + Shell script taking the deciphered secret on its standard input
112 + and which must put on its standard output
113 + the actual secret material to be installed.
114 + This allows to decorate the secret with non-secret bits.
117 + path = lib.mkOption {
120 + apply = p: if match "^/.*" p == null then "/run/keys/gnupg/"+p+"/file" else p;
122 + The path on the target system where the secret is installed to.
123 + Default to the name of the secret,
124 + prefixed by <filename>/run/keys/gnupg/</filename>
125 + and suffixed by <filename>/file</filename>,
129 + service = lib.mkOption {
131 + default = "secret-" + escapeUnitName name + ".service";
133 + The name of the systemd service.
134 + Useful to put constraints like <literal>after</literal> or <literal>wants</wants>
135 + into services requiring this secret.
138 + postStart = lib.mkOption {
139 + type = types.lines;
141 + example = "systemctl reload nginx.service";
143 + Commands to run after new secrets go live.
144 + Typically the web server and other servers using secrets
145 + need to be reloaded.
148 + postStop = lib.mkOption {
149 + type = types.lines;
151 + example = ''shred -u "$secret_file"'';
153 + Commands to run after stopping the service.
154 + Typically removing a persistent secret.
155 + For convenience the <literal>path</literal> of the secret
156 + is provided in the shell variable <literal>secret_file</literal>.
162 + enableGpgAgent = lib.mkEnableOption ''gpg-agent for decrypting secrets.
164 + Otherwise, you'll have to forward an <literal>agent-extra-socket</literal>:
166 + <prompt>$ </prompt>ssh -nNT root@example.org -o StreamLocalBindUnlink=yes -R /var/lib/gnupg/S.gpg-agent:$(gpgconf --list-dirs agent-extra-socket)
168 + '' // {default = true; example = false;};
169 + gpgAgentFlags = lib.mkOption {
170 + type = types.listOf types.str;
172 + "--default-cache-ttl" "600"
173 + "--max-cache-ttl" "7200"
176 + Extra flags passed to the <literal>gpg-agent</literal>
177 + used to decrypt secrets.
181 +config = lib.mkIf (cfg.secrets != {}) {
182 + systemd.sockets."gpg-agent" = lib.optionalAttrs cfg.enableGpgAgent {
183 + description = "Socket for gpg-agent";
184 + wantedBy = ["sockets.target"];
185 + socketConfig.ListenStream = "${gnupgHome}/S.gpg-agent";
186 + socketConfig.SocketMode = "0600";
188 + environment.systemPackages =
189 + # TODO: maybe this would be better to do that directly in pkgs.gnupg?
190 + let gpgPresetPassphrase = pkgs.runCommand "gpg-preset-passphrase"
191 + { preferLocalBuild = true;
192 + allowSubstitutes = false;
195 + ln -s -t $out/bin ${pkgs.gnupg}/libexec/gpg-preset-passphrase
197 + [ pkgs.gnupg gpgPresetPassphrase ];
199 + lib.optionalAttrs cfg.enableGpgAgent {
201 + description = "gpg-agent for decrypting GnuPG-protected secrets";
202 + requires = ["gpg-agent.socket"];
205 + # Because /run/user/0 is wiped out by pam_systemd when root logouts,
206 + # systemd.services.gpg-agent cannot put its socket in
207 + # the path expected by gpg:
208 + # /run/user/0/gnupg/d.6qoenf9br6fajbkknuz1i6ts
209 + # derived from ${gnupgHome}.
211 + # But GPG_AGENT_INFO is ignored with gpg >= 2.1,
212 + # hence this hack makes gpg connect to ${gnupgHome}/S.gpg-agent
213 + # by relaxing permissions of the expected socket directory.
214 + # With this hack, uploading the passphrase should now be doable with just:
215 + # ssh root@example.org 'gpg-preset-passphrase --homedir /var/lib/gnupg --preset $keygrip'
216 + ExecStartPre = "${pkgs.coreutils}/bin/install -D -d -m 640 /run/user/0/gnupg/d.6qoenf9br6fajbkknuz1i6ts";
217 + ExecStart = ''${pkgs.gnupg}/bin/gpg-agent \
219 + --homedir '${gnupgHome}' \
220 + --allow-loopback-pinentry \
221 + --allow-preset-passphrase \
222 + ${lib.escapeShellArgs cfg.gpgAgentFlags}
224 + Restart = "on-failure";
226 + StateDirectory = ["gnupg"];
227 + StateDirectoryMode = "700";
231 + lib.mapAttrs' (target: secret:
232 + lib.nameValuePair (lib.removeSuffix ".service" secret.service) {
233 + description = "Install secret ${secret.path}";
234 + after = ["gpg-agent.service"];
235 + wants = ["gpg-agent.service"];
240 + # In case /run/user/0 has been cleaned up by pam_systemd
241 + install -D -m 640 -d /run/user/0/gnupg/d.6qoenf9br6fajbkknuz1i6ts
242 + ${pkgs.gnupg}/bin/gpg --homedir '${gnupgHome}' --no-autostart --batch --decrypt '${secret.gpg}' |
243 + ${lib.optionalString (secret.pipe != null) (secret.pipe+" |")} \
244 + install -D -m '${secret.mode}' -o '${secret.user}' -g '${secret.group}' /dev/stdin '${secret.path}'
246 + while ! decrypt; do sleep $((1 + ($RANDOM % 12))); done
248 + postStart = lib.optionalString (secret.postStart != "") ''
250 + ${secret.postStart}
252 + postStop = lib.optionalString (secret.postStop != "") ''
253 + secret_file='${secret.path}'
260 + RemainAfterExit = true;
261 + } // lib.optionalAttrs (match "^/.*" target == null) {
262 + RuntimeDirectory = lib.removePrefix "/run/" (dirOf secret.path);
263 + RuntimeDirectoryMode = "711";
264 + RuntimeDirectoryPreserve = false;
269 +meta.maintainers = with lib.maintainers; [ julm ];