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.
This commit is contained in:
Lucas Savva 2021-12-04 18:09:43 +00:00
parent 07c1583309
commit 8d01b0862d
No known key found for this signature in database
GPG Key ID: E4EC5BF2E2F116A2
5 changed files with 172 additions and 29 deletions

View File

@ -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.<name>
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
<citerefentry><refentrytitle>systemd.time</refentrytitle>
@ -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 <code>systemctl try-reload-or-restart</code>
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 = "<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

View File

@ -7,8 +7,9 @@
<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
@ -29,7 +30,7 @@
<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,
@ -38,7 +39,7 @@
<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>
@ -60,12 +61,12 @@
= 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> = {
@ -114,7 +115,7 @@ services.nginx = {
<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
@ -218,7 +219,7 @@ services.bind = {
# 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";
@ -231,25 +232,39 @@ services.bind = {
<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>
@ -258,6 +273,106 @@ chmod 400 /var/lib/secrets/certs.secret
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>

View File

@ -72,7 +72,7 @@ services.prosody = {
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" = {

View File

@ -25,7 +25,7 @@ services.discourse = {
};
<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>

View File

@ -20,7 +20,7 @@
};
<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>
@ -46,7 +46,7 @@
};
<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>