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