]> Git — Sourcephile - sourcephile-nix.git/blob - nixpkgs/patches/security.gnupg.diff
nix: update nixpkgs/patches
[sourcephile-nix.git] / nixpkgs / patches / security.gnupg.diff
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
5 @@ -193,6 +193,7 @@
6 ./security/lock-kernel-modules.nix
7 ./security/misc.nix
8 ./security/oath.nix
9 + ./security/gnupg.nix
10 ./security/pam.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
14 new file mode 100644
15 index 00000000000..a7a3c752267
16 --- /dev/null
17 +++ b/nixos/modules/security/gnupg.nix
18 @@ -0,0 +1,252 @@
19 +{ config, lib, pkgs, ... }:
20 +let
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);
28 +in
29 +{
30 +options.security.gnupg = {
31 + store = lib.mkOption {
32 + type = types.path;
33 + default = "/root/.password-store";
34 + description = ''
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>.
39 + '';
40 + # Does not copy the entire password-store into the Nix store,
41 + # only the keys actually used will be.
42 + apply = toString;
43 + };
44 + secrets = lib.mkOption {
45 + description = "Available secrets.";
46 + default = {};
47 + example = {
48 + "/root/.ssh/id_ed25519" = {};
49 + "knot/tsig/example.org/acme.conf" = {
50 + user = "knot";
51 + };
52 + "lego/example.org/rfc2136" = {
53 + pipe = "
54 + cat -
55 + cat <EOF
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
63 + RFC2136_TTL=1
64 + EOF
65 + ";
66 + };
67 + };
68 + type = types.attrsOf (types.submodule ({name, config, ...}: {
69 + options = {
70 + gpg = lib.mkOption {
71 + type = types.path;
72 + default = builtins.path {
73 + path = cfg.store + "/${name}.gpg";
74 + name = "${escapeUnitName name}.gpg";
75 + };
76 + description = ''
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>.
83 + '';
84 + };
85 + mode = lib.mkOption {
86 + type = types.str;
87 + default = "400";
88 + description = ''
89 + Permission mode of the secret <literal>path</literal>.
90 + '';
91 + };
92 + user = lib.mkOption {
93 + type = types.str;
94 + default = "root";
95 + description = ''
96 + Owner of the secret <literal>path</literal>.
97 + '';
98 + };
99 + group = lib.mkOption {
100 + type = types.str;
101 + default = "root";
102 + description = ''
103 + Group of the secret <literal>path</literal>.
104 + '';
105 + };
106 + pipe = lib.mkOption {
107 + type = types.nullOr types.str;
108 + default = null;
109 + apply = x: if x == null then null else pkgs.writeShellScript "pipe" x;
110 + description = ''
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.
115 + '';
116 + };
117 + path = lib.mkOption {
118 + type = types.str;
119 + default = name;
120 + apply = p: if match "^/.*" p == null then "/run/keys/gnupg/"+p+"/file" else p;
121 + description = ''
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>,
126 + if non-absolute.
127 + '';
128 + };
129 + service = lib.mkOption {
130 + type = types.str;
131 + default = "secret-" + escapeUnitName name + ".service";
132 + description = ''
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.
136 + '';
137 + };
138 + postStart = lib.mkOption {
139 + type = types.lines;
140 + default = "";
141 + example = "systemctl reload nginx.service";
142 + description = ''
143 + Commands to run after new secrets go live.
144 + Typically the web server and other servers using secrets
145 + need to be reloaded.
146 + '';
147 + };
148 + postStop = lib.mkOption {
149 + type = types.lines;
150 + default = "";
151 + example = ''shred -u "$secret_file"'';
152 + description = ''
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>.
157 + '';
158 + };
159 + };
160 + }));
161 + };
162 + enableGpgAgent = lib.mkEnableOption ''gpg-agent for decrypting secrets.
163 +
164 + Otherwise, you'll have to forward an <literal>agent-extra-socket</literal>:
165 + <screen>
166 + <prompt>$ </prompt>ssh -nNT root@example.org -o StreamLocalBindUnlink=yes -R /var/lib/gnupg/S.gpg-agent:$(gpgconf --list-dirs agent-extra-socket)
167 + </screen>
168 + '' // {default = true; example = false;};
169 + gpgAgentFlags = lib.mkOption {
170 + type = types.listOf types.str;
171 + default = [
172 + "--default-cache-ttl" "600"
173 + "--max-cache-ttl" "7200"
174 + ];
175 + description = ''
176 + Extra flags passed to the <literal>gpg-agent</literal>
177 + used to decrypt secrets.
178 + '';
179 + };
180 +};
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";
187 + };
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;
193 + } ''
194 + mkdir -p $out/bin
195 + ln -s -t $out/bin ${pkgs.gnupg}/libexec/gpg-preset-passphrase
196 + ''; in
197 + [ pkgs.gnupg gpgPresetPassphrase ];
198 + systemd.services =
199 + lib.optionalAttrs cfg.enableGpgAgent {
200 + gpg-agent = {
201 + description = "gpg-agent for decrypting GnuPG-protected secrets";
202 + requires = ["gpg-agent.socket"];
203 + serviceConfig = {
204 + Type = "simple";
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}.
210 + #
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 \
218 + --supervised \
219 + --homedir '${gnupgHome}' \
220 + --allow-loopback-pinentry \
221 + --allow-preset-passphrase \
222 + ${lib.escapeShellArgs cfg.gpgAgentFlags}
223 + '';
224 + Restart = "on-failure";
225 + RestartSec = 5;
226 + StateDirectory = ["gnupg"];
227 + StateDirectoryMode = "700";
228 + };
229 + };
230 + } //
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"];
236 + script = ''
237 + set -o pipefail
238 + set -eux
239 + decrypt() {
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}'
245 + }
246 + while ! decrypt; do sleep $((1 + ($RANDOM % 12))); done
247 + '';
248 + postStart = lib.optionalString (secret.postStart != "") ''
249 + set -eux
250 + ${secret.postStart}
251 + '';
252 + postStop = lib.optionalString (secret.postStop != "") ''
253 + secret_file='${secret.path}'
254 + set -eux
255 + ${secret.postStop}
256 + '';
257 + serviceConfig = {
258 + Type = "oneshot";
259 + PrivateTmp = true;
260 + RemainAfterExit = true;
261 + } // lib.optionalAttrs (match "^/.*" target == null) {
262 + RuntimeDirectory = lib.removePrefix "/run/" (dirOf secret.path);
263 + RuntimeDirectoryMode = "711";
264 + RuntimeDirectoryPreserve = false;
265 + };
266 + }
267 + ) cfg.secrets;
268 +};
269 +meta.maintainers = with lib.maintainers; [ julm ];
270 +}