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.

Changed files
+172 -29
nixos
modules
security
services
+31 -3
nixos/modules/security/acme.nix
···
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.<name>
inheritableOpts =
···
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
<citerefentry><refentrytitle>systemd.time</refentrytitle>
···
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.
···
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,
···
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
···
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 <code>systemctl try-reload-or-restart</code>
on.
···
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
···
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
···
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"
···
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:
···
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.
···
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.
···
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:
···
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.
'';
···
extraLegoRenewFlags = mkOption {
type = types.listOf types.str;
default = if inheritDefaults then defaults.extraLegoRenewFlags else [];
+
defaultText = mkDefaultText "[]";
description = ''
Additional flags to pass to lego renew.
'';
···
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 = "<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;
···
};
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
+137 -22
nixos/modules/security/acme.xml
···
<para>
NixOS supports automatic domain validation &amp; 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 <literal>lego</literal>
-
is used under the hood.
+
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
···
<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.email" /></literal>
+
<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,
···
<para>
Alternatively, you can use a different ACME server by changing the
-
<literal><xref linkend="opt-security.acme.server" /></literal> option
+
<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>
···
= 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.
+
<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.email" /> = "admin+acme@example.com";
+
<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> = {
···
<programlisting>
<xref linkend="opt-security.acme.acceptTerms" /> = true;
-
<xref linkend="opt-security.acme.email" /> = "admin+acme@example.com";
+
<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
···
# Now we can configure ACME
<xref linkend="opt-security.acme.acceptTerms" /> = true;
-
<xref linkend="opt-security.acme.email" /> = "admin+acme@example.com";
+
<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";
···
<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 these commands:
+
Nix config. Instead, generate them one time with a systemd service:
</para>
<programlisting>
-
mkdir -p /var/lib/secrets
-
tsig-keygen rfc2136key.example.com &gt; /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 &gt; /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 &gt; /var/lib/secrets/certs.secret &lt;&lt; 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 &gt; /var/lib/secrets/certs.secret &lt;&lt; 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>
···
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>
+1 -1
nixos/modules/services/networking/prosody.xml
···
a TLS certificate for the three endponits:
<programlisting>
security.acme = {
-
<link linkend="opt-security.acme.email">email</link> = "root@example.org";
+
<link linkend="opt-security.acme.defaults.email">email</link> = "root@example.org";
<link linkend="opt-security.acme.acceptTerms">acceptTerms</link> = true;
<link linkend="opt-security.acme.certs">certs</link> = {
"example.org" = {
+1 -1
nixos/modules/services/web-apps/discourse.xml
···
};
<link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
};
-
<link linkend="opt-security.acme.email">security.acme.email</link> = "me@example.com";
+
<link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
</programlisting>
</para>
+2 -2
nixos/modules/services/web-apps/jitsi-meet.xml
···
};
<link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
<link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
-
<link linkend="opt-security.acme.email">security.acme.email</link> = "me@example.com";
+
<link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
}</programlisting>
</para>
···
};
<link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
<link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
-
<link linkend="opt-security.acme.email">security.acme.email</link> = "me@example.com";
+
<link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
}</programlisting>
</para>