diff options
Diffstat (limited to 'modules/acme2.nix')
-rw-r--r-- | modules/acme2.nix | 353 |
1 files changed, 0 insertions, 353 deletions
diff --git a/modules/acme2.nix b/modules/acme2.nix deleted file mode 100644 index b22e4ccc..00000000 --- a/modules/acme2.nix +++ /dev/null | |||
@@ -1,353 +0,0 @@ | |||
1 | { config, lib, pkgs, ... }: | ||
2 | |||
3 | with lib; | ||
4 | |||
5 | let | ||
6 | |||
7 | cfg = config.security.acme2; | ||
8 | |||
9 | certOpts = { name, ... }: { | ||
10 | options = { | ||
11 | webroot = mkOption { | ||
12 | type = types.str; | ||
13 | example = "/var/lib/acme/acme-challenges"; | ||
14 | description = '' | ||
15 | Where the webroot of the HTTP vhost is located. | ||
16 | <filename>.well-known/acme-challenge/</filename> directory | ||
17 | will be created below the webroot if it doesn't exist. | ||
18 | <literal>http://example.org/.well-known/acme-challenge/</literal> must also | ||
19 | be available (notice unencrypted HTTP). | ||
20 | ''; | ||
21 | }; | ||
22 | |||
23 | server = mkOption { | ||
24 | type = types.nullOr types.str; | ||
25 | default = null; | ||
26 | description = '' | ||
27 | ACME Directory Resource URI. Defaults to let's encrypt | ||
28 | production endpoint, | ||
29 | https://acme-v02.api.letsencrypt.org/directory, if unset. | ||
30 | ''; | ||
31 | }; | ||
32 | |||
33 | domain = mkOption { | ||
34 | type = types.str; | ||
35 | default = name; | ||
36 | description = "Domain to fetch certificate for (defaults to the entry name)"; | ||
37 | }; | ||
38 | |||
39 | email = mkOption { | ||
40 | type = types.nullOr types.str; | ||
41 | default = null; | ||
42 | description = "Contact email address for the CA to be able to reach you."; | ||
43 | }; | ||
44 | |||
45 | user = mkOption { | ||
46 | type = types.str; | ||
47 | default = "root"; | ||
48 | description = "User running the ACME client."; | ||
49 | }; | ||
50 | |||
51 | group = mkOption { | ||
52 | type = types.str; | ||
53 | default = "root"; | ||
54 | description = "Group running the ACME client."; | ||
55 | }; | ||
56 | |||
57 | allowKeysForGroup = mkOption { | ||
58 | type = types.bool; | ||
59 | default = false; | ||
60 | description = '' | ||
61 | Give read permissions to the specified group | ||
62 | (<option>security.acme2.cert.<name>.group</option>) to read SSL private certificates. | ||
63 | ''; | ||
64 | }; | ||
65 | |||
66 | postRun = mkOption { | ||
67 | type = types.lines; | ||
68 | default = ""; | ||
69 | example = "systemctl reload nginx.service"; | ||
70 | description = '' | ||
71 | Commands to run after new certificates go live. Typically | ||
72 | the web server and other servers using certificates need to | ||
73 | be reloaded. | ||
74 | |||
75 | Executed in the same directory with the new certificate. | ||
76 | ''; | ||
77 | }; | ||
78 | |||
79 | plugins = mkOption { | ||
80 | type = types.listOf (types.enum [ | ||
81 | "cert.der" "cert.pem" "chain.pem" "external.sh" | ||
82 | "fullchain.pem" "full.pem" "key.der" "key.pem" "account_key.json" "account_reg.json" | ||
83 | ]); | ||
84 | default = [ "fullchain.pem" "full.pem" "key.pem" "account_key.json" "account_reg.json" ]; | ||
85 | description = '' | ||
86 | Plugins to enable. With default settings simp_le will | ||
87 | store public certificate bundle in <filename>fullchain.pem</filename>, | ||
88 | private key in <filename>key.pem</filename> and those two previous | ||
89 | files combined in <filename>full.pem</filename> in its state directory. | ||
90 | ''; | ||
91 | }; | ||
92 | |||
93 | directory = mkOption { | ||
94 | type = types.str; | ||
95 | readOnly = true; | ||
96 | default = "/var/lib/acme/${name}"; | ||
97 | description = "Directory where certificate and other state is stored."; | ||
98 | }; | ||
99 | |||
100 | extraDomains = mkOption { | ||
101 | type = types.attrsOf (types.nullOr types.str); | ||
102 | default = {}; | ||
103 | example = literalExample '' | ||
104 | { | ||
105 | "example.org" = "/srv/http/nginx"; | ||
106 | "mydomain.org" = null; | ||
107 | } | ||
108 | ''; | ||
109 | description = '' | ||
110 | A list of extra domain names, which are included in the one certificate to be issued, with their | ||
111 | own server roots if needed. | ||
112 | ''; | ||
113 | }; | ||
114 | }; | ||
115 | }; | ||
116 | |||
117 | in | ||
118 | |||
119 | { | ||
120 | |||
121 | ###### interface | ||
122 | imports = [ | ||
123 | (mkRemovedOptionModule [ "security" "acme2" "production" ] '' | ||
124 | Use security.acme2.server to define your staging ACME server URL instead. | ||
125 | |||
126 | To use the let's encrypt staging server, use security.acme2.server = | ||
127 | "https://acme-staging-v02.api.letsencrypt.org/directory". | ||
128 | '' | ||
129 | ) | ||
130 | (mkRemovedOptionModule [ "security" "acme2" "directory"] "ACME Directory is now hardcoded to /var/lib/acme and its permisisons are managed by systemd. See https://github.com/NixOS/nixpkgs/issues/53852 for more info.") | ||
131 | (mkRemovedOptionModule [ "security" "acme" "preDelay"] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal") | ||
132 | (mkRemovedOptionModule [ "security" "acme" "activationDelay"] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal") | ||
133 | ]; | ||
134 | options = { | ||
135 | security.acme2 = { | ||
136 | |||
137 | validMin = mkOption { | ||
138 | type = types.int; | ||
139 | default = 30 * 24 * 3600; | ||
140 | description = "Minimum remaining validity before renewal in seconds."; | ||
141 | }; | ||
142 | |||
143 | renewInterval = mkOption { | ||
144 | type = types.str; | ||
145 | default = "weekly"; | ||
146 | description = '' | ||
147 | Systemd calendar expression when to check for renewal. See | ||
148 | <citerefentry><refentrytitle>systemd.time</refentrytitle> | ||
149 | <manvolnum>7</manvolnum></citerefentry>. | ||
150 | ''; | ||
151 | }; | ||
152 | |||
153 | server = mkOption { | ||
154 | type = types.nullOr types.str; | ||
155 | default = null; | ||
156 | description = '' | ||
157 | ACME Directory Resource URI. Defaults to let's encrypt | ||
158 | production endpoint, | ||
159 | <literal>https://acme-v02.api.letsencrypt.org/directory</literal>, if unset. | ||
160 | ''; | ||
161 | }; | ||
162 | |||
163 | preliminarySelfsigned = mkOption { | ||
164 | type = types.bool; | ||
165 | default = true; | ||
166 | description = '' | ||
167 | Whether a preliminary self-signed certificate should be generated before | ||
168 | doing ACME requests. This can be useful when certificates are required in | ||
169 | a webserver, but ACME needs the webserver to make its requests. | ||
170 | |||
171 | With preliminary self-signed certificate the webserver can be started and | ||
172 | can later reload the correct ACME certificates. | ||
173 | ''; | ||
174 | }; | ||
175 | |||
176 | certs = mkOption { | ||
177 | default = { }; | ||
178 | type = with types; attrsOf (submodule certOpts); | ||
179 | description = '' | ||
180 | Attribute set of certificates to get signed and renewed. Creates | ||
181 | <literal>acme-''${cert}.{service,timer}</literal> systemd units for | ||
182 | each certificate defined here. Other services can add dependencies | ||
183 | to those units if they rely on the certificates being present, | ||
184 | or trigger restarts of the service if certificates get renewed. | ||
185 | ''; | ||
186 | example = literalExample '' | ||
187 | { | ||
188 | "example.com" = { | ||
189 | webroot = "/var/www/challenges/"; | ||
190 | email = "foo@example.com"; | ||
191 | extraDomains = { "www.example.com" = null; "foo.example.com" = "/var/www/foo/"; }; | ||
192 | }; | ||
193 | "bar.example.com" = { | ||
194 | webroot = "/var/www/challenges/"; | ||
195 | email = "bar@example.com"; | ||
196 | }; | ||
197 | } | ||
198 | ''; | ||
199 | }; | ||
200 | }; | ||
201 | }; | ||
202 | |||
203 | ###### implementation | ||
204 | config = mkMerge [ | ||
205 | (mkIf (cfg.certs != { }) { | ||
206 | |||
207 | systemd.services = let | ||
208 | services = concatLists servicesLists; | ||
209 | servicesLists = mapAttrsToList certToServices cfg.certs; | ||
210 | certToServices = cert: data: | ||
211 | let | ||
212 | lpath = "acme/${cert}"; | ||
213 | rights = if data.allowKeysForGroup then "750" else "700"; | ||
214 | cmdline = [ "-v" "-d" data.domain "--default_root" data.webroot "--valid_min" cfg.validMin ] | ||
215 | ++ optionals (data.email != null) [ "--email" data.email ] | ||
216 | ++ concatMap (p: [ "-f" p ]) data.plugins | ||
217 | ++ concatLists (mapAttrsToList (name: root: [ "-d" (if root == null then name else "${name}:${root}")]) data.extraDomains) | ||
218 | ++ optionals (cfg.server != null || data.server != null) ["--server" (if data.server == null then cfg.server else data.server)]; | ||
219 | acmeService = { | ||
220 | description = "Renew ACME Certificate for ${cert}"; | ||
221 | after = [ "network.target" "network-online.target" ]; | ||
222 | wants = [ "network-online.target" ]; | ||
223 | # simp_le uses requests, which uses certifi under the hood, | ||
224 | # which doesn't respect the system trust store. | ||
225 | # At least in the acme test, we provision a fake CA, impersonating the LE endpoint. | ||
226 | # REQUESTS_CA_BUNDLE is a way to teach python requests to use something else | ||
227 | environment.REQUESTS_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"; | ||
228 | serviceConfig = { | ||
229 | Type = "oneshot"; | ||
230 | # With RemainAfterExit the service is considered active even | ||
231 | # after the main process having exited, which means when it | ||
232 | # gets changed, the activation phase restarts it, meaning | ||
233 | # the permissions of the StateDirectory get adjusted | ||
234 | # according to the specified group | ||
235 | # Edit: Timers will never run because of this | ||
236 | # RemainAfterExit = true; | ||
237 | SuccessExitStatus = [ "0" "1" ]; | ||
238 | User = data.user; | ||
239 | Group = data.group; | ||
240 | PrivateTmp = true; | ||
241 | StateDirectory = lpath; | ||
242 | StateDirectoryMode = rights; | ||
243 | ExecStartPre = | ||
244 | let | ||
245 | script = pkgs.writeScript "acme-pre-start" '' | ||
246 | #!${pkgs.runtimeShell} -e | ||
247 | mkdir -p '${data.webroot}/.well-known/acme-challenge' | ||
248 | chmod a+w '${data.webroot}/.well-known/acme-challenge' | ||
249 | #doesn't work for multiple concurrent runs | ||
250 | #chown -R '${data.user}:${data.group}' '${data.webroot}/.well-known/acme-challenge' | ||
251 | ''; | ||
252 | in | ||
253 | "+${script}"; | ||
254 | WorkingDirectory = "/var/lib/${lpath}"; | ||
255 | ExecStart = "${pkgs.simp_le_0_17}/bin/simp_le ${escapeShellArgs cmdline}"; | ||
256 | ExecStartPost = | ||
257 | let | ||
258 | script = pkgs.writeScript "acme-post-start" '' | ||
259 | #!${pkgs.runtimeShell} -e | ||
260 | ${data.postRun} | ||
261 | ''; | ||
262 | in | ||
263 | "+${script}"; | ||
264 | }; | ||
265 | |||
266 | }; | ||
267 | selfsignedService = { | ||
268 | description = "Create preliminary self-signed certificate for ${cert}"; | ||
269 | path = [ pkgs.openssl ]; | ||
270 | script = | ||
271 | '' | ||
272 | workdir="$(mktemp -d)" | ||
273 | |||
274 | # Create CA | ||
275 | openssl genrsa -des3 -passout pass:xxxx -out $workdir/ca.pass.key 2048 | ||
276 | openssl rsa -passin pass:xxxx -in $workdir/ca.pass.key -out $workdir/ca.key | ||
277 | openssl req -new -key $workdir/ca.key -out $workdir/ca.csr \ | ||
278 | -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=Security Department/CN=example.com" | ||
279 | openssl x509 -req -days 1 -in $workdir/ca.csr -signkey $workdir/ca.key -out $workdir/ca.crt | ||
280 | |||
281 | # Create key | ||
282 | openssl genrsa -des3 -passout pass:xxxx -out $workdir/server.pass.key 2048 | ||
283 | openssl rsa -passin pass:xxxx -in $workdir/server.pass.key -out $workdir/server.key | ||
284 | openssl req -new -key $workdir/server.key -out $workdir/server.csr \ | ||
285 | -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com" | ||
286 | openssl x509 -req -days 1 -in $workdir/server.csr -CA $workdir/ca.crt \ | ||
287 | -CAkey $workdir/ca.key -CAserial $workdir/ca.srl -CAcreateserial \ | ||
288 | -out $workdir/server.crt | ||
289 | |||
290 | # Copy key to destination | ||
291 | cp $workdir/server.key /var/lib/${lpath}/key.pem | ||
292 | |||
293 | # Create fullchain.pem (same format as "simp_le ... -f fullchain.pem" creates) | ||
294 | cat $workdir/{server.crt,ca.crt} > "/var/lib/${lpath}/fullchain.pem" | ||
295 | |||
296 | # Create full.pem for e.g. lighttpd | ||
297 | cat $workdir/{server.key,server.crt,ca.crt} > "/var/lib/${lpath}/full.pem" | ||
298 | |||
299 | # Give key acme permissions | ||
300 | chown '${data.user}:${data.group}' "/var/lib/${lpath}/"{key,fullchain,full}.pem | ||
301 | chmod ${rights} "/var/lib/${lpath}/"{key,fullchain,full}.pem | ||
302 | ''; | ||
303 | serviceConfig = { | ||
304 | Type = "oneshot"; | ||
305 | PrivateTmp = true; | ||
306 | StateDirectory = lpath; | ||
307 | User = data.user; | ||
308 | Group = data.group; | ||
309 | }; | ||
310 | unitConfig = { | ||
311 | # Do not create self-signed key when key already exists | ||
312 | ConditionPathExists = "!/var/lib/${lpath}/key.pem"; | ||
313 | }; | ||
314 | }; | ||
315 | in ( | ||
316 | [ { name = "acme-${cert}"; value = acmeService; } ] | ||
317 | ++ optional cfg.preliminarySelfsigned { name = "acme-selfsigned-${cert}"; value = selfsignedService; } | ||
318 | ); | ||
319 | servicesAttr = listToAttrs services; | ||
320 | in | ||
321 | servicesAttr; | ||
322 | |||
323 | # FIXME: this doesn't work for multiple users | ||
324 | systemd.tmpfiles.rules = | ||
325 | flip mapAttrsToList cfg.certs | ||
326 | (cert: data: "d ${data.webroot}/.well-known/acme-challenge - ${data.user} ${data.group}"); | ||
327 | |||
328 | systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair | ||
329 | ("acme-${cert}") | ||
330 | ({ | ||
331 | description = "Renew ACME Certificate for ${cert}"; | ||
332 | wantedBy = [ "timers.target" ]; | ||
333 | timerConfig = { | ||
334 | OnCalendar = cfg.renewInterval; | ||
335 | Unit = "acme-${cert}.service"; | ||
336 | Persistent = "yes"; | ||
337 | AccuracySec = "5m"; | ||
338 | RandomizedDelaySec = "1h"; | ||
339 | }; | ||
340 | }) | ||
341 | ); | ||
342 | |||
343 | systemd.targets.acme-selfsigned-certificates = mkIf cfg.preliminarySelfsigned {}; | ||
344 | systemd.targets.acme-certificates = {}; | ||
345 | }) | ||
346 | |||
347 | ]; | ||
348 | |||
349 | meta = { | ||
350 | maintainers = with lib.maintainers; [ abbradar fpletz globin ]; | ||
351 | #doc = ./acme.xml; | ||
352 | }; | ||
353 | } | ||