diff options
Diffstat (limited to 'nixos/modules/security/acme/doc.xml')
-rw-r--r-- | nixos/modules/security/acme/doc.xml | 413 |
1 files changed, 413 insertions, 0 deletions
diff --git a/nixos/modules/security/acme/doc.xml b/nixos/modules/security/acme/doc.xml new file mode 100644 index 00000000000..f623cc509be --- /dev/null +++ b/nixos/modules/security/acme/doc.xml @@ -0,0 +1,413 @@ +<chapter xmlns="http://docbook.org/ns/docbook" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:xi="http://www.w3.org/2001/XInclude" + version="5.0" + xml:id="module-security-acme"> + <title>SSL/TLS Certificates with ACME</title> + <para> + NixOS supports automatic domain validation & certificate retrieval and + renewal using the ACME protocol. Any provider can be used, but by default + NixOS uses Let's Encrypt. The alternative ACME client + <link xlink:href="https://go-acme.github.io/lego/">lego</link> is used under + the hood. + </para> + <para> + Automatic cert validation and configuration for Apache and Nginx virtual + hosts is included in NixOS, however if you would like to generate a wildcard + cert or you are not using a web server you will have to configure DNS + based validation. + </para> + <section xml:id="module-security-acme-prerequisites"> + <title>Prerequisites</title> + + <para> + To use the ACME module, you must accept the provider's terms of service + by setting <literal><xref linkend="opt-security.acme.acceptTerms" /></literal> + to <literal>true</literal>. The Let's Encrypt ToS can be found + <link xlink:href="https://letsencrypt.org/repository/">here</link>. + </para> + + <para> + You must also set an email address to be used when creating accounts with + Let's Encrypt. You can set this for all certs with + <literal><xref linkend="opt-security.acme.defaults.email" /></literal> + and/or on a per-cert basis with + <literal><xref linkend="opt-security.acme.certs._name_.email" /></literal>. + This address is only used for registration and renewal reminders, + and cannot be used to administer the certificates in any way. + </para> + + <para> + Alternatively, you can use a different ACME server by changing the + <literal><xref linkend="opt-security.acme.defaults.server" /></literal> option + to a provider of your choosing, or just change the server for one cert with + <literal><xref linkend="opt-security.acme.certs._name_.server" /></literal>. + </para> + + <para> + You will need an HTTP server or DNS server for verification. For HTTP, + the server must have a webroot defined that can serve + <filename>.well-known/acme-challenge</filename>. This directory must be + writeable by the user that will run the ACME client. For DNS, you must + set up credentials with your provider/server for use with lego. + </para> + </section> + <section xml:id="module-security-acme-nginx"> + <title>Using ACME certificates in Nginx</title> + + <para> + NixOS supports fetching ACME certificates for you by setting + <literal><link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> + = true;</literal> in a virtualHost config. We first create self-signed + placeholder certificates in place of the real ACME certs. The placeholder + certs are overwritten when the ACME certs arrive. For + <literal>foo.example.com</literal> the config would look like this: + </para> + +<programlisting> +<xref linkend="opt-security.acme.acceptTerms" /> = true; +<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com"; +services.nginx = { + <link linkend="opt-services.nginx.enable">enable</link> = true; + <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = { + "foo.example.com" = { + <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true; + <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true; + # All serverAliases will be added as <link linkend="opt-security.acme.certs._name_.extraDomainNames">extra domain names</link> on the certificate. + <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [ "bar.example.com" ]; + locations."/" = { + <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/www"; + }; + }; + + # We can also add a different vhost and reuse the same certificate + # but we have to append extraDomainNames manually. + <link linkend="opt-security.acme.certs._name_.extraDomainNames">security.acme.certs."foo.example.com".extraDomainNames</link> = [ "baz.example.com" ]; + "baz.example.com" = { + <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true; + <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">useACMEHost</link> = "foo.example.com"; + locations."/" = { + <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/www"; + }; + }; + }; +} +</programlisting> + </section> + <section xml:id="module-security-acme-httpd"> + <title>Using ACME certificates in Apache/httpd</title> + + <para> + Using ACME certificates with Apache virtual hosts is identical + to using them with Nginx. The attribute names are all the same, just replace + "nginx" with "httpd" where appropriate. + </para> + </section> + <section xml:id="module-security-acme-configuring"> + <title>Manual configuration of HTTP-01 validation</title> + + <para> + First off you will need to set up a virtual host to serve the challenges. + This example uses a vhost called <literal>certs.example.com</literal>, with + the intent that you will generate certs for all your vhosts and redirect + everyone to HTTPS. + </para> + +<programlisting> +<xref linkend="opt-security.acme.acceptTerms" /> = true; +<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com"; + +# /var/lib/acme/.challenges must be writable by the ACME user +# and readable by the Nginx user. The easiest way to achieve +# this is to add the Nginx user to the ACME group. +<link linkend="opt-users.users._name_.extraGroups">users.users.nginx.extraGroups</link> = [ "acme" ]; + +services.nginx = { + <link linkend="opt-services.nginx.enable">enable</link> = true; + <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = { + "acmechallenge.example.com" = { + # Catchall vhost, will redirect users to HTTPS for all vhosts + <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [ "*.example.com" ]; + locations."/.well-known/acme-challenge" = { + <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/lib/acme/.challenges"; + }; + locations."/" = { + <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.return">return</link> = "301 https://$host$request_uri"; + }; + }; + }; +} +# Alternative config for Apache +<link linkend="opt-users.users._name_.extraGroups">users.users.wwwrun.extraGroups</link> = [ "acme" ]; +services.httpd = { + <link linkend="opt-services.httpd.enable">enable = true;</link> + <link linkend="opt-services.httpd.virtualHosts">virtualHosts</link> = { + "acmechallenge.example.com" = { + # Catchall vhost, will redirect users to HTTPS for all vhosts + <link linkend="opt-services.httpd.virtualHosts._name_.serverAliases">serverAliases</link> = [ "*.example.com" ]; + # /var/lib/acme/.challenges must be writable by the ACME user and readable by the Apache user. + # By default, this is the case. + <link linkend="opt-services.httpd.virtualHosts._name_.documentRoot">documentRoot</link> = "/var/lib/acme/.challenges"; + <link linkend="opt-services.httpd.virtualHosts._name_.extraConfig">extraConfig</link> = '' + RewriteEngine On + RewriteCond %{HTTPS} off + RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge [NC] + RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301] + ''; + }; + }; +} +</programlisting> + + <para> + Now you need to configure ACME to generate a certificate. + </para> + +<programlisting> +<xref linkend="opt-security.acme.certs"/>."foo.example.com" = { + <link linkend="opt-security.acme.certs._name_.webroot">webroot</link> = "/var/lib/acme/.challenges"; + <link linkend="opt-security.acme.certs._name_.email">email</link> = "foo@example.com"; + # Ensure that the web server you use can read the generated certs + # Take a look at the <link linkend="opt-services.nginx.group">group</link> option for the web server you choose. + <link linkend="opt-security.acme.certs._name_.group">group</link> = "nginx"; + # Since we have a wildcard vhost to handle port 80, + # we can generate certs for anything! + # Just make sure your DNS resolves them. + <link linkend="opt-security.acme.certs._name_.extraDomainNames">extraDomainNames</link> = [ "mail.example.com" ]; +}; +</programlisting> + + <para> + The private key <filename>key.pem</filename> and certificate + <filename>fullchain.pem</filename> will be put into + <filename>/var/lib/acme/foo.example.com</filename>. + </para> + + <para> + Refer to <xref linkend="ch-options" /> for all available configuration + options for the <link linkend="opt-security.acme.certs">security.acme</link> + module. + </para> + </section> + <section xml:id="module-security-acme-config-dns"> + <title>Configuring ACME for DNS validation</title> + + <para> + This is useful if you want to generate a wildcard certificate, since + ACME servers will only hand out wildcard certs over DNS validation. + There are a number of supported DNS providers and servers you can utilise, + see the <link xlink:href="https://go-acme.github.io/lego/dns/">lego docs</link> + for provider/server specific configuration values. For the sake of these + docs, we will provide a fully self-hosted example using bind. + </para> + +<programlisting> +services.bind = { + <link linkend="opt-services.bind.enable">enable</link> = true; + <link linkend="opt-services.bind.extraConfig">extraConfig</link> = '' + include "/var/lib/secrets/dnskeys.conf"; + ''; + <link linkend="opt-services.bind.zones">zones</link> = [ + rec { + name = "example.com"; + file = "/var/db/bind/${name}"; + master = true; + extraConfig = "allow-update { key rfc2136key.example.com.; };"; + } + ]; +} + +# Now we can configure ACME +<xref linkend="opt-security.acme.acceptTerms" /> = true; +<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com"; +<xref linkend="opt-security.acme.certs" />."example.com" = { + <link linkend="opt-security.acme.certs._name_.domain">domain</link> = "*.example.com"; + <link linkend="opt-security.acme.certs._name_.dnsProvider">dnsProvider</link> = "rfc2136"; + <link linkend="opt-security.acme.certs._name_.credentialsFile">credentialsFile</link> = "/var/lib/secrets/certs.secret"; + # We don't need to wait for propagation since this is a local DNS server + <link linkend="opt-security.acme.certs._name_.dnsPropagationCheck">dnsPropagationCheck</link> = false; +}; +</programlisting> + + <para> + The <filename>dnskeys.conf</filename> and <filename>certs.secret</filename> + must be kept secure and thus you should not keep their contents in your + Nix config. Instead, generate them one time with a systemd service: + </para> + +<programlisting> +systemd.services.dns-rfc2136-conf = { + requiredBy = ["acme-example.com.service", "bind.service"]; + before = ["acme-example.com.service", "bind.service"]; + unitConfig = { + ConditionPathExists = "!/var/lib/secrets/dnskeys.conf"; + }; + serviceConfig = { + Type = "oneshot"; + UMask = 0077; + }; + path = [ pkgs.bind ]; + script = '' + mkdir -p /var/lib/secrets + tsig-keygen rfc2136key.example.com > /var/lib/secrets/dnskeys.conf + chown named:root /var/lib/secrets/dnskeys.conf + chmod 400 /var/lib/secrets/dnskeys.conf + + # Copy the secret value from the dnskeys.conf, and put it in + # RFC2136_TSIG_SECRET below + + cat > /var/lib/secrets/certs.secret << EOF + RFC2136_NAMESERVER='127.0.0.1:53' + RFC2136_TSIG_ALGORITHM='hmac-sha256.' + RFC2136_TSIG_KEY='rfc2136key.example.com' + RFC2136_TSIG_SECRET='your secret key' + EOF + chmod 400 /var/lib/secrets/certs.secret + ''; +}; +</programlisting> + + <para> + Now you're all set to generate certs! You should monitor the first invocation + by running <literal>systemctl start acme-example.com.service & + journalctl -fu acme-example.com.service</literal> and watching its log output. + </para> + </section> + + <section xml:id="module-security-acme-config-dns-with-vhosts"> + <title>Using DNS validation with web server virtual hosts</title> + + <para> + It is possible to use DNS-01 validation with all certificates, + including those automatically configured via the Nginx/Apache + <literal><link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link></literal> + option. This configuration pattern is fully + supported and part of the module's test suite for Nginx + Apache. + </para> + + <para> + You must follow the guide above on configuring DNS-01 validation + first, however instead of setting the options for one certificate + (e.g. <xref linkend="opt-security.acme.certs._name_.dnsProvider" />) + you will set them as defaults + (e.g. <xref linkend="opt-security.acme.defaults.dnsProvider" />). + </para> + +<programlisting> +# Configure ACME appropriately +<xref linkend="opt-security.acme.acceptTerms" /> = true; +<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com"; +<xref linkend="opt-security.acme.defaults" /> = { + <link linkend="opt-security.acme.defaults.dnsProvider">dnsProvider</link> = "rfc2136"; + <link linkend="opt-security.acme.defaults.credentialsFile">credentialsFile</link> = "/var/lib/secrets/certs.secret"; + # We don't need to wait for propagation since this is a local DNS server + <link linkend="opt-security.acme.defaults.dnsPropagationCheck">dnsPropagationCheck</link> = false; +}; + +# For each virtual host you would like to use DNS-01 validation with, +# set acmeRoot = null +services.nginx = { + <link linkend="opt-services.nginx.enable">enable</link> = true; + <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = { + "foo.example.com" = { + <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true; + <link linkend="opt-services.nginx.virtualHosts._name_.acmeRoot">acmeRoot</link> = null; + }; + }; +} +</programlisting> + + <para> + And that's it! Next time your configuration is rebuilt, or when + you add a new virtualHost, it will be DNS-01 validated. + </para> + </section> + + <section xml:id="module-security-acme-root-owned"> + <title>Using ACME with services demanding root owned certificates</title> + + <para> + Some services refuse to start if the configured certificate files + are not owned by root. PostgreSQL and OpenSMTPD are examples of these. + There is no way to change the user the ACME module uses (it will always be + <literal>acme</literal>), however you can use systemd's + <literal>LoadCredential</literal> feature to resolve this elegantly. + Below is an example configuration for OpenSMTPD, but this pattern + can be applied to any service. + </para> + +<programlisting> +# Configure ACME however you like (DNS or HTTP validation), adding +# the following configuration for the relevant certificate. +# Note: You cannot use `systemctl reload` here as that would mean +# the LoadCredential configuration below would be skipped and +# the service would continue to use old certificates. +security.acme.certs."mail.example.com".postRun = '' + systemctl restart opensmtpd +''; + +# Now you must augment OpenSMTPD's systemd service to load +# the certificate files. +<link linkend="opt-systemd.services._name_.requires">systemd.services.opensmtpd.requires</link> = ["acme-finished-mail.example.com.target"]; +<link linkend="opt-systemd.services._name_.serviceConfig">systemd.services.opensmtpd.serviceConfig.LoadCredential</link> = let + certDir = config.security.acme.certs."mail.example.com".directory; +in [ + "cert.pem:${certDir}/cert.pem" + "key.pem:${certDir}/key.pem" +]; + +# Finally, configure OpenSMTPD to use these certs. +services.opensmtpd = let + credsDir = "/run/credentials/opensmtpd.service"; +in { + enable = true; + setSendmail = false; + serverConfiguration = '' + pki mail.example.com cert "${credsDir}/cert.pem" + pki mail.example.com key "${credsDir}/key.pem" + listen on localhost tls pki mail.example.com + action act1 relay host smtp://127.0.0.1:10027 + match for local action act1 + ''; +}; +</programlisting> + </section> + + <section xml:id="module-security-acme-regenerate"> + <title>Regenerating certificates</title> + + <para> + Should you need to regenerate a particular certificate in a hurry, such + as when a vulnerability is found in Let's Encrypt, there is now a convenient + mechanism for doing so. Running + <literal>systemctl clean --what=state acme-example.com.service</literal> + will remove all certificate files and the account data for the given domain, + allowing you to then <literal>systemctl start acme-example.com.service</literal> + to generate fresh ones. + </para> + </section> + <section xml:id="module-security-acme-fix-jws"> + <title>Fixing JWS Verification error</title> + + <para> + It is possible that your account credentials file may become corrupt and need + to be regenerated. In this scenario lego will produce the error <literal>JWS verification error</literal>. + The solution is to simply delete the associated accounts file and + re-run the affected service(s). + </para> + +<programlisting> +# Find the accounts folder for the certificate +systemctl cat acme-example.com.service | grep -Po 'accounts/[^:]*' +export accountdir="$(!!)" +# Move this folder to some place else +mv /var/lib/acme/.lego/$accountdir{,.bak} +# Recreate the folder using systemd-tmpfiles +systemd-tmpfiles --create +# Get a new account and reissue certificates +# Note: Do this for all certs that share the same account email address +systemctl start acme-example.com.service +</programlisting> + + </section> +</chapter> |