acme: added option `security.acme.preliminarySelfsigned` (#15562)

authored by bobvanderlinden.tngl.sh and committed by

Domen Kožar 4e6697dc 164ead31

+162 -46
+134 -46
nixos/modules/security/acme.nix
··· 114 ''; 115 }; 116 117 certs = mkOption { 118 default = { }; 119 type = types.loaOf types.optionSet; ··· 140 config = mkMerge [ 141 (mkIf (cfg.certs != { }) { 142 143 - systemd.services = flip mapAttrs' cfg.certs (cert: data: 144 - let 145 - cpath = "${cfg.directory}/${cert}"; 146 - rights = if data.allowKeysForGroup then "750" else "700"; 147 - cmdline = [ "-v" "-d" cert "--default_root" data.webroot "--valid_min" cfg.validMin ] 148 - ++ optionals (data.email != null) [ "--email" data.email ] 149 - ++ concatMap (p: [ "-f" p ]) data.plugins 150 - ++ concatLists (mapAttrsToList (name: root: [ "-d" (if root == null then name else "${name}:${root}")]) data.extraDomains); 151 152 - in nameValuePair 153 - ("acme-${cert}") 154 - ({ 155 - description = "Renew ACME Certificate for ${cert}"; 156 - after = [ "network.target" ]; 157 - serviceConfig = { 158 - Type = "oneshot"; 159 - SuccessExitStatus = [ "0" "1" ]; 160 - PermissionsStartOnly = true; 161 - User = data.user; 162 - Group = data.group; 163 - PrivateTmp = true; 164 }; 165 - path = [ pkgs.simp_le ]; 166 - preStart = '' 167 - mkdir -p '${cfg.directory}' 168 - if [ ! -d '${cpath}' ]; then 169 - mkdir '${cpath}' 170 - fi 171 - chmod ${rights} '${cpath}' 172 - chown -R '${data.user}:${data.group}' '${cpath}' 173 - ''; 174 - script = '' 175 - cd '${cpath}' 176 - set +e 177 - simp_le ${concatMapStringsSep " " (arg: escapeShellArg (toString arg)) cmdline} 178 - EXITCODE=$? 179 - set -e 180 - echo "$EXITCODE" > /tmp/lastExitCode 181 - exit "$EXITCODE" 182 - ''; 183 - postStop = '' 184 - if [ -e /tmp/lastExitCode ] && [ "$(cat /tmp/lastExitCode)" = "0" ]; then 185 - echo "Executing postRun hook..." 186 - ${data.postRun} 187 - fi 188 - ''; 189 - }) 190 - ); 191 192 systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair 193 ("acme-${cert}") ··· 200 }; 201 }) 202 ); 203 }) 204 205 { meta.maintainers = with lib.maintainers; [ abbradar fpletz globin ];
··· 114 ''; 115 }; 116 117 + preliminarySelfsigned = mkOption { 118 + type = types.bool; 119 + default = true; 120 + description = '' 121 + Whether a preliminary self-signed certificate should be generated before 122 + doing ACME requests. This can be useful when certificates are required in 123 + a webserver, but ACME needs the webserver to make its requests. 124 + 125 + With preliminary self-signed certificate the webserver can be started and 126 + can later reload the correct ACME certificates. 127 + ''; 128 + }; 129 + 130 certs = mkOption { 131 default = { }; 132 type = types.loaOf types.optionSet; ··· 153 config = mkMerge [ 154 (mkIf (cfg.certs != { }) { 155 156 + systemd.services = let 157 + services = concatLists servicesLists; 158 + servicesLists = mapAttrsToList certToServices cfg.certs; 159 + certToServices = cert: data: 160 + let 161 + cpath = "${cfg.directory}/${cert}"; 162 + rights = if data.allowKeysForGroup then "750" else "700"; 163 + cmdline = [ "-v" "-d" cert "--default_root" data.webroot "--valid_min" cfg.validMin ] 164 + ++ optionals (data.email != null) [ "--email" data.email ] 165 + ++ concatMap (p: [ "-f" p ]) data.plugins 166 + ++ concatLists (mapAttrsToList (name: root: [ "-d" (if root == null then name else "${name}:${root}")]) data.extraDomains); 167 + acmeService = { 168 + description = "Renew ACME Certificate for ${cert}"; 169 + after = [ "network.target" ]; 170 + serviceConfig = { 171 + Type = "oneshot"; 172 + SuccessExitStatus = [ "0" "1" ]; 173 + PermissionsStartOnly = true; 174 + User = data.user; 175 + Group = data.group; 176 + PrivateTmp = true; 177 + }; 178 + path = [ pkgs.simp_le ]; 179 + preStart = '' 180 + mkdir -p '${cfg.directory}' 181 + if [ ! -d '${cpath}' ]; then 182 + mkdir '${cpath}' 183 + fi 184 + chmod ${rights} '${cpath}' 185 + chown -R '${data.user}:${data.group}' '${cpath}' 186 + ''; 187 + script = '' 188 + cd '${cpath}' 189 + set +e 190 + simp_le ${concatMapStringsSep " " (arg: escapeShellArg (toString arg)) cmdline} 191 + EXITCODE=$? 192 + set -e 193 + echo "$EXITCODE" > /tmp/lastExitCode 194 + exit "$EXITCODE" 195 + ''; 196 + postStop = '' 197 + if [ -e /tmp/lastExitCode ] && [ "$(cat /tmp/lastExitCode)" = "0" ]; then 198 + echo "Executing postRun hook..." 199 + ${data.postRun} 200 + fi 201 + ''; 202 203 + before = [ "acme-certificates.target" ]; 204 + wantedBy = [ "acme-certificates.target" ]; 205 + }; 206 + selfsignedService = { 207 + description = "Create preliminary self-signed certificate for ${cert}"; 208 + preStart = '' 209 + if [ ! -d '${cpath}' ] 210 + then 211 + mkdir -p '${cpath}' 212 + chmod ${rights} '${cpath}' 213 + chown '${data.user}:${data.group}' '${cpath}' 214 + fi 215 + ''; 216 + script = 217 + '' 218 + # Create self-signed key 219 + workdir="/run/acme-selfsigned-${cert}" 220 + ${pkgs.openssl.bin}/bin/openssl genrsa -des3 -passout pass:x -out $workdir/server.pass.key 2048 221 + ${pkgs.openssl.bin}/bin/openssl rsa -passin pass:x -in $workdir/server.pass.key -out $workdir/server.key 222 + ${pkgs.openssl.bin}/bin/openssl req -new -key $workdir/server.key -out $workdir/server.csr \ 223 + -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com" 224 + ${pkgs.openssl.bin}/bin/openssl x509 -req -days 1 -in $workdir/server.csr -signkey $workdir/server.key -out $workdir/server.crt 225 + 226 + # Move key to destination 227 + mv $workdir/server.key ${cpath}/key.pem 228 + mv $workdir/server.crt ${cpath}/fullchain.pem 229 + 230 + # Clean up working directory 231 + rm $workdir/server.csr 232 + rm $workdir/server.pass.key 233 + 234 + # Give key acme permissions 235 + chmod ${rights} '${cpath}/key.pem' 236 + chown '${data.user}:${data.group}' '${cpath}/key.pem' 237 + chmod ${rights} '${cpath}/fullchain.pem' 238 + chown '${data.user}:${data.group}' '${cpath}/fullchain.pem' 239 + ''; 240 + serviceConfig = { 241 + Type = "oneshot"; 242 + RuntimeDirectory = "acme-selfsigned-${cert}"; 243 + PermissionsStartOnly = true; 244 + User = data.user; 245 + Group = data.group; 246 + }; 247 + unitConfig = { 248 + # Do not create self-signed key when key already exists 249 + ConditionPathExists = "!${cpath}/key.pem"; 250 + }; 251 + before = [ 252 + "acme-selfsigned-certificates.target" 253 + ]; 254 + wantedBy = [ 255 + "acme-selfsigned-certificates.target" 256 + ]; 257 + }; 258 + in ( 259 + [ { name = "acme-${cert}"; value = acmeService; } ] 260 + ++ 261 + (if cfg.preliminarySelfsigned 262 + then [ { name = "acme-selfsigned-${cert}"; value = selfsignedService; } ] 263 + else [] 264 + ) 265 + ); 266 + servicesAttr = listToAttrs services; 267 + nginxAttr = { 268 + nginx = { 269 + after = [ "acme-selfsigned-certificates.target" ]; 270 + wants = [ "acme-selfsigned-certificates.target" "acme-certificates.target" ]; 271 + }; 272 }; 273 + in 274 + servicesAttr // 275 + (if config.services.nginx.enable then nginxAttr else {}); 276 277 systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair 278 ("acme-${cert}") ··· 285 }; 286 }) 287 ); 288 + 289 + systemd.targets."acme-selfsigned-certificates" = mkIf cfg.preliminarySelfsigned {}; 290 + systemd.targets."acme-certificates" = {}; 291 }) 292 293 { meta.maintainers = with lib.maintainers; [ abbradar fpletz globin ];
+28
nixos/modules/security/acme.xml
··· 66 67 </section> 68 69 </chapter>
··· 66 67 </section> 68 69 + <section><title>Using ACME certificates in Nginx</title> 70 + <para>In practice ACME is mostly used for retrieval and renewal of 71 + certificates that will be used in a webserver like Nginx. A configuration for 72 + Nginx that uses the certificates from ACME for 73 + <literal>foo.example.com</literal> will look similar to: 74 + </para> 75 + 76 + <programlisting> 77 + services.nginx.httpConfig = '' 78 + server { 79 + server_name foo.example.com; 80 + listen 443 ssl; 81 + ssl_certificate ${config.security.acme.directory}/foo.example.com/fullchain.pem; 82 + ssl_certificate_key ${config.security.acme.directory}/foo.example.com/key.pem; 83 + root /var/www/foo.example.com/; 84 + } 85 + ''; 86 + </programlisting> 87 + 88 + <para>Now Nginx will try to use the certificates that will be retrieved by ACME. 89 + ACME needs Nginx (or any other webserver) to function and Nginx needs 90 + the certificates to actually start. For this reason the ACME module 91 + automatically generates self-signed certificates that will be used by Nginx to 92 + start. After that Nginx is used by ACME to retrieve the actual ACME 93 + certificates. <literal>security.acme.preliminarySelfsigned</literal> can be 94 + used to control whether to generate the self-signed certificates. 95 + </para> 96 + </section> 97 </chapter>