]> Git — Sourcephile - sourcephile-nix.git/blob - nixpkgs/patches/security.gnupg.diff
environment: add networking tools
[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..cbf9aad3eae
16 --- /dev/null
17 +++ b/nixos/modules/security/gnupg.nix
18 @@ -0,0 +1,271 @@
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 + # 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.
186 + #
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";
204 + };
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;
210 + } ''
211 + mkdir -p $out/bin
212 + ln -s -t $out/bin ${pkgs.gnupg}/libexec/gpg-preset-passphrase
213 + ''; in
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" ''
223 + [Unit]
224 + [Service]
225 + ExecStartPost=${pkgs.writeShellScript "redirect-gpg-agent-run-socket" ''
226 + install -D -d -m 640 /run/user/0/gnupg/d.6qoenf9br6fajbkknuz1i6ts
227 + ''}
228 + '')
229 + ];
230 + systemd.services =
231 + lib.optionalAttrs cfg.enableGpgAgent {
232 + gpg-agent = {
233 + description = "gpg-agent for decrypting GnuPG-protected secrets";
234 + requires = ["gpg-agent.socket"];
235 + serviceConfig = {
236 + Type = "simple";
237 + ExecStart = ''${pkgs.gnupg}/bin/gpg-agent \
238 + --supervised \
239 + --homedir '${gnupgHome}' \
240 + --allow-loopback-pinentry \
241 + --allow-preset-passphrase \
242 + ${lib.escapeShellArgs cfg.gpgAgentFlags}
243 + '';
244 + Restart = "on-failure";
245 + RestartSec = 5;
246 + StateDirectory = ["gnupg" "gnupg/empty"];
247 + StateDirectoryMode = "700";
248 + };
249 + };
250 + } //
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"];
256 + script = ''
257 + set -o pipefail
258 + set -eux
259 + decrypt() {
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}'
263 + }
264 + while ! decrypt; do sleep $((1 + ($RANDOM % 12))); done
265 + '';
266 + postStart = lib.optionalString (secret.postStart != "") ''
267 + set -eux
268 + ${secret.postStart}
269 + '';
270 + postStop = lib.optionalString (secret.postStop != "") ''
271 + secret_file='${secret.path}'
272 + set -eux
273 + ${secret.postStop}
274 + '';
275 + serviceConfig = {
276 + Type = "oneshot";
277 + RemainAfterExit = true;
278 + PrivateTmp = 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;
284 + };
285 + }
286 + ) cfg.secrets;
287 +};
288 +meta.maintainers = with lib.maintainers; [ julm ];
289 +}