diff options
author | Winter <winter@winter.cafe> | 2022-01-08 15:05:34 -0500 |
---|---|---|
committer | Winter <winter@winter.cafe> | 2022-01-08 15:05:34 -0500 |
commit | b52607f43b11319edb716d65bbecbfdbf2f5b92b (patch) | |
tree | 29e66e146b373f2f65203bf9451b42e18ad39b81 /nixos/modules/security/acme | |
parent | 85a078a25d7d41d805ef5fb3e90af7476d5fefd4 (diff) | |
download | nixpkgs-b52607f43b11319edb716d65bbecbfdbf2f5b92b.tar nixpkgs-b52607f43b11319edb716d65bbecbfdbf2f5b92b.tar.gz nixpkgs-b52607f43b11319edb716d65bbecbfdbf2f5b92b.tar.bz2 nixpkgs-b52607f43b11319edb716d65bbecbfdbf2f5b92b.tar.lz nixpkgs-b52607f43b11319edb716d65bbecbfdbf2f5b92b.tar.xz nixpkgs-b52607f43b11319edb716d65bbecbfdbf2f5b92b.tar.zst nixpkgs-b52607f43b11319edb716d65bbecbfdbf2f5b92b.zip |
nixos/acme: ensure web servers using certs can access them
Diffstat (limited to 'nixos/modules/security/acme')
-rw-r--r-- | nixos/modules/security/acme/default.nix | 921 | ||||
-rw-r--r-- | nixos/modules/security/acme/doc.xml | 413 | ||||
-rw-r--r-- | nixos/modules/security/acme/mk-cert-ownership-assertion.nix | 4 |
3 files changed, 1338 insertions, 0 deletions
diff --git a/nixos/modules/security/acme/default.nix b/nixos/modules/security/acme/default.nix new file mode 100644 index 00000000000..d827c448055 --- /dev/null +++ b/nixos/modules/security/acme/default.nix @@ -0,0 +1,921 @@ +{ config, lib, pkgs, options, ... }: +with lib; +let + cfg = config.security.acme; + opt = options.security.acme; + user = if cfg.useRoot then "root" else "acme"; + + # Used to calculate timer accuracy for coalescing + numCerts = length (builtins.attrNames cfg.certs); + _24hSecs = 60 * 60 * 24; + + # Used to make unique paths for each cert/account config set + mkHash = with builtins; val: substring 0 20 (hashString "sha256" val); + mkAccountHash = acmeServer: data: mkHash "${toString acmeServer} ${data.keyType} ${data.email}"; + accountDirRoot = "/var/lib/acme/.lego/accounts/"; + + # There are many services required to make cert renewals work. + # They all follow a common structure: + # - They inherit this commonServiceConfig + # - They all run as the acme user + # - They all use BindPath and StateDirectory where possible + # to set up a sort of build environment in /tmp + # The Group can vary depending on what the user has specified in + # security.acme.certs.<cert>.group on some of the services. + commonServiceConfig = { + Type = "oneshot"; + User = user; + Group = mkDefault "acme"; + UMask = 0022; + StateDirectoryMode = 750; + ProtectSystem = "strict"; + ReadWritePaths = [ + "/var/lib/acme" + ]; + PrivateTmp = true; + + WorkingDirectory = "/tmp"; + + CapabilityBoundingSet = [ "" ]; + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + ProtectClock = true; + ProtectHome = true; + ProtectHostname = true; + ProtectControlGroups = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + # 1. allow a reasonable set of syscalls + "@system-service" + # 2. and deny unreasonable ones + "~@privileged @resources" + # 3. then allow the required subset within denied groups + "@chown" + ]; + }; + + # In order to avoid race conditions creating the CA for selfsigned certs, + # we have a separate service which will create the necessary files. + selfsignCAService = { + description = "Generate self-signed certificate authority"; + + path = with pkgs; [ minica ]; + + unitConfig = { + ConditionPathExists = "!/var/lib/acme/.minica/key.pem"; + StartLimitIntervalSec = 0; + }; + + serviceConfig = commonServiceConfig // { + StateDirectory = "acme/.minica"; + BindPaths = "/var/lib/acme/.minica:/tmp/ca"; + UMask = 0077; + }; + + # Working directory will be /tmp + script = '' + minica \ + --ca-key ca/key.pem \ + --ca-cert ca/cert.pem \ + --domains selfsigned.local + ''; + }; + + # Ensures that directories which are shared across all certs + # exist and have the correct user and group, since group + # is configurable on a per-cert basis. + userMigrationService = let + script = with builtins; '' + chown -R ${user} .lego/accounts + '' + (concatStringsSep "\n" (mapAttrsToList (cert: data: '' + for fixpath in ${escapeShellArg cert} .lego/${escapeShellArg cert}; do + if [ -d "$fixpath" ]; then + chmod -R u=rwX,g=rX,o= "$fixpath" + chown -R ${user}:${data.group} "$fixpath" + fi + done + '') certConfigs)); + in { + description = "Fix owner and group of all ACME certificates"; + + serviceConfig = commonServiceConfig // { + # We don't want this to run every time a renewal happens + RemainAfterExit = true; + + # These StateDirectory entries negate the need for tmpfiles + StateDirectory = [ "acme" "acme/.lego" "acme/.lego/accounts" ]; + StateDirectoryMode = 755; + WorkingDirectory = "/var/lib/acme"; + + # Run the start script as root + ExecStart = "+" + (pkgs.writeShellScript "acme-fixperms" script); + }; + }; + + certToConfig = cert: data: let + acmeServer = data.server; + useDns = data.dnsProvider != null; + destPath = "/var/lib/acme/${cert}"; + selfsignedDeps = optionals (cfg.preliminarySelfsigned) [ "acme-selfsigned-${cert}.service" ]; + + # Minica and lego have a "feature" which replaces * with _. We need + # to make this substitution to reference the output files from both programs. + # End users never see this since we rename the certs. + keyName = builtins.replaceStrings ["*"] ["_"] data.domain; + + # FIXME when mkChangedOptionModule supports submodules, change to that. + # This is a workaround + extraDomains = data.extraDomainNames ++ ( + optionals + (data.extraDomains != "_mkMergedOptionModule") + (builtins.attrNames data.extraDomains) + ); + + # Create hashes for cert data directories based on configuration + # Flags are separated to avoid collisions + hashData = with builtins; '' + ${concatStringsSep " " data.extraLegoFlags} - + ${concatStringsSep " " data.extraLegoRunFlags} - + ${concatStringsSep " " data.extraLegoRenewFlags} - + ${toString acmeServer} ${toString data.dnsProvider} + ${toString data.ocspMustStaple} ${data.keyType} + ''; + certDir = mkHash hashData; + # TODO remove domainHash usage entirely. Waiting on go-acme/lego#1532 + domainHash = mkHash "${concatStringsSep " " extraDomains} ${data.domain}"; + accountHash = (mkAccountHash acmeServer data); + accountDir = accountDirRoot + accountHash; + + protocolOpts = if useDns then ( + [ "--dns" data.dnsProvider ] + ++ optionals (!data.dnsPropagationCheck) [ "--dns.disable-cp" ] + ++ optionals (data.dnsResolver != null) [ "--dns.resolvers" data.dnsResolver ] + ) else if data.listenHTTP != null then [ "--http" "--http.port" data.listenHTTP ] + else [ "--http" "--http.webroot" data.webroot ]; + + commonOpts = [ + "--accept-tos" # Checking the option is covered by the assertions + "--path" "." + "-d" data.domain + "--email" data.email + "--key-type" data.keyType + ] ++ protocolOpts + ++ optionals (acmeServer != null) [ "--server" acmeServer ] + ++ concatMap (name: [ "-d" name ]) extraDomains + ++ data.extraLegoFlags; + + # Although --must-staple is common to both modes, it is not declared as a + # mode-agnostic argument in lego and thus must come after the mode. + runOpts = escapeShellArgs ( + commonOpts + ++ [ "run" ] + ++ optionals data.ocspMustStaple [ "--must-staple" ] + ++ data.extraLegoRunFlags + ); + renewOpts = escapeShellArgs ( + commonOpts + ++ [ "renew" ] + ++ optionals data.ocspMustStaple [ "--must-staple" ] + ++ data.extraLegoRenewFlags + ); + + # We need to collect all the ACME webroots to grant them write + # access in the systemd service. + webroots = + lib.remove null + (lib.unique + (builtins.map + (certAttrs: certAttrs.webroot) + (lib.attrValues config.security.acme.certs))); + in { + inherit accountHash cert selfsignedDeps; + + group = data.group; + + renewTimer = { + description = "Renew ACME Certificate for ${cert}"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = data.renewInterval; + Unit = "acme-${cert}.service"; + Persistent = "yes"; + + # Allow systemd to pick a convenient time within the day + # to run the check. + # This allows the coalescing of multiple timer jobs. + # We divide by the number of certificates so that if you + # have many certificates, the renewals are distributed over + # the course of the day to avoid rate limits. + AccuracySec = "${toString (_24hSecs / numCerts)}s"; + + # Skew randomly within the day, per https://letsencrypt.org/docs/integration-guide/. + RandomizedDelaySec = "24h"; + }; + }; + + selfsignService = { + description = "Generate self-signed certificate for ${cert}"; + after = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ]; + requires = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ]; + + path = with pkgs; [ minica ]; + + unitConfig = { + ConditionPathExists = "!/var/lib/acme/${cert}/key.pem"; + StartLimitIntervalSec = 0; + }; + + serviceConfig = commonServiceConfig // { + Group = data.group; + UMask = 0027; + + StateDirectory = "acme/${cert}"; + + BindPaths = [ + "/var/lib/acme/.minica:/tmp/ca" + "/var/lib/acme/${cert}:/tmp/${keyName}" + ]; + }; + + # Working directory will be /tmp + # minica will output to a folder sharing the name of the first domain + # in the list, which will be ${data.domain} + script = '' + minica \ + --ca-key ca/key.pem \ + --ca-cert ca/cert.pem \ + --domains ${escapeShellArg (builtins.concatStringsSep "," ([ data.domain ] ++ extraDomains))} + + # Create files to match directory layout for real certificates + cd '${keyName}' + cp ../ca/cert.pem chain.pem + cat cert.pem chain.pem > fullchain.pem + cat key.pem fullchain.pem > full.pem + + # Group might change between runs, re-apply it + chown '${user}:${data.group}' * + + # Default permissions make the files unreadable by group + anon + # Need to be readable by group + chmod 640 * + ''; + }; + + renewService = { + description = "Renew ACME certificate for ${cert}"; + after = [ "network.target" "network-online.target" "acme-fixperms.service" "nss-lookup.target" ] ++ selfsignedDeps; + wants = [ "network-online.target" "acme-fixperms.service" ] ++ selfsignedDeps; + + # https://github.com/NixOS/nixpkgs/pull/81371#issuecomment-605526099 + wantedBy = optionals (!config.boot.isContainer) [ "multi-user.target" ]; + + path = with pkgs; [ lego coreutils diffutils openssl ]; + + serviceConfig = commonServiceConfig // { + Group = data.group; + + # Keep in mind that these directories will be deleted if the user runs + # systemctl clean --what=state + # acme/.lego/${cert} is listed for this reason. + StateDirectory = [ + "acme/${cert}" + "acme/.lego/${cert}" + "acme/.lego/${cert}/${certDir}" + "acme/.lego/accounts/${accountHash}" + ]; + + ReadWritePaths = commonServiceConfig.ReadWritePaths ++ webroots; + + # Needs to be space separated, but can't use a multiline string because that'll include newlines + BindPaths = [ + "${accountDir}:/tmp/accounts" + "/var/lib/acme/${cert}:/tmp/out" + "/var/lib/acme/.lego/${cert}/${certDir}:/tmp/certificates" + ]; + + # Only try loading the credentialsFile if the dns challenge is enabled + EnvironmentFile = mkIf useDns data.credentialsFile; + + # Run as root (Prefixed with +) + ExecStartPost = "+" + (pkgs.writeShellScript "acme-postrun" '' + cd /var/lib/acme/${escapeShellArg cert} + if [ -e renewed ]; then + rm renewed + ${data.postRun} + ${optionalString (data.reloadServices != []) + "systemctl --no-block try-reload-or-restart ${escapeShellArgs data.reloadServices}" + } + fi + ''); + } // optionalAttrs (data.listenHTTP != null && toInt (elemAt (splitString ":" data.listenHTTP) 1) < 1024) { + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; + }; + + # Working directory will be /tmp + script = '' + ${optionalString data.enableDebugLogs "set -x"} + set -euo pipefail + + # This reimplements the expiration date check, but without querying + # the acme server first. By doing this offline, we avoid errors + # when the network or DNS are unavailable, which can happen during + # nixos-rebuild switch. + is_expiration_skippable() { + pem=$1 + + # This function relies on set -e to exit early if any of the + # conditions or programs fail. + + [[ -e $pem ]] + + expiration_line="$( + set -euxo pipefail + openssl x509 -noout -enddate <$pem \ + | grep notAfter \ + | sed -e 's/^notAfter=//' + )" + [[ -n "$expiration_line" ]] + + expiration_date="$(date -d "$expiration_line" +%s)" + now="$(date +%s)" + expiration_s=$[expiration_date - now] + expiration_days=$[expiration_s / (3600 * 24)] # rounds down + + [[ $expiration_days -gt ${toString data.validMinDays} ]] + } + + ${optionalString (data.webroot != null) '' + # Ensure the webroot exists. Fixing group is required in case configuration was changed between runs. + # Lego will fail if the webroot does not exist at all. + ( + mkdir -p '${data.webroot}/.well-known/acme-challenge' \ + && chgrp '${data.group}' ${data.webroot}/.well-known/acme-challenge + ) || ( + echo 'Please ensure ${data.webroot}/.well-known/acme-challenge exists and is writable by acme:${data.group}' \ + && exit 1 + ) + ''} + + echo '${domainHash}' > domainhash.txt + + # Check if we can renew. + # We can only renew if the list of domains has not changed. + if cmp -s domainhash.txt certificates/domainhash.txt && [ -e 'certificates/${keyName}.key' -a -e 'certificates/${keyName}.crt' -a -n "$(ls -1 accounts)" ]; then + + # Even if a cert is not expired, it may be revoked by the CA. + # Try to renew, and silently fail if the cert is not expired. + # Avoids #85794 and resolves #129838 + if ! lego ${renewOpts} --days ${toString data.validMinDays}; then + if is_expiration_skippable out/full.pem; then + echo 1>&2 "nixos-acme: Ignoring failed renewal because expiration isn't within the coming ${toString data.validMinDays} days" + else + # High number to avoid Systemd reserved codes. + exit 11 + fi + fi + + # Otherwise do a full run + elif ! lego ${runOpts}; then + # Produce a nice error for those doing their first nixos-rebuild with these certs + echo Failed to fetch certificates. \ + This may mean your DNS records are set up incorrectly. \ + ${optionalString (cfg.preliminarySelfsigned) "Selfsigned certs are in place and dependant services will still start."} + # Exit 10 so that users can potentially amend SuccessExitStatus to ignore this error. + # High number to avoid Systemd reserved codes. + exit 10 + fi + + mv domainhash.txt certificates/ + + # Group might change between runs, re-apply it + chown '${user}:${data.group}' certificates/* + + # Copy all certs to the "real" certs directory + if ! cmp -s 'certificates/${keyName}.crt' out/fullchain.pem; then + touch out/renewed + echo Installing new certificate + cp -vp 'certificates/${keyName}.crt' out/fullchain.pem + cp -vp 'certificates/${keyName}.key' out/key.pem + cp -vp 'certificates/${keyName}.issuer.crt' out/chain.pem + ln -sf fullchain.pem out/cert.pem + cat out/key.pem out/fullchain.pem > out/full.pem + fi + + # By default group will have no access to the cert files. + # This chmod will fix that. + chmod 640 out/* + ''; + }; + }; + + certConfigs = mapAttrs certToConfig cfg.certs; + + # These options can be specified within + # security.acme.defaults or security.acme.certs.<name> + inheritableModule = isDefaults: { config, ... }: let + defaultAndText = name: default: { + # When ! isDefaults then this is the option declaration for the + # security.acme.certs.<name> path, which has the extra inheritDefaults + # option, which if disabled means that we can't inherit it + default = if isDefaults || ! config.inheritDefaults then default else cfg.defaults.${name}; + # The docs however don't need to depend on inheritDefaults, they should + # stay constant. Though notably it wouldn't matter much, because to get + # the option information, a submodule with name `<name>` is evaluated + # without any definitions. + defaultText = if isDefaults then default else literalExpression "config.security.acme.defaults.${name}"; + }; + in { + options = { + validMinDays = mkOption { + type = types.int; + inherit (defaultAndText "validMinDays" 30) default defaultText; + description = "Minimum remaining validity before renewal in days."; + }; + + renewInterval = mkOption { + type = types.str; + inherit (defaultAndText "renewInterval" "daily") default defaultText; + description = '' + Systemd calendar expression when to check for renewal. See + <citerefentry><refentrytitle>systemd.time</refentrytitle> + <manvolnum>7</manvolnum></citerefentry>. + ''; + }; + + enableDebugLogs = mkEnableOption "debug logging for this certificate" // { + inherit (defaultAndText "enableDebugLogs" true) default defaultText; + }; + + webroot = mkOption { + type = types.nullOr types.str; + inherit (defaultAndText "webroot" null) default defaultText; + example = "/var/lib/acme/acme-challenge"; + description = '' + Where the webroot of the HTTP vhost is located. + <filename>.well-known/acme-challenge/</filename> directory + will be created below the webroot if it doesn't exist. + <literal>http://example.org/.well-known/acme-challenge/</literal> must also + be available (notice unencrypted HTTP). + ''; + }; + + server = mkOption { + type = types.nullOr types.str; + inherit (defaultAndText "server" null) default defaultText; + description = '' + ACME Directory Resource URI. Defaults to Let's Encrypt's + production endpoint, + <link xlink:href="https://acme-v02.api.letsencrypt.org/directory"/>, if unset. + ''; + }; + + email = mkOption { + type = types.str; + inherit (defaultAndText "email" null) default defaultText; + 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 + creation limits. + ''; + }; + + group = mkOption { + type = types.str; + inherit (defaultAndText "group" "acme") default defaultText; + description = "Group running the ACME client."; + }; + + reloadServices = mkOption { + type = types.listOf types.str; + inherit (defaultAndText "reloadServices" []) default defaultText; + description = '' + The list of systemd services to call <code>systemctl try-reload-or-restart</code> + on. + ''; + }; + + postRun = mkOption { + type = types.lines; + inherit (defaultAndText "postRun" "") default defaultText; + example = "cp full.pem backup.pem"; + description = '' + Commands to run after new certificates go live. Note that + these commands run as the root user. + + Executed in the same directory with the new certificate. + ''; + }; + + keyType = mkOption { + type = types.str; + inherit (defaultAndText "keyType" "ec256") default defaultText; + description = '' + Key type to use for private keys. + For an up to date list of supported values check the --key-type option + at <link xlink:href="https://go-acme.github.io/lego/usage/cli/#usage"/>. + ''; + }; + + dnsProvider = mkOption { + type = types.nullOr types.str; + inherit (defaultAndText "dnsProvider" null) default defaultText; + example = "route53"; + description = '' + DNS Challenge provider. For a list of supported providers, see the "code" + field of the DNS providers listed at <link xlink:href="https://go-acme.github.io/lego/dns/"/>. + ''; + }; + + dnsResolver = mkOption { + type = types.nullOr types.str; + inherit (defaultAndText "dnsResolver" null) default defaultText; + example = "1.1.1.1:53"; + description = '' + Set the resolver to use for performing recursive DNS queries. Supported: + host:port. The default is to use the system resolvers, or Google's DNS + resolvers if the system's cannot be determined. + ''; + }; + + credentialsFile = mkOption { + type = types.path; + inherit (defaultAndText "credentialsFile" null) default defaultText; + description = '' + Path to an EnvironmentFile for the cert's service containing any required and + optional environment variables for your selected dnsProvider. + To find out what values you need to set, consult the documentation at + <link xlink:href="https://go-acme.github.io/lego/dns/"/> for the corresponding dnsProvider. + ''; + example = "/var/src/secrets/example.org-route53-api-token"; + }; + + dnsPropagationCheck = mkOption { + type = types.bool; + inherit (defaultAndText "dnsPropagationCheck" true) default defaultText; + 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; + inherit (defaultAndText "ocspMustStaple" false) default defaultText; + description = '' + Turns on the OCSP Must-Staple TLS extension. + Make sure you know what you're doing! See: + <itemizedlist> + <listitem><para><link xlink:href="https://blog.apnic.net/2019/01/15/is-the-web-ready-for-ocsp-must-staple/" /></para></listitem> + <listitem><para><link xlink:href="https://blog.hboeck.de/archives/886-The-Problem-with-OCSP-Stapling-and-Must-Staple-and-why-Certificate-Revocation-is-still-broken.html" /></para></listitem> + </itemizedlist> + ''; + }; + + extraLegoFlags = mkOption { + type = types.listOf types.str; + inherit (defaultAndText "extraLegoFlags" []) default defaultText; + description = '' + Additional global flags to pass to all lego commands. + ''; + }; + + extraLegoRenewFlags = mkOption { + type = types.listOf types.str; + inherit (defaultAndText "extraLegoRenewFlags" []) default defaultText; + description = '' + Additional flags to pass to lego renew. + ''; + }; + + extraLegoRunFlags = mkOption { + type = types.listOf types.str; + inherit (defaultAndText "extraLegoRunFlags" []) default defaultText; + description = '' + Additional flags to pass to lego run. + ''; + }; + }; + }; + + certOpts = { name, config, ... }: { + options = { + # user option has been removed + user = mkOption { + visible = false; + default = "_mkRemovedOptionModule"; + }; + + # allowKeysForGroup option has been removed + allowKeysForGroup = mkOption { + visible = false; + default = "_mkRemovedOptionModule"; + }; + + # extraDomains was replaced with extraDomainNames + extraDomains = mkOption { + visible = false; + default = "_mkMergedOptionModule"; + }; + + directory = mkOption { + type = types.str; + readOnly = true; + default = "/var/lib/acme/${name}"; + description = "Directory where certificate and other state is stored."; + }; + + domain = mkOption { + type = types.str; + default = name; + description = "Domain to fetch certificate for (defaults to the entry name)."; + }; + + extraDomainNames = mkOption { + type = types.listOf types.str; + default = []; + example = literalExpression '' + [ + "example.org" + "mydomain.org" + ] + ''; + description = '' + A list of extra domain names, which are included in the one certificate to be issued. + ''; + }; + + # This setting must be different for each configured certificate, otherwise + # two or more renewals may fail to bind to the address. Hence, it is not in + # the inheritableOpts. + listenHTTP = mkOption { + type = types.nullOr types.str; + default = null; + example = ":1360"; + description = '' + Interface and port to listen on to solve HTTP challenges + in the form [INTERFACE]:PORT. + If you use a port other than 80, you must proxy port 80 to this port. + ''; + }; + + inheritDefaults = mkOption { + default = true; + example = true; + description = "Whether to inherit values set in `security.acme.defaults` or not."; + type = lib.types.bool; + }; + }; + }; + +in { + + options = { + security.acme = { + preliminarySelfsigned = mkOption { + type = types.bool; + default = true; + description = '' + Whether a preliminary self-signed certificate should be generated before + doing ACME requests. This can be useful when certificates are required in + a webserver, but ACME needs the webserver to make its requests. + + With preliminary self-signed certificate the webserver can be started and + can later reload the correct ACME certificates. + ''; + }; + + acceptTerms = mkOption { + type = types.bool; + default = false; + description = '' + Accept the CA's terms of service. The default provider is Let's Encrypt, + you can find their ToS at <link xlink:href="https://letsencrypt.org/repository/"/>. + ''; + }; + + useRoot = mkOption { + type = types.bool; + default = false; + description = '' + Whether to use the root user when generating certs. This is not recommended + for security + compatiblity reasons. If a service requires root owned certificates + consider following the guide on "Using ACME with services demanding root + owned certificates" in the NixOS manual, and only using this as a fallback + or for testing. + ''; + }; + + defaults = mkOption { + type = types.submodule (inheritableModule true); + description = '' + Default values inheritable by all configured certs. You can + use this to define options shared by all your certs. These defaults + can also be ignored on a per-cert basis using the + `security.acme.certs.''${cert}.inheritDefaults' option. + ''; + }; + + certs = mkOption { + default = { }; + type = with types; attrsOf (submodule [ (inheritableModule false) certOpts ]); + description = '' + Attribute set of certificates to get signed and renewed. Creates + <literal>acme-''${cert}.{service,timer}</literal> systemd units for + each certificate defined here. Other services can add dependencies + to those units if they rely on the certificates being present, + or trigger restarts of the service if certificates get renewed. + ''; + example = literalExpression '' + { + "example.com" = { + webroot = "/var/lib/acme/acme-challenge/"; + email = "foo@example.com"; + extraDomainNames = [ "www.example.com" "foo.example.com" ]; + }; + "bar.example.com" = { + webroot = "/var/lib/acme/acme-challenge/"; + email = "bar@example.com"; + }; + } + ''; + }; + }; + }; + + imports = [ + (mkRemovedOptionModule [ "security" "acme" "production" ] '' + Use security.acme.server to define your staging ACME server URL instead. + + To use the let's encrypt staging server, use security.acme.server = + "https://acme-staging-v02.api.letsencrypt.org/directory". + '') + (mkRemovedOptionModule [ "security" "acme" "directory" ] "ACME Directory is now hardcoded to /var/lib/acme and its permisisons are managed by systemd. See https://github.com/NixOS/nixpkgs/issues/53852 for more info.") + (mkRemovedOptionModule [ "security" "acme" "preDelay" ] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal") + (mkRemovedOptionModule [ "security" "acme" "activationDelay" ] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal") + (mkChangedOptionModule [ "security" "acme" "validMin" ] [ "security" "acme" "defaults" "validMinDays" ] (config: config.security.acme.validMin / (24 * 3600))) + (mkChangedOptionModule [ "security" "acme" "validMinDays" ] [ "security" "acme" "defaults" "validMinDays" ] (config: config.security.acme.validMinDays)) + (mkChangedOptionModule [ "security" "acme" "renewInterval" ] [ "security" "acme" "defaults" "renewInterval" ] (config: config.security.acme.renewInterval)) + (mkChangedOptionModule [ "security" "acme" "email" ] [ "security" "acme" "defaults" "email" ] (config: config.security.acme.email)) + (mkChangedOptionModule [ "security" "acme" "server" ] [ "security" "acme" "defaults" "server" ] (config: config.security.acme.server)) + (mkChangedOptionModule [ "security" "acme" "enableDebugLogs" ] [ "security" "acme" "defaults" "enableDebugLogs" ] (config: config.security.acme.enableDebugLogs)) + ]; + + config = mkMerge [ + (mkIf (cfg.certs != { }) { + + # FIXME Most of these custom warnings and filters for security.acme.certs.* are required + # because using mkRemovedOptionModule/mkChangedOptionModule with attrsets isn't possible. + warnings = filter (w: w != "") (mapAttrsToList (cert: data: if data.extraDomains != "_mkMergedOptionModule" then '' + The option definition `security.acme.certs.${cert}.extraDomains` has changed + to `security.acme.certs.${cert}.extraDomainNames` and is now a list of strings. + Setting a custom webroot for extra domains is not possible, instead use separate certs. + '' else "") cfg.certs); + + assertions = let + certs = attrValues cfg.certs; + in [ + { + assertion = cfg.email != null || all (certOpts: certOpts.email != null) certs; + message = '' + You must define `security.acme.certs.<name>.email` or + `security.acme.email` to register with the CA. Note that using + many different addresses for certs may trigger account rate limits. + ''; + } + { + assertion = cfg.acceptTerms; + message = '' + You must accept the CA's terms of service before using + the ACME module by setting `security.acme.acceptTerms` + to `true`. For Let's Encrypt's ToS see https://letsencrypt.org/repository/ + ''; + } + ] ++ (builtins.concatLists (mapAttrsToList (cert: data: [ + { + assertion = data.user == "_mkRemovedOptionModule"; + message = '' + The option definition `security.acme.certs.${cert}.user' no longer has any effect; Please remove it. + Certificate user is now hard coded to the "acme" user. If you would + like another user to have access, consider adding them to the + "acme" group or changing security.acme.certs.${cert}.group. + ''; + } + { + assertion = data.allowKeysForGroup == "_mkRemovedOptionModule"; + message = '' + The option definition `security.acme.certs.${cert}.allowKeysForGroup' no longer has any effect; Please remove it. + All certs are readable by the configured group. If this is undesired, + consider changing security.acme.certs.${cert}.group to an unused group. + ''; + } + # * in the cert value breaks building of systemd services, and makes + # referencing them as a user quite weird too. Best practice is to use + # the domain option. + { + assertion = ! hasInfix "*" cert; + message = '' + The cert option path `security.acme.certs.${cert}.dnsProvider` + cannot contain a * character. + Instead, set `security.acme.certs.${cert}.domain = "${cert}";` + and remove the wildcard from the path. + ''; + } + { + assertion = data.dnsProvider == null || data.webroot == null; + message = '' + Options `security.acme.certs.${cert}.dnsProvider` and + `security.acme.certs.${cert}.webroot` are mutually exclusive. + ''; + } + { + assertion = data.webroot == null || data.listenHTTP == null; + message = '' + Options `security.acme.certs.${cert}.webroot` and + `security.acme.certs.${cert}.listenHTTP` are mutually exclusive. + ''; + } + { + assertion = data.listenHTTP == null || data.dnsProvider == null; + message = '' + Options `security.acme.certs.${cert}.listenHTTP` and + `security.acme.certs.${cert}.dnsProvider` are mutually exclusive. + ''; + } + { + assertion = data.dnsProvider != null || data.webroot != null || data.listenHTTP != null; + message = '' + One of `security.acme.certs.${cert}.dnsProvider`, + `security.acme.certs.${cert}.webroot`, or + `security.acme.certs.${cert}.listenHTTP` must be provided. + ''; + } + ]) cfg.certs)); + + users.users.acme = { + home = "/var/lib/acme"; + group = "acme"; + isSystemUser = true; + }; + + users.groups.acme = {}; + + systemd.services = { + "acme-fixperms" = userMigrationService; + } // (mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewService) certConfigs) + // (optionalAttrs (cfg.preliminarySelfsigned) ({ + "acme-selfsigned-ca" = selfsignCAService; + } // (mapAttrs' (cert: conf: nameValuePair "acme-selfsigned-${cert}" conf.selfsignService) certConfigs))); + + systemd.timers = mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewTimer) certConfigs; + + systemd.targets = let + # Create some targets which can be depended on to be "active" after cert renewals + finishedTargets = mapAttrs' (cert: conf: nameValuePair "acme-finished-${cert}" { + wantedBy = [ "default.target" ]; + requires = [ "acme-${cert}.service" ]; + after = [ "acme-${cert}.service" ]; + }) certConfigs; + + # Create targets to limit the number of simultaneous account creations + # How it works: + # - Pick a "leader" cert service, which will be in charge of creating the account, + # and run first (requires + after) + # - Make all other cert services sharing the same account wait for the leader to + # finish before starting (requiredBy + before). + # Using a target here is fine - account creation is a one time event. Even if + # systemd clean --what=state is used to delete the account, so long as the user + # then runs one of the cert services, there won't be any issues. + accountTargets = mapAttrs' (hash: confs: let + leader = "acme-${(builtins.head confs).cert}.service"; + dependantServices = map (conf: "acme-${conf.cert}.service") (builtins.tail confs); + in nameValuePair "acme-account-${hash}" { + requiredBy = dependantServices; + before = dependantServices; + requires = [ leader ]; + after = [ leader ]; + }) (groupBy (conf: conf.accountHash) (attrValues certConfigs)); + in finishedTargets // accountTargets; + }) + ]; + + meta = { + maintainers = lib.teams.acme.members; + doc = ./doc.xml; + }; +} diff --git a/nixos/modules/security/acme/doc.xml b/nixos/modules/security/acme/doc.xml new file mode 100644 index 00000000000..f623cc509be --- /dev/null +++ b/nixos/modules/security/acme/doc.xml @@ -0,0 +1,413 @@ +<chapter xmlns="http://docbook.org/ns/docbook" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:xi="http://www.w3.org/2001/XInclude" + version="5.0" + xml:id="module-security-acme"> + <title>SSL/TLS Certificates with ACME</title> + <para> + 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 + <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 + hosts is included in NixOS, however if you would like to generate a wildcard + cert or you are not using a web server you will have to configure DNS + based validation. + </para> + <section xml:id="module-security-acme-prerequisites"> + <title>Prerequisites</title> + + <para> + To use the ACME module, you must accept the provider's terms of service + by setting <literal><xref linkend="opt-security.acme.acceptTerms" /></literal> + to <literal>true</literal>. The Let's Encrypt ToS can be found + <link xlink:href="https://letsencrypt.org/repository/">here</link>. + </para> + + <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.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, + and cannot be used to administer the certificates in any way. + </para> + + <para> + Alternatively, you can use a different ACME server by changing the + <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> + + <para> + You will need an HTTP server or DNS server for verification. For HTTP, + the server must have a webroot defined that can serve + <filename>.well-known/acme-challenge</filename>. This directory must be + writeable by the user that will run the ACME client. For DNS, you must + set up credentials with your provider/server for use with lego. + </para> + </section> + <section xml:id="module-security-acme-nginx"> + <title>Using ACME certificates in Nginx</title> + + <para> + NixOS supports fetching ACME certificates for you by setting + <literal><link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> + = 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 this: + </para> + +<programlisting> +<xref linkend="opt-security.acme.acceptTerms" /> = true; +<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> = { + "foo.example.com" = { + <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true; + <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true; + # All serverAliases will be added as <link linkend="opt-security.acme.certs._name_.extraDomainNames">extra domain names</link> on the certificate. + <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [ "bar.example.com" ]; + locations."/" = { + <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/www"; + }; + }; + + # We can also add a different vhost and reuse the same certificate + # but we have to append extraDomainNames manually. + <link linkend="opt-security.acme.certs._name_.extraDomainNames">security.acme.certs."foo.example.com".extraDomainNames</link> = [ "baz.example.com" ]; + "baz.example.com" = { + <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true; + <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">useACMEHost</link> = "foo.example.com"; + locations."/" = { + <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/www"; + }; + }; + }; +} +</programlisting> + </section> + <section xml:id="module-security-acme-httpd"> + <title>Using ACME certificates in Apache/httpd</title> + + <para> + Using ACME certificates with Apache virtual hosts is identical + to using them with Nginx. The attribute names are all the same, just replace + "nginx" with "httpd" where appropriate. + </para> + </section> + <section xml:id="module-security-acme-configuring"> + <title>Manual configuration of HTTP-01 validation</title> + + <para> + First off you will need to set up a virtual host to serve the challenges. + This example uses a vhost called <literal>certs.example.com</literal>, with + the intent that you will generate certs for all your vhosts and redirect + everyone to HTTPS. + </para> + +<programlisting> +<xref linkend="opt-security.acme.acceptTerms" /> = true; +<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 +# this is to add the Nginx user to the ACME group. +<link linkend="opt-users.users._name_.extraGroups">users.users.nginx.extraGroups</link> = [ "acme" ]; + +services.nginx = { + <link linkend="opt-services.nginx.enable">enable</link> = true; + <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = { + "acmechallenge.example.com" = { + # Catchall vhost, will redirect users to HTTPS for all vhosts + <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [ "*.example.com" ]; + locations."/.well-known/acme-challenge" = { + <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/lib/acme/.challenges"; + }; + locations."/" = { + <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.return">return</link> = "301 https://$host$request_uri"; + }; + }; + }; +} +# Alternative config for Apache +<link linkend="opt-users.users._name_.extraGroups">users.users.wwwrun.extraGroups</link> = [ "acme" ]; +services.httpd = { + <link linkend="opt-services.httpd.enable">enable = true;</link> + <link linkend="opt-services.httpd.virtualHosts">virtualHosts</link> = { + "acmechallenge.example.com" = { + # Catchall vhost, will redirect users to HTTPS for all vhosts + <link linkend="opt-services.httpd.virtualHosts._name_.serverAliases">serverAliases</link> = [ "*.example.com" ]; + # /var/lib/acme/.challenges must be writable by the ACME user and readable by the Apache user. + # By default, this is the case. + <link linkend="opt-services.httpd.virtualHosts._name_.documentRoot">documentRoot</link> = "/var/lib/acme/.challenges"; + <link linkend="opt-services.httpd.virtualHosts._name_.extraConfig">extraConfig</link> = '' + RewriteEngine On + RewriteCond %{HTTPS} off + RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge [NC] + RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301] + ''; + }; + }; +} +</programlisting> + + <para> + Now you need to configure ACME to generate a certificate. + </para> + +<programlisting> +<xref linkend="opt-security.acme.certs"/>."foo.example.com" = { + <link linkend="opt-security.acme.certs._name_.webroot">webroot</link> = "/var/lib/acme/.challenges"; + <link linkend="opt-security.acme.certs._name_.email">email</link> = "foo@example.com"; + # Ensure that the web server you use can read the generated certs + # Take a look at the <link linkend="opt-services.nginx.group">group</link> option for the web server you choose. + <link linkend="opt-security.acme.certs._name_.group">group</link> = "nginx"; + # Since we have a wildcard vhost to handle port 80, + # we can generate certs for anything! + # Just make sure your DNS resolves them. + <link linkend="opt-security.acme.certs._name_.extraDomainNames">extraDomainNames</link> = [ "mail.example.com" ]; +}; +</programlisting> + + <para> + The private key <filename>key.pem</filename> and certificate + <filename>fullchain.pem</filename> will be put into + <filename>/var/lib/acme/foo.example.com</filename>. + </para> + + <para> + Refer to <xref linkend="ch-options" /> for all available configuration + options for the <link linkend="opt-security.acme.certs">security.acme</link> + module. + </para> + </section> + <section xml:id="module-security-acme-config-dns"> + <title>Configuring ACME for DNS validation</title> + + <para> + This is useful if you want to generate a wildcard certificate, since + ACME servers will only hand out wildcard certs over DNS validation. + There are a number of supported DNS providers and servers you can utilise, + see the <link xlink:href="https://go-acme.github.io/lego/dns/">lego docs</link> + for provider/server specific configuration values. For the sake of these + docs, we will provide a fully self-hosted example using bind. + </para> + +<programlisting> +services.bind = { + <link linkend="opt-services.bind.enable">enable</link> = true; + <link linkend="opt-services.bind.extraConfig">extraConfig</link> = '' + include "/var/lib/secrets/dnskeys.conf"; + ''; + <link linkend="opt-services.bind.zones">zones</link> = [ + rec { + name = "example.com"; + file = "/var/db/bind/${name}"; + master = true; + extraConfig = "allow-update { key rfc2136key.example.com.; };"; + } + ]; +} + +# Now we can configure ACME +<xref linkend="opt-security.acme.acceptTerms" /> = true; +<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"; + <link linkend="opt-security.acme.certs._name_.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.certs._name_.dnsPropagationCheck">dnsPropagationCheck</link> = false; +}; +</programlisting> + + <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 a systemd service: + </para> + +<programlisting> +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 + + 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 + ''; +}; +</programlisting> + + <para> + Now you're all set to generate certs! You should monitor the first invocation + by running <literal>systemctl start acme-example.com.service & + 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> + + <para> + Should you need to regenerate a particular certificate in a hurry, such + as when a vulnerability is found in Let's Encrypt, there is now a convenient + mechanism for doing so. Running + <literal>systemctl clean --what=state acme-example.com.service</literal> + will remove all certificate files and the account data for the given domain, + allowing you to then <literal>systemctl start acme-example.com.service</literal> + to generate fresh ones. + </para> + </section> + <section xml:id="module-security-acme-fix-jws"> + <title>Fixing JWS Verification error</title> + + <para> + It is possible that your account credentials file may become corrupt and need + to be regenerated. In this scenario lego will produce the error <literal>JWS verification error</literal>. + The solution is to simply delete the associated accounts file and + re-run the affected service(s). + </para> + +<programlisting> +# Find the accounts folder for the certificate +systemctl cat acme-example.com.service | grep -Po 'accounts/[^:]*' +export accountdir="$(!!)" +# Move this folder to some place else +mv /var/lib/acme/.lego/$accountdir{,.bak} +# Recreate the folder using systemd-tmpfiles +systemd-tmpfiles --create +# Get a new account and reissue certificates +# Note: Do this for all certs that share the same account email address +systemctl start acme-example.com.service +</programlisting> + + </section> +</chapter> diff --git a/nixos/modules/security/acme/mk-cert-ownership-assertion.nix b/nixos/modules/security/acme/mk-cert-ownership-assertion.nix new file mode 100644 index 00000000000..b80d89aeb9f --- /dev/null +++ b/nixos/modules/security/acme/mk-cert-ownership-assertion.nix @@ -0,0 +1,4 @@ +{ cert, group, groups, user }: { + assertion = cert.group == group || builtins.any (u: u == user) groups.${cert.group}.members; + message = "Group for certificate ${cert.domain} must be ${group}, or user ${user} must be a member of group ${cert.group}"; +} |