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..cbf9aad3eae
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 + # Because /run/user/0 is wiped out by pam_systemd when root logouts,
183 + # systemd.services.gpg-agent cannot put its socket in
184 + # the path expected by gpg: /run/user/0/gnupg/d.6qoenf9br6fajbkknuz1i6ts
185 + # derived from ${gnupgHome}, since this removal would kill gpg-agent.
187 + # Unfortunately, for reaching such a persistent gpg-agent,
188 + # GPG_AGENT_INFO can no longer be used as it is ignored with gpg >= 2.1,
189 + # hence three different hacks are done here to make gpg connect
190 + # to ${gnupgHome}/S.gpg-agent depending on the concern:
191 + # - For gpg-agent, --supervised mode is used to pass it a socket
192 + # in the persistent directory gnupgHome.
193 + # - For the root user, on its login pam_systemd is mounting a fresh tmpfs on /run/user/0
194 + # so wrong perms are set on /run/user/0/gnupg/d.6qoenf9br6fajbkknuz1i6ts
195 + # when /run/user/0 is mounted, by overriding user-runtime-dir@.service
196 + # - For secret decrypting services, /run/user/0/gnupg
197 + # is emptied and keept empty by privately mounting
198 + # an empty directory on it, using BindReadOnlyPaths=
199 + systemd.sockets."gpg-agent" = lib.optionalAttrs cfg.enableGpgAgent {
200 + description = "Socket for gpg-agent";
201 + wantedBy = ["sockets.target"];
202 + socketConfig.ListenStream = "${gnupgHome}/S.gpg-agent";
203 + socketConfig.SocketMode = "0600";
205 + environment.systemPackages =
206 + # TODO: maybe this would be better to do that directly in pkgs.gnupg?
207 + let gpgPresetPassphrase = pkgs.runCommand "gpg-preset-passphrase"
208 + { preferLocalBuild = true;
209 + allowSubstitutes = false;
212 + ln -s -t $out/bin ${pkgs.gnupg}/libexec/gpg-preset-passphrase
214 + [ pkgs.gnupg gpgPresetPassphrase ];
215 + systemd.packages = [
216 + # Here, passing by systemd.packages is kind of a hack to be able to
217 + # write this file which is neither writable using environment.etc
218 + # (because environment.etc."systemd/system".source is set)
219 + # nor using systemd.services (because systemd.services."user-runtime-dir@0"
220 + # does not exist, and should not to keep using systemd's upstream template
221 + # and systemd.services."user-runtime-dir@").
222 + (pkgs.writeTextDir "etc/systemd/system/user-runtime-dir@0.service.d/override.conf" ''
225 + ExecStartPost=${pkgs.writeShellScript "redirect-gpg-agent-run-socket" ''
226 + install -D -d -m 640 /run/user/0/gnupg/d.6qoenf9br6fajbkknuz1i6ts
231 + lib.optionalAttrs cfg.enableGpgAgent {
233 + description = "gpg-agent for decrypting GnuPG-protected secrets";
234 + requires = ["gpg-agent.socket"];
237 + ExecStart = ''${pkgs.gnupg}/bin/gpg-agent \
239 + --homedir '${gnupgHome}' \
240 + --allow-loopback-pinentry \
241 + --allow-preset-passphrase \
242 + ${lib.escapeShellArgs cfg.gpgAgentFlags}
244 + Restart = "on-failure";
246 + StateDirectory = ["gnupg" "gnupg/empty"];
247 + StateDirectoryMode = "700";
251 + lib.mapAttrs' (target: secret:
252 + lib.nameValuePair (lib.removeSuffix ".service" secret.service) {
253 + description = "Install secret ${secret.path}";
254 + after = ["gpg-agent.service"];
255 + wants = ["gpg-agent.service"];
260 + ${pkgs.gnupg}/bin/gpg --homedir '${gnupgHome}' --no-autostart --batch --decrypt '${secret.gpg}' |
261 + ${lib.optionalString (secret.pipe != null) (secret.pipe+" |")} \
262 + install -D -m '${secret.mode}' -o '${secret.user}' -g '${secret.group}' /dev/stdin '${secret.path}'
264 + while ! decrypt; do sleep $((1 + ($RANDOM % 12))); done
266 + postStart = lib.optionalString (secret.postStart != "") ''
268 + ${secret.postStart}
270 + postStop = lib.optionalString (secret.postStop != "") ''
271 + secret_file='${secret.path}'
277 + RemainAfterExit = true;
279 + BindReadOnlyPaths = [ "/var/lib/gnupg/empty:/run/user/0/gnupg" ];
280 + } // lib.optionalAttrs (match "^/.*" target == null) {
281 + RuntimeDirectory = lib.removePrefix "/run/" (dirOf secret.path);
282 + RuntimeDirectoryMode = "711";
283 + RuntimeDirectoryPreserve = false;
288 +meta.maintainers = with lib.maintainers; [ julm ];