]> Git — Sourcephile - sourcephile-nix.git/blob - nixos/modules/security/pass.nix
nix: simplify the sending root's OpenPGP key
[sourcephile-nix.git] / nixos / modules / security / pass.nix
1 { pkgs, lib, config, ... }:
2 let
3 inherit (builtins) dirOf head listToAttrs match split;
4 inherit (lib) types;
5 inherit (config.security) pass;
6 escapeUnitName = name:
7 lib.concatMapStrings (s: if lib.isList s then "-" else s)
8 (split "[^a-zA-Z0-9_.\\-]+" name);
9 in
10 {
11 options.security.pass = {
12 store = lib.mkOption {
13 type = types.path;
14 description = ''
15 Default path to the password-store of the orchestrating system.
16 '';
17 # Does not copy the entire password-store into the Nix store,
18 # only the keys actually used will be.
19 apply = toString;
20 };
21 secrets = lib.mkOption {
22 default = {};
23 type = types.attrsOf (types.submodule ({name, config, ...}: {
24 options = {
25 gpg = lib.mkOption {
26 type = types.path;
27 default = builtins.path {
28 path = pass.store + "/${name}.gpg";
29 name = "${escapeUnitName name}.gpg";
30 };
31 description = ''
32 The path to the gnupg-encrypted secret.
33 It will be copied into the Nix store of the orchestrating and of the target system.
34 It must be decipherable by an OpenPGP key within <literal>gnupgHome</literal>,
35 whose passhrase is on the target system into <literal>passphraseFile</literal>.
36 Defaults to the name of the secret, prefixed by <literal>passwordStore</literal>
37 and suffixed by <literal>.gpg</literal>.
38 '';
39 };
40 gnupgHome = lib.mkOption {
41 type = types.str;
42 default = "/root/.gnupg";
43 description = ''
44 The directory on the target system to the <literal>gnupg</literal> home
45 used to decrypt the secret.
46 '';
47 };
48 passphraseFile = lib.mkOption {
49 type = types.str;
50 default = "/root/key.pass";
51 description = ''
52 The directory on the target system to a file containing
53 the password of an OpenPGP key in <literal>gnupgHome</literal>,
54 to which <literal>gpg</literal> secret is encrypted to.
55 '';
56 };
57 mode = lib.mkOption {
58 type = types.str;
59 default = "400";
60 description = ''
61 Permission mode of the secret <literal>path</literal>.
62 '';
63 };
64 user = lib.mkOption {
65 type = types.str;
66 default = "root";
67 description = ''
68 Owner of the secret <literal>path</literal>.
69 '';
70 };
71 group = lib.mkOption {
72 type = types.str;
73 default = "root";
74 description = ''
75 Group of the secret <literal>path</literal>.
76 '';
77 };
78 pipe = lib.mkOption {
79 type = types.nullOr types.str;
80 default = null;
81 description = ''
82 Shell command taking the deciphered secret on its standard input
83 and which must put on its standard output
84 the actual material to be installed.
85 This allows to decorate the secret with non-secret bits.
86 '';
87 };
88 path = lib.mkOption {
89 type = types.str;
90 default = name;
91 apply = p: if match "^/.*" p == null then "/run/pass-secrets/"+p+"/file" else p;
92 description = ''
93 The path on the target system where the secret is installed to.
94 Any non-absolute path is relative to <filename>/run/pass-secrets</filename>.
95 Default to the name of the secret.
96 '';
97 };
98 service = lib.mkOption {
99 type = types.str;
100 default = "secret-" + escapeUnitName name + ".service";
101 description = ''
102 The name of the systemd service.
103 Useful to put constraints like <literal>after</literal> or <literal>wants</wants>
104 into services requiring this secret.
105 '';
106 };
107 postStart = lib.mkOption {
108 type = types.lines;
109 default = "";
110 example = "systemctl reload nginx.service";
111 description = ''
112 Commands to run after new secrets go live. Typically
113 the web server and other servers using secrets need to
114 be reloaded.
115 '';
116 };
117 };
118 }));
119 };
120 };
121 config = lib.mkIf (pass.secrets != {}) {
122 systemd.services =
123 lib.mapAttrs' (target: secret:
124 lib.nameValuePair (lib.removeSuffix ".service" secret.service) {
125 description = "Install secret ${secret.path}";
126 script = ''
127 set -o pipefail
128 set -eux
129 decrypt() {
130 {
131 ${pkgs.gnupg}/bin/gpg --batch --pinentry-mode loopback \
132 --passphrase-file '${secret.passphraseFile}' \
133 --decrypt '${secret.gpg}' \
134 ${lib.optionalString (secret.pipe != null) (" | "+secret.pipe)}
135 } |
136 install -D -m '${secret.mode}' -o '${secret.user}' -g '${secret.group}' /dev/stdin \
137 '${secret.path}'
138 }
139 while ! decrypt; do sleep $((1 + ($RANDOM % 10))); done
140 '';
141 postStart = lib.optionalString (secret.postStart != "") ''
142 set -eux
143 ${secret.postStart}
144 '';
145 restartIfChanged = true;
146 restartTriggers = [
147 secret.passphraseFile
148 secret.gpg
149 ];
150 serviceConfig = {
151 Type = "oneshot";
152 Environment = "GNUPGHOME=${secret.gnupgHome}";
153 PrivateTmp = true;
154 RemainAfterExit = true;
155 WorkingDirectory = dirOf secret.gnupgHome;
156 } // lib.optionalAttrs (match "^/.*" target == null) {
157 RuntimeDirectory = lib.removePrefix "/run/" (dirOf secret.path);
158 RuntimeDirectoryMode = "711";
159 RuntimeDirectoryPreserve = false;
160 };
161 }
162 ) pass.secrets;
163 };
164 meta.maintainers = with lib.maintainers; [ julm ];
165 }