aboutsummaryrefslogtreecommitdiff
path: root/modules/private/certificates.nix
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2020-05-23 03:35:03 +0200
committerIsmaël Bouya <ismael.bouya@normalesup.org>2020-05-23 03:35:03 +0200
commitcfda3cfc35445979225850f686f338e6d4ace372 (patch)
treea0f6bde8e7d04962085896e73a6467645b61c952 /modules/private/certificates.nix
parentdb343436f0e678ef3a97e6f8ac559ffa0507e422 (diff)
downloadNix-cfda3cfc35445979225850f686f338e6d4ace372.tar.gz
Nix-cfda3cfc35445979225850f686f338e6d4ace372.tar.zst
Nix-cfda3cfc35445979225850f686f338e6d4ace372.zip
Fix acme certificate generation
Diffstat (limited to 'modules/private/certificates.nix')
-rw-r--r--modules/private/certificates.nix151
1 files changed, 110 insertions, 41 deletions
diff --git a/modules/private/certificates.nix b/modules/private/certificates.nix
index bbe4c3b..c568783 100644
--- a/modules/private/certificates.nix
+++ b/modules/private/certificates.nix
@@ -2,9 +2,13 @@
2{ 2{
3 options.myServices.certificates = { 3 options.myServices.certificates = {
4 enable = lib.mkEnableOption "enable certificates"; 4 enable = lib.mkEnableOption "enable certificates";
5 webroot = lib.mkOption {
6 readOnly = true;
7 default = "/var/lib/acme/acme-challenges";
8 };
5 certConfig = lib.mkOption { 9 certConfig = lib.mkOption {
6 default = { 10 default = {
7 webroot = "/var/lib/acme/acme-challenges"; 11 webroot = lib.mkForce null; # avoids creation of tmpfiles
8 email = "ismael@bouya.org"; 12 email = "ismael@bouya.org";
9 postRun = builtins.concatStringsSep "\n" [ 13 postRun = builtins.concatStringsSep "\n" [
10 (lib.optionalString config.services.httpd.Prod.enable "systemctl reload httpdProd.service") 14 (lib.optionalString config.services.httpd.Prod.enable "systemctl reload httpdProd.service")
@@ -13,6 +17,7 @@
13 (lib.optionalString config.services.nginx.enable "systemctl reload nginx.service") 17 (lib.optionalString config.services.nginx.enable "systemctl reload nginx.service")
14 ]; 18 ];
15 extraLegoRenewFlags = [ "--reuse-key" ]; 19 extraLegoRenewFlags = [ "--reuse-key" ];
20 keyType = lib.mkDefault "ec256"; # https://github.com/NixOS/nixpkgs/pull/83121
16 }; 21 };
17 description = "Default configuration for certificates"; 22 description = "Default configuration for certificates";
18 }; 23 };
@@ -20,13 +25,13 @@
20 25
21 config = lib.mkIf config.myServices.certificates.enable { 26 config = lib.mkIf config.myServices.certificates.enable {
22 services.duplyBackup.profiles.system.excludeFile = '' 27 services.duplyBackup.profiles.system.excludeFile = ''
23 + /var/lib/acme/acme-challenges 28 + ${config.myServices.certificates.webroot}
24 ''; 29 '';
25 services.nginx = { 30 services.nginx = {
26 recommendedTlsSettings = true; 31 recommendedTlsSettings = true;
27 virtualHosts = { 32 virtualHosts = {
28 "${config.hostEnv.fqdn}" = { 33 "${config.hostEnv.fqdn}" = {
29 acmeRoot = config.security.acme.certs."${name}".webroot; 34 acmeRoot = config.myServices.certificates.webroot;
30 useACMEHost = name; 35 useACMEHost = name;
31 forceSSL = true; 36 forceSSL = true;
32 }; 37 };
@@ -45,6 +50,15 @@
45 }; 50 };
46 }; 51 };
47 52
53 users.users.acme = {
54 uid = config.ids.uids.acme;
55 group = "acme";
56 description = "Acme user";
57 };
58 users.groups.acme = {
59 gid = config.ids.gids.acme;
60 };
61
48 systemd.services = lib.attrsets.mapAttrs' (k: v: 62 systemd.services = lib.attrsets.mapAttrs' (k: v:
49 lib.attrsets.nameValuePair "acme-selfsigned-${k}" { 63 lib.attrsets.nameValuePair "acme-selfsigned-${k}" {
50 wantedBy = [ "acme-selfsigned-certificates.target" ]; 64 wantedBy = [ "acme-selfsigned-certificates.target" ];
@@ -62,50 +76,105 @@
62 lib.attrsets.mapAttrs' (k: data: 76 lib.attrsets.mapAttrs' (k: data:
63 lib.attrsets.nameValuePair "acme-${k}" { 77 lib.attrsets.nameValuePair "acme-${k}" {
64 after = lib.mkAfter [ "bind.service" ]; 78 after = lib.mkAfter [ "bind.service" ];
65 serviceConfig.ExecStartPre = 79 serviceConfig =
66 let
67 script = pkgs.writeScript "acme-pre-start" ''
68 #!${pkgs.runtimeShell} -e
69 mkdir -p '${data.webroot}/.well-known/acme-challenge'
70 chmod a+w '${data.webroot}/.well-known/acme-challenge'
71 #doesn't work for multiple concurrent runs
72 #chown -R '${data.user}:${data.group}' '${data.webroot}/.well-known/acme-challenge'
73 '';
74 in
75 "+${script}";
76 # This is a workaround to
77 # https://github.com/NixOS/nixpkgs/issues/84409
78 # https://github.com/NixOS/nixpkgs/issues/84633
79 serviceConfig.RemainAfterExit = lib.mkForce false;
80 serviceConfig.WorkingDirectory = lib.mkForce "/var/lib/acme/${k}/.lego";
81 serviceConfig.StateDirectory = lib.mkForce "acme/${k}/.lego acme/${k} acme/.lego/${k} acme/.lego/accounts";
82 serviceConfig.ExecStartPost =
83 let 80 let
84 keyName = builtins.replaceStrings ["*"] ["_"] data.domain; 81 cfg = config.security.acme;
82 hashOptions = let
83 domains = builtins.concatStringsSep "," (
84 [ data.domain ] ++ (builtins.attrNames data.extraDomains)
85 );
86 certOptions = builtins.concatStringsSep "," [
87 (if data.ocspMustStaple then "must-staple" else "no-must-staple")
88 ];
89 in
90 builtins.hashString "sha256" (builtins.concatStringsSep ";" [ data.keyType domains certOptions ]);
91 accountsDir = "accounts-${data.keyType}";
92 lpath = "acme/${k}";
93 apath = "/var/lib/${lpath}";
94 spath = "/var/lib/acme/.lego/${k}";
85 fileMode = if data.allowKeysForGroup then "640" else "600"; 95 fileMode = if data.allowKeysForGroup then "640" else "600";
86 spath = "/var/lib/acme/${k}/.lego"; 96 dirFileMode = if data.allowKeysForGroup then "750" else "700";
87 script = pkgs.writeScript "acme-post-start" '' 97 globalOpts = [ "-d" data.domain "--email" data.email "--path" "." "--key-type" data.keyType ]
98 ++ lib.optionals (cfg.acceptTerms) [ "--accept-tos" ]
99 ++ lib.optionals (data.dnsProvider != null && !data.dnsPropagationCheck) [ "--dns.disable-cp" ]
100 ++ lib.concatLists (lib.mapAttrsToList (name: root: [ "-d" name ]) data.extraDomains)
101 ++ (if data.dnsProvider != null then [ "--dns" data.dnsProvider ] else [ "--http" "--http.webroot" config.myServices.certificates.webroot ])
102 ++ lib.optionals (cfg.server != null || data.server != null) ["--server" (if data.server == null then cfg.server else data.server)];
103 certOpts = lib.optionals data.ocspMustStaple [ "--must-staple" ];
104 runOpts = lib.escapeShellArgs (globalOpts ++ [ "run" ] ++ certOpts);
105 renewOpts = lib.escapeShellArgs (globalOpts ++
106 [ "renew" "--days" (builtins.toString cfg.validMinDays) ] ++
107 certOpts ++ data.extraLegoRenewFlags);
108 forceRenewOpts = lib.escapeShellArgs (globalOpts ++
109 [ "renew" "--days" "999" ] ++
110 certOpts ++ data.extraLegoRenewFlags);
111 keyName = builtins.replaceStrings ["*"] ["_"] data.domain;
112 in {
113 User = lib.mkForce "acme";
114 Group = lib.mkForce "acme";
115 WorkingDirectory = lib.mkForce spath;
116 StateDirectory = lib.mkForce "acme/.lego/${k} acme/.lego/${accountsDir}";
117 ExecStartPre =
118 let
119 script = pkgs.writeScript "acme-prestart" ''
120 #!${pkgs.runtimeShell} -e
121 install -m 0755 -o acme -g acme -d ${config.myServices.certificates.webroot}
122 '';
123 in
124 lib.mkForce "+${script}";
125 ExecStart = lib.mkForce (pkgs.writeScript "acme-start" ''
88 #!${pkgs.runtimeShell} -e 126 #!${pkgs.runtimeShell} -e
89 cd /var/lib/acme/${k} 127 # lego doesn't check key type after initial creation, we
90 128 # need to check for him
91 # Test that existing cert is older than new cert 129 if [ -L ${spath}/accounts -o -d ${spath}/accounts ]; then
92 KEY=${spath}/certificates/${keyName}.key 130 if [ -L ${spath}/accounts -a "$(readlink ${spath}/accounts)" != ../${accountsDir} ]; then
93 if [ -e $KEY -a $KEY -nt key.pem ]; then 131 ln -sfn ../${accountsDir} ${spath}/accounts
94 cp -p ${spath}/certificates/${keyName}.key key.pem 132 mv -f ${spath}/certificates/${keyName}.key ${spath}/certificates/${keyName}.key.old
95 cp -p ${spath}/certificates/${keyName}.crt fullchain.pem 133 fi
96 cp -p ${spath}/certificates/${keyName}.issuer.crt chain.pem 134 else
97 ln -sf fullchain.pem cert.pem 135 ln -s ../${accountsDir} ${spath}/accounts
98 cat key.pem fullchain.pem > full.pem 136 fi
99 137 # check if domain changed: lego doesn't check by itself
100 ${data.postRun} 138 if [ ! -e ${spath}/certificates/${keyName}.crt -o ! -e ${spath}/certificates/${keyName}.key -o ! -e "${spath}/accounts/acme-v02.api.letsencrypt.org/${data.email}/account.json" ]; then
139 ${pkgs.lego}/bin/lego ${runOpts}
140 elif [ ! -f ${spath}/currentDomains -o "$(cat ${spath}/currentDomains)" != "${hashOptions}" ]; then
141 ${pkgs.lego}/bin/lego ${forceRenewOpts}
142 else
143 ${pkgs.lego}/bin/lego ${renewOpts}
101 fi 144 fi
145 '');
146 ExecStartPost =
147 let
148 script = pkgs.writeScript "acme-post-start" ''
149 #!${pkgs.runtimeShell} -e
150 install -m 0755 -o root -g root -d /var/lib/acme
151 install -m 0${dirFileMode} -o ${data.user} -g ${data.group} -d /var/lib/acme/${k}
152 cd /var/lib/acme/${k}
153
154 # Test that existing cert is older than new cert
155 KEY=${spath}/certificates/${keyName}.key
156 KEY_CHANGED=no
157 if [ -e $KEY -a $KEY -nt key.pem ]; then
158 KEY_CHANGED=yes
159 cp -p ${spath}/certificates/${keyName}.key key.pem
160 cp -p ${spath}/certificates/${keyName}.crt fullchain.pem
161 cp -p ${spath}/certificates/${keyName}.issuer.crt chain.pem
162 ln -sf fullchain.pem cert.pem
163 cat key.pem fullchain.pem > full.pem
164 echo -n "${hashOptions}" > ${spath}/currentDomains
165 fi
102 166
103 chmod ${fileMode} *.pem 167 chmod ${fileMode} *.pem
104 chown '${data.user}:${data.group}' *.pem 168 chown '${data.user}:${data.group}' *.pem
105 '';
106 in
107 lib.mkForce "+${script}";
108 169
170 if [ "$KEY_CHANGED" = "yes" ]; then
171 : # noop in case postRun is empty
172 ${data.postRun}
173 fi
174 '';
175 in
176 lib.mkForce "+${script}";
177 };
109 } 178 }
110 ) config.security.acme.certs // 179 ) config.security.acme.certs //
111 { 180 {