From 8d01b0862d3d52d72539cff65a405c09d864f82f Mon Sep 17 00:00:00 2001 From: Lucas Savva Date: Sat, 4 Dec 2021 18:09:43 +0000 Subject: [PATCH] nixos/acme: Update documentation - Added defaultText for all inheritable options. - Add docs on using new defaults option to configure DNS validation for all domains. - Update DNS docs to show using a service to configure rfc2136 instead of manual steps. --- nixos/modules/security/acme.nix | 34 +++- nixos/modules/security/acme.xml | 159 +++++++++++++++--- nixos/modules/services/networking/prosody.xml | 2 +- nixos/modules/services/web-apps/discourse.xml | 2 +- .../modules/services/web-apps/jitsi-meet.xml | 4 +- 5 files changed, 172 insertions(+), 29 deletions(-) diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix index c39653d174e5..1b116482caeb 100644 --- a/nixos/modules/security/acme.nix +++ b/nixos/modules/security/acme.nix @@ -425,6 +425,8 @@ let certConfigs = mapAttrs certToConfig cfg.certs; + mkDefaultText = val: "Inherit from security.acme.defaults, otherwise ${val}" ; + # These options can be specified within # security.acme or security.acme.certs. inheritableOpts = @@ -432,12 +434,14 @@ let validMinDays = mkOption { type = types.int; default = if inheritDefaults then defaults.validMinDays else 30; + defaultText = mkDefaultText "30"; description = "Minimum remaining validity before renewal in days."; }; renewInterval = mkOption { type = types.str; default = if inheritDefaults then defaults.renewInterval else "daily"; + defaultText = mkDefaultText "'daily'"; description = '' Systemd calendar expression when to check for renewal. See systemd.time @@ -452,6 +456,7 @@ let webroot = mkOption { type = types.nullOr types.str; default = if inheritDefaults then defaults.webroot else null; + defaultText = mkDefaultText "null"; example = "/var/lib/acme/acme-challenge"; description = '' Where the webroot of the HTTP vhost is located. @@ -465,6 +470,7 @@ let server = mkOption { type = types.nullOr types.str; default = if inheritDefaults then defaults.server else null; + defaultText = mkDefaultText "null"; description = '' ACME Directory Resource URI. Defaults to Let's Encrypt's production endpoint, @@ -475,6 +481,7 @@ let email = mkOption { type = types.str; default = if inheritDefaults then defaults.email else null; + defaultText = mkDefaultText "null"; description = '' Email address for account creation and correspondence from the CA. It is recommended to use the same email for all certs to avoid account @@ -485,12 +492,14 @@ let group = mkOption { type = types.str; default = if inheritDefaults then defaults.group else "acme"; + defaultText = mkDefaultText "'acme'"; description = "Group running the ACME client."; }; reloadServices = mkOption { type = types.listOf types.str; default = if inheritDefaults then defaults.reloadServices else []; + defaultText = mkDefaultText "[]"; description = '' The list of systemd services to call systemctl try-reload-or-restart on. @@ -500,6 +509,7 @@ let postRun = mkOption { type = types.lines; default = if inheritDefaults then defaults.postRun else ""; + defaultText = mkDefaultText "''"; example = "cp full.pem backup.pem"; description = '' Commands to run after new certificates go live. Note that @@ -512,6 +522,7 @@ let keyType = mkOption { type = types.str; default = if inheritDefaults then defaults.keyType else "ec256"; + defaultText = mkDefaultText "'ec256'"; description = '' Key type to use for private keys. For an up to date list of supported values check the --key-type option @@ -522,6 +533,7 @@ let dnsProvider = mkOption { type = types.nullOr types.str; default = if inheritDefaults then defaults.dnsProvider else null; + defaultText = mkDefaultText "null"; example = "route53"; description = '' DNS Challenge provider. For a list of supported providers, see the "code" @@ -532,6 +544,7 @@ let dnsResolver = mkOption { type = types.nullOr types.str; default = if inheritDefaults then defaults.dnsResolver else null; + defaultText = mkDefaultText "null"; example = "1.1.1.1:53"; description = '' Set the resolver to use for performing recursive DNS queries. Supported: @@ -543,6 +556,7 @@ let credentialsFile = mkOption { type = types.path; default = if inheritDefaults then defaults.credentialsFile else null; + defaultText = mkDefaultText "null"; description = '' Path to an EnvironmentFile for the cert's service containing any required and optional environment variables for your selected dnsProvider. @@ -555,6 +569,7 @@ let dnsPropagationCheck = mkOption { type = types.bool; default = if inheritDefaults then defaults.dnsPropagationCheck else true; + defaultText = mkDefaultText "true"; description = '' Toggles lego DNS propagation check, which is used alongside DNS-01 challenge to ensure the DNS entries required are available. @@ -564,6 +579,7 @@ let ocspMustStaple = mkOption { type = types.bool; default = if inheritDefaults then defaults.ocspMustStaple else false; + defaultText = mkDefaultText "false"; description = '' Turns on the OCSP Must-Staple TLS extension. Make sure you know what you're doing! See: @@ -577,6 +593,7 @@ let extraLegoFlags = mkOption { type = types.listOf types.str; default = if inheritDefaults then defaults.extraLegoFlags else []; + defaultText = mkDefaultText "[]"; description = '' Additional global flags to pass to all lego commands. ''; @@ -585,6 +602,7 @@ let extraLegoRenewFlags = mkOption { type = types.listOf types.str; default = if inheritDefaults then defaults.extraLegoRenewFlags else []; + defaultText = mkDefaultText "[]"; description = '' Additional flags to pass to lego renew. ''; @@ -593,14 +611,24 @@ let extraLegoRunFlags = mkOption { type = types.listOf types.str; default = if inheritDefaults then defaults.extraLegoRunFlags else []; + defaultText = mkDefaultText "[]"; description = '' Additional flags to pass to lego run. ''; }; }; - certOpts = { name, ... }: { - options = (inheritableOpts { inherit (cfg) defaults; inheritDefaults = cfg.certs."${name}".inheritDefaults; }) // { + certOpts = { name, config, ... }: { + options = (inheritableOpts { + inherit (cfg) defaults; + # During doc generation, name = "" and doesn't really + # exist as a cert. As such, handle undfined certs. + inheritDefaults = (lib.attrByPath + [name] + { inheritDefaults = false; } + cfg.certs + ).inheritDefaults; + }) // { # user option has been removed user = mkOption { visible = false; @@ -696,7 +724,7 @@ in { }; defaults = mkOption { - type = types.submodule ({ ... }: { options = inheritableOpts {}; }); + type = types.submodule { options = inheritableOpts {}; }; description = '' Default values inheritable by all configured certs. You can use this to define options shared by all your certs. These defaults diff --git a/nixos/modules/security/acme.xml b/nixos/modules/security/acme.xml index bf93800a0af4..f623cc509be6 100644 --- a/nixos/modules/security/acme.xml +++ b/nixos/modules/security/acme.xml @@ -7,8 +7,9 @@ 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 lego - is used under the hood. + NixOS uses Let's Encrypt. The alternative ACME client + lego is used under + the hood. Automatic cert validation and configuration for Apache and Nginx virtual @@ -29,7 +30,7 @@ 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 - + and/or on a per-cert basis with . This address is only used for registration and renewal reminders, @@ -38,7 +39,7 @@ Alternatively, you can use a different ACME server by changing the - option + option to a provider of your choosing, or just change the server for one cert with . @@ -60,12 +61,12 @@ = true; 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 - foo.example.com the config would look like. + foo.example.com the config would look like this: = true; - = "admin+acme@example.com"; + = "admin+acme@example.com"; services.nginx = { enable = true; virtualHosts = { @@ -114,7 +115,7 @@ services.nginx = { = true; - = "admin+acme@example.com"; + = "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 @@ -218,7 +219,7 @@ services.bind = { # Now we can configure ACME = true; - = "admin+acme@example.com"; + = "admin+acme@example.com"; ."example.com" = { domain = "*.example.com"; dnsProvider = "rfc2136"; @@ -231,25 +232,39 @@ services.bind = { The dnskeys.conf and certs.secret must be kept secure and thus you should not keep their contents in your - Nix config. Instead, generate them one time with these commands: + Nix config. Instead, generate them one time with a systemd service: -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 +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 + # 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 + 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 + ''; +}; @@ -258,6 +273,106 @@ chmod 400 /var/lib/secrets/certs.secret journalctl -fu acme-example.com.service and watching its log output. + +
+ Using DNS validation with web server virtual hosts + + + It is possible to use DNS-01 validation with all certificates, + including those automatically configured via the Nginx/Apache + enableACME + option. This configuration pattern is fully + supported and part of the module's test suite for Nginx + Apache. + + + + You must follow the guide above on configuring DNS-01 validation + first, however instead of setting the options for one certificate + (e.g. ) + you will set them as defaults + (e.g. ). + + + +# Configure ACME appropriately + = true; + = "admin+acme@example.com"; + = { + dnsProvider = "rfc2136"; + credentialsFile = "/var/lib/secrets/certs.secret"; + # We don't need to wait for propagation since this is a local DNS server + dnsPropagationCheck = false; +}; + +# For each virtual host you would like to use DNS-01 validation with, +# set acmeRoot = null +services.nginx = { + enable = true; + virtualHosts = { + "foo.example.com" = { + enableACME = true; + acmeRoot = null; + }; + }; +} + + + + And that's it! Next time your configuration is rebuilt, or when + you add a new virtualHost, it will be DNS-01 validated. + +
+ +
+ Using ACME with services demanding root owned certificates + + + 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 + acme), however you can use systemd's + LoadCredential feature to resolve this elegantly. + Below is an example configuration for OpenSMTPD, but this pattern + can be applied to any service. + + + +# 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. +systemd.services.opensmtpd.requires = ["acme-finished-mail.example.com.target"]; +systemd.services.opensmtpd.serviceConfig.LoadCredential = 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 + ''; +}; + +
+
Regenerating certificates diff --git a/nixos/modules/services/networking/prosody.xml b/nixos/modules/services/networking/prosody.xml index 471240cd1475..6358d744ff78 100644 --- a/nixos/modules/services/networking/prosody.xml +++ b/nixos/modules/services/networking/prosody.xml @@ -72,7 +72,7 @@ services.prosody = { a TLS certificate for the three endponits: security.acme = { - email = "root@example.org"; + email = "root@example.org"; acceptTerms = true; certs = { "example.org" = { diff --git a/nixos/modules/services/web-apps/discourse.xml b/nixos/modules/services/web-apps/discourse.xml index e91d3eac422d..ad9b65abf51e 100644 --- a/nixos/modules/services/web-apps/discourse.xml +++ b/nixos/modules/services/web-apps/discourse.xml @@ -25,7 +25,7 @@ services.discourse = { }; secretKeyBaseFile = "/path/to/secret_key_base_file"; }; -security.acme.email = "me@example.com"; +security.acme.email = "me@example.com"; security.acme.acceptTerms = true; diff --git a/nixos/modules/services/web-apps/jitsi-meet.xml b/nixos/modules/services/web-apps/jitsi-meet.xml index 97373bc6d9a8..ff44c724adf4 100644 --- a/nixos/modules/services/web-apps/jitsi-meet.xml +++ b/nixos/modules/services/web-apps/jitsi-meet.xml @@ -20,7 +20,7 @@ }; services.jitsi-videobridge.openFirewall = true; networking.firewall.allowedTCPPorts = [ 80 443 ]; - security.acme.email = "me@example.com"; + security.acme.email = "me@example.com"; security.acme.acceptTerms = true; } @@ -46,7 +46,7 @@ }; services.jitsi-videobridge.openFirewall = true; networking.firewall.allowedTCPPorts = [ 80 443 ]; - security.acme.email = "me@example.com"; + security.acme.email = "me@example.com"; security.acme.acceptTerms = true; }