···612612 </listitem>
613613 <listitem>
614614 <para>
615615+ In the ACME module, the data used to build the hash for the account
616616+ directory has changed to accomodate new features to reduce account
617617+ rate limit issues. This will trigger new account creation on the first
618618+ rebuild following this update. No issues are expected to arise from this,
619619+ thanks to the new account creation handling.
620620+ </para>
621621+ </listitem>
622622+ <listitem>
623623+ <para>
615624 <xref linkend="opt-users.users._name_.createHome" /> now always ensures home directory permissions to be <literal>0700</literal>.
616625 Permissions had previously been ignored for already existing home directories, possibly leaving them readable by others.
617626 The option's description was incorrect regarding ownership management and has been simplified greatly.
+84-43
nixos/modules/security/acme.nix
···77 numCerts = length (builtins.attrNames cfg.certs);
88 _24hSecs = 60 * 60 * 24;
991010+ # Used to make unique paths for each cert/account config set
1111+ mkHash = with builtins; val: substring 0 20 (hashString "sha256" val);
1212+ mkAccountHash = acmeServer: data: mkHash "${toString acmeServer} ${data.keyType} ${data.email}";
1313+ accountDirRoot = "/var/lib/acme/.lego/accounts/";
1414+1015 # There are many services required to make cert renewals work.
1116 # They all follow a common structure:
1217 # - They inherit this commonServiceConfig
···1924 Type = "oneshot";
2025 User = "acme";
2126 Group = mkDefault "acme";
2222- UMask = 0027;
2727+ UMask = 0023;
2328 StateDirectoryMode = 750;
2429 ProtectSystem = "full";
2530 PrivateTmp = true;
···5459 '';
5560 };
56615757- # Previously, all certs were owned by whatever user was configured in
5858- # config.security.acme.certs.<cert>.user. Now everything is owned by and
5959- # run by the acme user.
6060- userMigrationService = {
6161- description = "Fix owner and group of all ACME certificates";
6262-6363- script = with builtins; concatStringsSep "\n" (mapAttrsToList (cert: data: ''
6464- for fixpath in /var/lib/acme/${escapeShellArg cert} /var/lib/acme/.lego/${escapeShellArg cert}; do
6262+ # Ensures that directories which are shared across all certs
6363+ # exist and have the correct user and group, since group
6464+ # is configurable on a per-cert basis.
6565+ userMigrationService = let
6666+ script = with builtins; ''
6767+ chown -R acme .lego/accounts
6868+ '' + (concatStringsSep "\n" (mapAttrsToList (cert: data: ''
6969+ for fixpath in ${escapeShellArg cert} .lego/${escapeShellArg cert}; do
6570 if [ -d "$fixpath" ]; then
6671 chmod -R u=rwX,g=rX,o= "$fixpath"
6772 chown -R acme:${data.group} "$fixpath"
6873 fi
6974 done
7070- '') certConfigs);
7575+ '') certConfigs));
7676+ in {
7777+ description = "Fix owner and group of all ACME certificates";
7878+7979+ serviceConfig = commonServiceConfig // {
8080+ # We don't want this to run every time a renewal happens
8181+ RemainAfterExit = true;
8282+8383+ # These StateDirectory entries negate the need for tmpfiles
8484+ StateDirectory = [ "acme" "acme/.lego" "acme/.lego/accounts" ];
8585+ StateDirectoryMode = 755;
8686+ WorkingDirectory = "/var/lib/acme";
71877272- # We don't want this to run every time a renewal happens
7373- serviceConfig.RemainAfterExit = true;
8888+ # Run the start script as root
8989+ ExecStart = "+" + (pkgs.writeShellScript "acme-fixperms" script);
9090+ };
7491 };
75927693 certToConfig = cert: data: let
···101118 ${toString acmeServer} ${toString data.dnsProvider}
102119 ${toString data.ocspMustStaple} ${data.keyType}
103120 '';
104104- mkHash = with builtins; val: substring 0 20 (hashString "sha256" val);
105121 certDir = mkHash hashData;
106122 domainHash = mkHash "${concatStringsSep " " extraDomains} ${data.domain}";
107107- othersHash = mkHash "${toString acmeServer} ${data.keyType} ${data.email}";
108108- accountDir = "/var/lib/acme/.lego/accounts/" + othersHash;
123123+ accountHash = (mkAccountHash acmeServer data);
124124+ accountDir = accountDirRoot + accountHash;
109125110126 protocolOpts = if useDns then (
111127 [ "--dns" data.dnsProvider ]
···142158 );
143159144160 in {
145145- inherit accountDir selfsignedDeps;
161161+ inherit accountHash cert selfsignedDeps;
146162147147- webroot = data.webroot;
148163 group = data.group;
149164150165 renewTimer = {
···184199185200 StateDirectory = "acme/${cert}";
186201187187- BindPaths = "/var/lib/acme/.minica:/tmp/ca /var/lib/acme/${cert}:/tmp/${keyName}";
202202+ BindPaths = [
203203+ "/var/lib/acme/.minica:/tmp/ca"
204204+ "/var/lib/acme/${cert}:/tmp/${keyName}"
205205+ ];
188206 };
189207190208 # Working directory will be /tmp
···222240 serviceConfig = commonServiceConfig // {
223241 Group = data.group;
224242225225- # AccountDir dir will be created by tmpfiles to ensure correct permissions
226226- # And to avoid deletion during systemctl clean
227227- # acme/.lego/${cert} is listed so that it is deleted during systemctl clean
228228- StateDirectory = "acme/${cert} acme/.lego/${cert} acme/.lego/${cert}/${certDir}";
243243+ # Keep in mind that these directories will be deleted if the user runs
244244+ # systemctl clean --what=state
245245+ # acme/.lego/${cert} is listed for this reason.
246246+ StateDirectory = [
247247+ "acme/${cert}"
248248+ "acme/.lego/${cert}"
249249+ "acme/.lego/${cert}/${certDir}"
250250+ "acme/.lego/accounts/${accountHash}"
251251+ ];
229252230253 # Needs to be space separated, but can't use a multiline string because that'll include newlines
231231- BindPaths =
232232- "${accountDir}:/tmp/accounts " +
233233- "/var/lib/acme/${cert}:/tmp/out " +
234234- "/var/lib/acme/.lego/${cert}/${certDir}:/tmp/certificates ";
254254+ BindPaths = [
255255+ "${accountDir}:/tmp/accounts"
256256+ "/var/lib/acme/${cert}:/tmp/out"
257257+ "/var/lib/acme/.lego/${cert}/${certDir}:/tmp/certificates"
258258+ ];
235259236260 # Only try loading the credentialsFile if the dns challenge is enabled
237261 EnvironmentFile = mkIf useDns data.credentialsFile;
···248272249273 # Working directory will be /tmp
250274 script = ''
251251- set -euo pipefail
275275+ set -euxo pipefail
276276+277277+ ${optionalString (data.webroot != null) ''
278278+ # Ensure the webroot exists
279279+ mkdir -p '${data.webroot}/.well-known/acme-challenge'
280280+ chown 'acme:${data.group}' ${data.webroot}/{.well-known,.well-known/acme-challenge}
281281+ ''}
252282253283 echo '${domainHash}' > domainhash.txt
254284255285 # Check if we can renew
256256- # Certificates and account credentials must exist
257257- if [ -e 'certificates/${keyName}.key' -a -e 'certificates/${keyName}.crt' -a "$(ls -1 accounts)" ]; then
286286+ if [ -e 'certificates/${keyName}.key' -a -e 'certificates/${keyName}.crt' -a -n "$(ls -1 accounts)" ]; then
258287259288 # When domains are updated, there's no need to do a full
260289 # Lego run, but it's likely renew won't work if days is too low.
···664693665694 systemd.timers = mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewTimer) certConfigs;
666695667667- # .lego and .lego/accounts specified to fix any incorrect permissions
668668- systemd.tmpfiles.rules = [
669669- "d /var/lib/acme/.lego - acme acme"
670670- "d /var/lib/acme/.lego/accounts - acme acme"
671671- ] ++ (unique (concatMap (conf: [
672672- "d ${conf.accountDir} - acme acme"
673673- ] ++ (optional (conf.webroot != null) "d ${conf.webroot}/.well-known/acme-challenge - acme ${conf.group}")
674674- ) (attrValues certConfigs)));
696696+ systemd.targets = let
697697+ # Create some targets which can be depended on to be "active" after cert renewals
698698+ finishedTargets = mapAttrs' (cert: conf: nameValuePair "acme-finished-${cert}" {
699699+ wantedBy = [ "default.target" ];
700700+ requires = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
701701+ after = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
702702+ }) certConfigs;
675703676676- # Create some targets which can be depended on to be "active" after cert renewals
677677- systemd.targets = mapAttrs' (cert: conf: nameValuePair "acme-finished-${cert}" {
678678- wantedBy = [ "default.target" ];
679679- requires = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
680680- after = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
681681- }) certConfigs;
704704+ # Create targets to limit the number of simultaneous account creations
705705+ # How it works:
706706+ # - Pick a "leader" cert service, which will be in charge of creating the account,
707707+ # and run first (requires + after)
708708+ # - Make all other cert services sharing the same account wait for the leader to
709709+ # finish before starting (requiredBy + before).
710710+ # Using a target here is fine - account creation is a one time event. Even if
711711+ # systemd clean --what=state is used to delete the account, so long as the user
712712+ # then runs one of the cert services, there won't be any issues.
713713+ accountTargets = mapAttrs' (hash: confs: let
714714+ leader = "acme-${(builtins.head confs).cert}.service";
715715+ dependantServices = map (conf: "acme-${conf.cert}.service") (builtins.tail confs);
716716+ in nameValuePair "acme-account-${hash}" {
717717+ requiredBy = dependantServices;
718718+ before = dependantServices;
719719+ requires = [ leader ];
720720+ after = [ leader ];
721721+ }) (groupBy (conf: conf.accountHash) (attrValues certConfigs));
722722+ in finishedTargets // accountTargets;
682723 })
683724 ];
684725
+8-4
nixos/modules/security/acme.xml
···162162<xref linkend="opt-security.acme.certs"/>."foo.example.com" = {
163163 <link linkend="opt-security.acme.certs._name_.webroot">webroot</link> = "/var/lib/acme/.challenges";
164164 <link linkend="opt-security.acme.certs._name_.email">email</link> = "foo@example.com";
165165+ # Ensure that the web server you use can read the generated certs
166166+ # Take a look at the <link linkend="opt-services.nginx.group">group</link> option for the web server you choose.
167167+ <link linkend="opt-security.acme.certs._name_.group">group</link> = "nginx";
165168 # Since we have a wildcard vhost to handle port 80,
166169 # we can generate certs for anything!
167170 # Just make sure your DNS resolves them.
···257260 <para>
258261 Should you need to regenerate a particular certificate in a hurry, such
259262 as when a vulnerability is found in Let's Encrypt, there is now a convenient
260260- mechanism for doing so. Running <literal>systemctl clean acme-example.com.service</literal>
261261- will remove all certificate files for the given domain, allowing you to then
262262- <literal>systemctl start acme-example.com.service</literal> to generate fresh
263263- ones.
263263+ mechanism for doing so. Running
264264+ <literal>systemctl clean --what=state acme-example.com.service</literal>
265265+ will remove all certificate files and the account data for the given domain,
266266+ allowing you to then <literal>systemctl start acme-example.com.service</literal>
267267+ to generate fresh ones.
264268 </para>
265269 </section>
266270 <section xml:id="module-security-acme-fix-jws">
+31-1
nixos/tests/acme.nix
···7777 after = [ "acme-a.example.test.service" "nginx-config-reload.service" ];
7878 };
79798080+ # Test that account creation is collated into one service
8181+ specialisation.account-creation.configuration = { nodes, pkgs, lib, ... }: let
8282+ email = "newhostmaster@example.test";
8383+ caDomain = nodes.acme.config.test-support.acme.caDomain;
8484+ # Exit 99 to make it easier to track if this is the reason a renew failed
8585+ testScript = ''
8686+ test -e accounts/${caDomain}/${email}/account.json || exit 99
8787+ '';
8888+ in {
8989+ security.acme.email = lib.mkForce email;
9090+ systemd.services."b.example.test".preStart = testScript;
9191+ systemd.services."c.example.test".preStart = testScript;
9292+9393+ services.nginx.virtualHosts."b.example.test" = (vhostBase pkgs) // {
9494+ enableACME = true;
9595+ };
9696+ services.nginx.virtualHosts."c.example.test" = (vhostBase pkgs) // {
9797+ enableACME = true;
9898+ };
9999+ };
100100+80101 # Cert config changes will not cause the nginx configuration to change.
81102 # This tests that the reload service is correctly triggered.
82103 # It also tests that postRun is exec'd as root
···289310 acme.start()
290311 webserver.start()
291312292292- acme.wait_for_unit("default.target")
313313+ acme.wait_for_unit("network-online.target")
293314 acme.wait_for_unit("pebble.service")
294315295316 client.succeed("curl https://${caDomain}:15000/roots/0 > /tmp/ca.crt")
···313334 webserver.succeed("systemctl start test-renew-nginx.target")
314335 check_issuer(webserver, "a.example.test", "pebble")
315336 check_connection(client, "a.example.test")
337337+338338+ with subtest("Runs 1 cert for account creation before others"):
339339+ switch_to(webserver, "account-creation")
340340+ webserver.wait_for_unit("acme-finished-a.example.test.target")
341341+ check_connection(client, "a.example.test")
342342+ webserver.wait_for_unit("acme-finished-b.example.test.target")
343343+ webserver.wait_for_unit("acme-finished-c.example.test.target")
344344+ check_connection(client, "b.example.test")
345345+ check_connection(client, "c.example.test")
316346317347 with subtest("Can reload web server when cert configuration changes"):
318348 switch_to(webserver, "cert-change")