summary refs log tree commit diff
path: root/nixos/modules/security
diff options
context:
space:
mode:
authorLucas Savva <lucas@m1cr0man.com>2021-12-04 18:09:43 +0000
committerLucas Savva <lucas@m1cr0man.com>2021-12-26 16:49:55 +0000
commit8d01b0862d3d52d72539cff65a405c09d864f82f (patch)
tree8064ed968c2b767e32fbb872851bf4ff8f4d37f6 /nixos/modules/security
parent07c15833093b9db5dacb3829afda03d7c71cc077 (diff)
downloadnixpkgs-8d01b0862d3d52d72539cff65a405c09d864f82f.tar
nixpkgs-8d01b0862d3d52d72539cff65a405c09d864f82f.tar.gz
nixpkgs-8d01b0862d3d52d72539cff65a405c09d864f82f.tar.bz2
nixpkgs-8d01b0862d3d52d72539cff65a405c09d864f82f.tar.lz
nixpkgs-8d01b0862d3d52d72539cff65a405c09d864f82f.tar.xz
nixpkgs-8d01b0862d3d52d72539cff65a405c09d864f82f.tar.zst
nixpkgs-8d01b0862d3d52d72539cff65a405c09d864f82f.zip
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.
Diffstat (limited to 'nixos/modules/security')
-rw-r--r--nixos/modules/security/acme.nix34
-rw-r--r--nixos/modules/security/acme.xml163
2 files changed, 170 insertions, 27 deletions
diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix
index c39653d174e..1b116482cae 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.<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
diff --git a/nixos/modules/security/acme.xml b/nixos/modules/security/acme.xml
index bf93800a0af..f623cc509be 100644
--- a/nixos/modules/security/acme.xml
+++ b/nixos/modules/security/acme.xml
@@ -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
-
-# 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
+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
+
+    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>