1<chapter xmlns="http://docbook.org/ns/docbook"
2 xmlns:xlink="http://www.w3.org/1999/xlink"
3 xmlns:xi="http://www.w3.org/2001/XInclude"
4 version="5.0"
5 xml:id="module-security-acme">
6 <title>SSL/TLS Certificates with ACME</title>
7 <para>
8 NixOS supports automatic domain validation & certificate retrieval and
9 renewal using the ACME protocol. Any provider can be used, but by default
10 NixOS uses Let's Encrypt. The alternative ACME client
11 <link xlink:href="https://go-acme.github.io/lego/">lego</link> is used under
12 the hood.
13 </para>
14 <para>
15 Automatic cert validation and configuration for Apache and Nginx virtual
16 hosts is included in NixOS, however if you would like to generate a wildcard
17 cert or you are not using a web server you will have to configure DNS
18 based validation.
19 </para>
20 <section xml:id="module-security-acme-prerequisites">
21 <title>Prerequisites</title>
22
23 <para>
24 To use the ACME module, you must accept the provider's terms of service
25 by setting <literal><xref linkend="opt-security.acme.acceptTerms" /></literal>
26 to <literal>true</literal>. The Let's Encrypt ToS can be found
27 <link xlink:href="https://letsencrypt.org/repository/">here</link>.
28 </para>
29
30 <para>
31 You must also set an email address to be used when creating accounts with
32 Let's Encrypt. You can set this for all certs with
33 <literal><xref linkend="opt-security.acme.defaults.email" /></literal>
34 and/or on a per-cert basis with
35 <literal><xref linkend="opt-security.acme.certs._name_.email" /></literal>.
36 This address is only used for registration and renewal reminders,
37 and cannot be used to administer the certificates in any way.
38 </para>
39
40 <para>
41 Alternatively, you can use a different ACME server by changing the
42 <literal><xref linkend="opt-security.acme.defaults.server" /></literal> option
43 to a provider of your choosing, or just change the server for one cert with
44 <literal><xref linkend="opt-security.acme.certs._name_.server" /></literal>.
45 </para>
46
47 <para>
48 You will need an HTTP server or DNS server for verification. For HTTP,
49 the server must have a webroot defined that can serve
50 <filename>.well-known/acme-challenge</filename>. This directory must be
51 writeable by the user that will run the ACME client. For DNS, you must
52 set up credentials with your provider/server for use with lego.
53 </para>
54 </section>
55 <section xml:id="module-security-acme-nginx">
56 <title>Using ACME certificates in Nginx</title>
57
58 <para>
59 NixOS supports fetching ACME certificates for you by setting
60 <literal><link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link>
61 = true;</literal> in a virtualHost config. We first create self-signed
62 placeholder certificates in place of the real ACME certs. The placeholder
63 certs are overwritten when the ACME certs arrive. For
64 <literal>foo.example.com</literal> the config would look like this:
65 </para>
66
67<programlisting>
68<xref linkend="opt-security.acme.acceptTerms" /> = true;
69<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
70services.nginx = {
71 <link linkend="opt-services.nginx.enable">enable</link> = true;
72 <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
73 "foo.example.com" = {
74 <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
75 <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
76 # All serverAliases will be added as <link linkend="opt-security.acme.certs._name_.extraDomainNames">extra domain names</link> on the certificate.
77 <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [ "bar.example.com" ];
78 locations."/" = {
79 <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/www";
80 };
81 };
82
83 # We can also add a different vhost and reuse the same certificate
84 # but we have to append extraDomainNames manually beforehand:
85 # <link linkend="opt-security.acme.certs._name_.extraDomainNames">security.acme.certs."foo.example.com".extraDomainNames</link> = [ "baz.example.com" ];
86 "baz.example.com" = {
87 <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
88 <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">useACMEHost</link> = "foo.example.com";
89 locations."/" = {
90 <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/www";
91 };
92 };
93 };
94}
95</programlisting>
96 </section>
97 <section xml:id="module-security-acme-httpd">
98 <title>Using ACME certificates in Apache/httpd</title>
99
100 <para>
101 Using ACME certificates with Apache virtual hosts is identical
102 to using them with Nginx. The attribute names are all the same, just replace
103 "nginx" with "httpd" where appropriate.
104 </para>
105 </section>
106 <section xml:id="module-security-acme-configuring">
107 <title>Manual configuration of HTTP-01 validation</title>
108
109 <para>
110 First off you will need to set up a virtual host to serve the challenges.
111 This example uses a vhost called <literal>certs.example.com</literal>, with
112 the intent that you will generate certs for all your vhosts and redirect
113 everyone to HTTPS.
114 </para>
115
116<programlisting>
117<xref linkend="opt-security.acme.acceptTerms" /> = true;
118<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
119
120# /var/lib/acme/.challenges must be writable by the ACME user
121# and readable by the Nginx user. The easiest way to achieve
122# this is to add the Nginx user to the ACME group.
123<link linkend="opt-users.users._name_.extraGroups">users.users.nginx.extraGroups</link> = [ "acme" ];
124
125services.nginx = {
126 <link linkend="opt-services.nginx.enable">enable</link> = true;
127 <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
128 "acmechallenge.example.com" = {
129 # Catchall vhost, will redirect users to HTTPS for all vhosts
130 <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [ "*.example.com" ];
131 locations."/.well-known/acme-challenge" = {
132 <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/lib/acme/.challenges";
133 };
134 locations."/" = {
135 <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.return">return</link> = "301 https://$host$request_uri";
136 };
137 };
138 };
139}
140# Alternative config for Apache
141<link linkend="opt-users.users._name_.extraGroups">users.users.wwwrun.extraGroups</link> = [ "acme" ];
142services.httpd = {
143 <link linkend="opt-services.httpd.enable">enable = true;</link>
144 <link linkend="opt-services.httpd.virtualHosts">virtualHosts</link> = {
145 "acmechallenge.example.com" = {
146 # Catchall vhost, will redirect users to HTTPS for all vhosts
147 <link linkend="opt-services.httpd.virtualHosts._name_.serverAliases">serverAliases</link> = [ "*.example.com" ];
148 # /var/lib/acme/.challenges must be writable by the ACME user and readable by the Apache user.
149 # By default, this is the case.
150 <link linkend="opt-services.httpd.virtualHosts._name_.documentRoot">documentRoot</link> = "/var/lib/acme/.challenges";
151 <link linkend="opt-services.httpd.virtualHosts._name_.extraConfig">extraConfig</link> = ''
152 RewriteEngine On
153 RewriteCond %{HTTPS} off
154 RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge [NC]
155 RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301]
156 '';
157 };
158 };
159}
160</programlisting>
161
162 <para>
163 Now you need to configure ACME to generate a certificate.
164 </para>
165
166<programlisting>
167<xref linkend="opt-security.acme.certs"/>."foo.example.com" = {
168 <link linkend="opt-security.acme.certs._name_.webroot">webroot</link> = "/var/lib/acme/.challenges";
169 <link linkend="opt-security.acme.certs._name_.email">email</link> = "foo@example.com";
170 # Ensure that the web server you use can read the generated certs
171 # Take a look at the <link linkend="opt-services.nginx.group">group</link> option for the web server you choose.
172 <link linkend="opt-security.acme.certs._name_.group">group</link> = "nginx";
173 # Since we have a wildcard vhost to handle port 80,
174 # we can generate certs for anything!
175 # Just make sure your DNS resolves them.
176 <link linkend="opt-security.acme.certs._name_.extraDomainNames">extraDomainNames</link> = [ "mail.example.com" ];
177};
178</programlisting>
179
180 <para>
181 The private key <filename>key.pem</filename> and certificate
182 <filename>fullchain.pem</filename> will be put into
183 <filename>/var/lib/acme/foo.example.com</filename>.
184 </para>
185
186 <para>
187 Refer to <xref linkend="ch-options" /> for all available configuration
188 options for the <link linkend="opt-security.acme.certs">security.acme</link>
189 module.
190 </para>
191 </section>
192 <section xml:id="module-security-acme-config-dns">
193 <title>Configuring ACME for DNS validation</title>
194
195 <para>
196 This is useful if you want to generate a wildcard certificate, since
197 ACME servers will only hand out wildcard certs over DNS validation.
198 There are a number of supported DNS providers and servers you can utilise,
199 see the <link xlink:href="https://go-acme.github.io/lego/dns/">lego docs</link>
200 for provider/server specific configuration values. For the sake of these
201 docs, we will provide a fully self-hosted example using bind.
202 </para>
203
204<programlisting>
205services.bind = {
206 <link linkend="opt-services.bind.enable">enable</link> = true;
207 <link linkend="opt-services.bind.extraConfig">extraConfig</link> = ''
208 include "/var/lib/secrets/dnskeys.conf";
209 '';
210 <link linkend="opt-services.bind.zones">zones</link> = [
211 rec {
212 name = "example.com";
213 file = "/var/db/bind/${name}";
214 master = true;
215 extraConfig = "allow-update { key rfc2136key.example.com.; };";
216 }
217 ];
218}
219
220# Now we can configure ACME
221<xref linkend="opt-security.acme.acceptTerms" /> = true;
222<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
223<xref linkend="opt-security.acme.certs" />."example.com" = {
224 <link linkend="opt-security.acme.certs._name_.domain">domain</link> = "*.example.com";
225 <link linkend="opt-security.acme.certs._name_.dnsProvider">dnsProvider</link> = "rfc2136";
226 <link linkend="opt-security.acme.certs._name_.credentialsFile">credentialsFile</link> = "/var/lib/secrets/certs.secret";
227 # We don't need to wait for propagation since this is a local DNS server
228 <link linkend="opt-security.acme.certs._name_.dnsPropagationCheck">dnsPropagationCheck</link> = false;
229};
230</programlisting>
231
232 <para>
233 The <filename>dnskeys.conf</filename> and <filename>certs.secret</filename>
234 must be kept secure and thus you should not keep their contents in your
235 Nix config. Instead, generate them one time with a systemd service:
236 </para>
237
238<programlisting>
239systemd.services.dns-rfc2136-conf = {
240 requiredBy = ["acme-example.com.service" "bind.service"];
241 before = ["acme-example.com.service" "bind.service"];
242 unitConfig = {
243 ConditionPathExists = "!/var/lib/secrets/dnskeys.conf";
244 };
245 serviceConfig = {
246 Type = "oneshot";
247 UMask = 0077;
248 };
249 path = [ pkgs.bind ];
250 script = ''
251 mkdir -p /var/lib/secrets
252 chmod 755 /var/lib/secrets
253 tsig-keygen rfc2136key.example.com > /var/lib/secrets/dnskeys.conf
254 chown named:root /var/lib/secrets/dnskeys.conf
255 chmod 400 /var/lib/secrets/dnskeys.conf
256
257 # extract secret value from the dnskeys.conf
258 while read x y; do if [ "$x" = "secret" ]; then secret="''${y:1:''${#y}-3}"; fi; done < /var/lib/secrets/dnskeys.conf
259
260 cat > /var/lib/secrets/certs.secret << EOF
261 RFC2136_NAMESERVER='127.0.0.1:53'
262 RFC2136_TSIG_ALGORITHM='hmac-sha256.'
263 RFC2136_TSIG_KEY='rfc2136key.example.com'
264 RFC2136_TSIG_SECRET='$secret'
265 EOF
266 chmod 400 /var/lib/secrets/certs.secret
267 '';
268};
269</programlisting>
270
271 <para>
272 Now you're all set to generate certs! You should monitor the first invocation
273 by running <literal>systemctl start acme-example.com.service &
274 journalctl -fu acme-example.com.service</literal> and watching its log output.
275 </para>
276 </section>
277
278 <section xml:id="module-security-acme-config-dns-with-vhosts">
279 <title>Using DNS validation with web server virtual hosts</title>
280
281 <para>
282 It is possible to use DNS-01 validation with all certificates,
283 including those automatically configured via the Nginx/Apache
284 <literal><link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link></literal>
285 option. This configuration pattern is fully
286 supported and part of the module's test suite for Nginx + Apache.
287 </para>
288
289 <para>
290 You must follow the guide above on configuring DNS-01 validation
291 first, however instead of setting the options for one certificate
292 (e.g. <xref linkend="opt-security.acme.certs._name_.dnsProvider" />)
293 you will set them as defaults
294 (e.g. <xref linkend="opt-security.acme.defaults.dnsProvider" />).
295 </para>
296
297<programlisting>
298# Configure ACME appropriately
299<xref linkend="opt-security.acme.acceptTerms" /> = true;
300<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
301<xref linkend="opt-security.acme.defaults" /> = {
302 <link linkend="opt-security.acme.defaults.dnsProvider">dnsProvider</link> = "rfc2136";
303 <link linkend="opt-security.acme.defaults.credentialsFile">credentialsFile</link> = "/var/lib/secrets/certs.secret";
304 # We don't need to wait for propagation since this is a local DNS server
305 <link linkend="opt-security.acme.defaults.dnsPropagationCheck">dnsPropagationCheck</link> = false;
306};
307
308# For each virtual host you would like to use DNS-01 validation with,
309# set acmeRoot = null
310services.nginx = {
311 <link linkend="opt-services.nginx.enable">enable</link> = true;
312 <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
313 "foo.example.com" = {
314 <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
315 <link linkend="opt-services.nginx.virtualHosts._name_.acmeRoot">acmeRoot</link> = null;
316 };
317 };
318}
319</programlisting>
320
321 <para>
322 And that's it! Next time your configuration is rebuilt, or when
323 you add a new virtualHost, it will be DNS-01 validated.
324 </para>
325 </section>
326
327 <section xml:id="module-security-acme-root-owned">
328 <title>Using ACME with services demanding root owned certificates</title>
329
330 <para>
331 Some services refuse to start if the configured certificate files
332 are not owned by root. PostgreSQL and OpenSMTPD are examples of these.
333 There is no way to change the user the ACME module uses (it will always be
334 <literal>acme</literal>), however you can use systemd's
335 <literal>LoadCredential</literal> feature to resolve this elegantly.
336 Below is an example configuration for OpenSMTPD, but this pattern
337 can be applied to any service.
338 </para>
339
340<programlisting>
341# Configure ACME however you like (DNS or HTTP validation), adding
342# the following configuration for the relevant certificate.
343# Note: You cannot use `systemctl reload` here as that would mean
344# the LoadCredential configuration below would be skipped and
345# the service would continue to use old certificates.
346security.acme.certs."mail.example.com".postRun = ''
347 systemctl restart opensmtpd
348'';
349
350# Now you must augment OpenSMTPD's systemd service to load
351# the certificate files.
352<link linkend="opt-systemd.services._name_.requires">systemd.services.opensmtpd.requires</link> = ["acme-finished-mail.example.com.target"];
353<link linkend="opt-systemd.services._name_.serviceConfig">systemd.services.opensmtpd.serviceConfig.LoadCredential</link> = let
354 certDir = config.security.acme.certs."mail.example.com".directory;
355in [
356 "cert.pem:${certDir}/cert.pem"
357 "key.pem:${certDir}/key.pem"
358];
359
360# Finally, configure OpenSMTPD to use these certs.
361services.opensmtpd = let
362 credsDir = "/run/credentials/opensmtpd.service";
363in {
364 enable = true;
365 setSendmail = false;
366 serverConfiguration = ''
367 pki mail.example.com cert "${credsDir}/cert.pem"
368 pki mail.example.com key "${credsDir}/key.pem"
369 listen on localhost tls pki mail.example.com
370 action act1 relay host smtp://127.0.0.1:10027
371 match for local action act1
372 '';
373};
374</programlisting>
375 </section>
376
377 <section xml:id="module-security-acme-regenerate">
378 <title>Regenerating certificates</title>
379
380 <para>
381 Should you need to regenerate a particular certificate in a hurry, such
382 as when a vulnerability is found in Let's Encrypt, there is now a convenient
383 mechanism for doing so. Running
384 <literal>systemctl clean --what=state acme-example.com.service</literal>
385 will remove all certificate files and the account data for the given domain,
386 allowing you to then <literal>systemctl start acme-example.com.service</literal>
387 to generate fresh ones.
388 </para>
389 </section>
390 <section xml:id="module-security-acme-fix-jws">
391 <title>Fixing JWS Verification error</title>
392
393 <para>
394 It is possible that your account credentials file may become corrupt and need
395 to be regenerated. In this scenario lego will produce the error <literal>JWS verification error</literal>.
396 The solution is to simply delete the associated accounts file and
397 re-run the affected service(s).
398 </para>
399
400<programlisting>
401# Find the accounts folder for the certificate
402systemctl cat acme-example.com.service | grep -Po 'accounts/[^:]*'
403export accountdir="$(!!)"
404# Move this folder to some place else
405mv /var/lib/acme/.lego/$accountdir{,.bak}
406# Recreate the folder using systemd-tmpfiles
407systemd-tmpfiles --create
408# Get a new account and reissue certificates
409# Note: Do this for all certs that share the same account email address
410systemctl start acme-example.com.service
411</programlisting>
412
413 </section>
414</chapter>