at 18.09-beta 15 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 7 cfg = config.security.acme; 8 9 certOpts = { name, ... }: { 10 options = { 11 webroot = mkOption { 12 type = types.str; 13 example = "/var/lib/acme/acme-challenges"; 14 description = '' 15 Where the webroot of the HTTP vhost is located. 16 <filename>.well-known/acme-challenge/</filename> directory 17 will be created below the webroot if it doesn't exist. 18 <literal>http://example.org/.well-known/acme-challenge/</literal> must also 19 be available (notice unencrypted HTTP). 20 ''; 21 }; 22 23 domain = mkOption { 24 type = types.str; 25 default = name; 26 description = "Domain to fetch certificate for (defaults to the entry name)"; 27 }; 28 29 email = mkOption { 30 type = types.nullOr types.str; 31 default = null; 32 description = "Contact email address for the CA to be able to reach you."; 33 }; 34 35 user = mkOption { 36 type = types.str; 37 default = "root"; 38 description = "User running the ACME client."; 39 }; 40 41 group = mkOption { 42 type = types.str; 43 default = "root"; 44 description = "Group running the ACME client."; 45 }; 46 47 allowKeysForGroup = mkOption { 48 type = types.bool; 49 default = false; 50 description = '' 51 Give read permissions to the specified group 52 (<option>security.acme.cert.&lt;name&gt;.group</option>) to read SSL private certificates. 53 ''; 54 }; 55 56 postRun = mkOption { 57 type = types.lines; 58 default = ""; 59 example = "systemctl reload nginx.service"; 60 description = '' 61 Commands to run after new certificates go live. Typically 62 the web server and other servers using certificates need to 63 be reloaded. 64 65 Executed in the same directory with the new certificate. 66 ''; 67 }; 68 69 plugins = mkOption { 70 type = types.listOf (types.enum [ 71 "cert.der" "cert.pem" "chain.pem" "external.sh" 72 "fullchain.pem" "full.pem" "key.der" "key.pem" "account_key.json" 73 ]); 74 default = [ "fullchain.pem" "full.pem" "key.pem" "account_key.json" ]; 75 description = '' 76 Plugins to enable. With default settings simp_le will 77 store public certificate bundle in <filename>fullchain.pem</filename>, 78 private key in <filename>key.pem</filename> and those two previous 79 files combined in <filename>full.pem</filename> in its state directory. 80 ''; 81 }; 82 83 activationDelay = mkOption { 84 type = types.nullOr types.str; 85 default = null; 86 description = '' 87 Systemd time span expression to delay copying new certificates to main 88 state directory. See <citerefentry><refentrytitle>systemd.time</refentrytitle> 89 <manvolnum>7</manvolnum></citerefentry>. 90 ''; 91 }; 92 93 preDelay = mkOption { 94 type = types.lines; 95 default = ""; 96 description = '' 97 Commands to run after certificates are re-issued but before they are 98 activated. Typically the new certificate is published to DNS. 99 100 Executed in the same directory with the new certificate. 101 ''; 102 }; 103 104 extraDomains = mkOption { 105 type = types.attrsOf (types.nullOr types.str); 106 default = {}; 107 example = literalExample '' 108 { 109 "example.org" = "/srv/http/nginx"; 110 "mydomain.org" = null; 111 } 112 ''; 113 description = '' 114 A list of extra domain names, which are included in the one certificate to be issued, with their 115 own server roots if needed. 116 ''; 117 }; 118 }; 119 }; 120 121in 122 123{ 124 125 ###### interface 126 127 options = { 128 security.acme = { 129 directory = mkOption { 130 default = "/var/lib/acme"; 131 type = types.str; 132 description = '' 133 Directory where certs and other state will be stored by default. 134 ''; 135 }; 136 137 validMin = mkOption { 138 type = types.int; 139 default = 30 * 24 * 3600; 140 description = "Minimum remaining validity before renewal in seconds."; 141 }; 142 143 renewInterval = mkOption { 144 type = types.str; 145 default = "weekly"; 146 description = '' 147 Systemd calendar expression when to check for renewal. See 148 <citerefentry><refentrytitle>systemd.time</refentrytitle> 149 <manvolnum>7</manvolnum></citerefentry>. 150 ''; 151 }; 152 153 preliminarySelfsigned = mkOption { 154 type = types.bool; 155 default = true; 156 description = '' 157 Whether a preliminary self-signed certificate should be generated before 158 doing ACME requests. This can be useful when certificates are required in 159 a webserver, but ACME needs the webserver to make its requests. 160 161 With preliminary self-signed certificate the webserver can be started and 162 can later reload the correct ACME certificates. 163 ''; 164 }; 165 166 production = mkOption { 167 type = types.bool; 168 default = true; 169 description = '' 170 If set to true, use Let's Encrypt's production environment 171 instead of the staging environment. The main benefit of the 172 staging environment is to get much higher rate limits. 173 174 See 175 <literal>https://letsencrypt.org/docs/staging-environment</literal> 176 for more detail. 177 ''; 178 }; 179 180 certs = mkOption { 181 default = { }; 182 type = with types; attrsOf (submodule certOpts); 183 description = '' 184 Attribute set of certificates to get signed and renewed. 185 ''; 186 example = literalExample '' 187 { 188 "example.com" = { 189 webroot = "/var/www/challenges/"; 190 email = "foo@example.com"; 191 extraDomains = { "www.example.com" = null; "foo.example.com" = "/var/www/foo/"; }; 192 }; 193 "bar.example.com" = { 194 webroot = "/var/www/challenges/"; 195 email = "bar@example.com"; 196 }; 197 } 198 ''; 199 }; 200 }; 201 }; 202 203 ###### implementation 204 config = mkMerge [ 205 (mkIf (cfg.certs != { }) { 206 207 systemd.services = let 208 services = concatLists servicesLists; 209 servicesLists = mapAttrsToList certToServices cfg.certs; 210 certToServices = cert: data: 211 let 212 cpath = lpath + optionalString (data.activationDelay != null) ".staging"; 213 lpath = "${cfg.directory}/${cert}"; 214 rights = if data.allowKeysForGroup then "750" else "700"; 215 cmdline = [ "-v" "-d" data.domain "--default_root" data.webroot "--valid_min" cfg.validMin ] 216 ++ optionals (data.email != null) [ "--email" data.email ] 217 ++ concatMap (p: [ "-f" p ]) data.plugins 218 ++ concatLists (mapAttrsToList (name: root: [ "-d" (if root == null then name else "${name}:${root}")]) data.extraDomains) 219 ++ optionals (!cfg.production) ["--server" "https://acme-staging.api.letsencrypt.org/directory"]; 220 acmeService = { 221 description = "Renew ACME Certificate for ${cert}"; 222 after = [ "network.target" "network-online.target" ]; 223 wants = [ "network-online.target" ]; 224 serviceConfig = { 225 Type = "oneshot"; 226 SuccessExitStatus = [ "0" "1" ]; 227 PermissionsStartOnly = true; 228 User = data.user; 229 Group = data.group; 230 PrivateTmp = true; 231 }; 232 path = with pkgs; [ simp_le systemd ]; 233 preStart = '' 234 mkdir -p '${cfg.directory}' 235 chown 'root:root' '${cfg.directory}' 236 chmod 755 '${cfg.directory}' 237 if [ ! -d '${cpath}' ]; then 238 mkdir '${cpath}' 239 fi 240 chmod ${rights} '${cpath}' 241 chown -R '${data.user}:${data.group}' '${cpath}' 242 mkdir -p '${data.webroot}/.well-known/acme-challenge' 243 chown -R '${data.user}:${data.group}' '${data.webroot}/.well-known/acme-challenge' 244 ''; 245 script = '' 246 cd '${cpath}' 247 set +e 248 simp_le ${escapeShellArgs cmdline} 249 EXITCODE=$? 250 set -e 251 echo "$EXITCODE" > /tmp/lastExitCode 252 exit "$EXITCODE" 253 ''; 254 postStop = '' 255 cd '${cpath}' 256 257 if [ -e /tmp/lastExitCode ] && [ "$(cat /tmp/lastExitCode)" = "0" ]; then 258 ${if data.activationDelay != null then '' 259 260 ${data.preDelay} 261 262 if [ -d '${lpath}' ]; then 263 systemd-run --no-block --on-active='${data.activationDelay}' --unit acme-setlive-${cert}.service 264 else 265 systemctl --wait start acme-setlive-${cert}.service 266 fi 267 '' else data.postRun} 268 269 # noop ensuring that the "if" block is non-empty even if 270 # activationDelay == null and postRun == "" 271 true 272 fi 273 ''; 274 275 before = [ "acme-certificates.target" ]; 276 wantedBy = [ "acme-certificates.target" ]; 277 }; 278 delayService = { 279 description = "Set certificate for ${cert} live"; 280 path = with pkgs; [ rsync ]; 281 serviceConfig = { 282 Type = "oneshot"; 283 }; 284 script = '' 285 rsync -a --delete-after '${cpath}/' '${lpath}' 286 ''; 287 postStop = data.postRun; 288 }; 289 selfsignedService = { 290 description = "Create preliminary self-signed certificate for ${cert}"; 291 path = [ pkgs.openssl ]; 292 preStart = '' 293 if [ ! -d '${cpath}' ] 294 then 295 mkdir -p '${cpath}' 296 chmod ${rights} '${cpath}' 297 chown '${data.user}:${data.group}' '${cpath}' 298 fi 299 ''; 300 script = 301 '' 302 workdir="$(mktemp -d)" 303 304 # Create CA 305 openssl genrsa -des3 -passout pass:x -out $workdir/ca.pass.key 2048 306 openssl rsa -passin pass:x -in $workdir/ca.pass.key -out $workdir/ca.key 307 openssl req -new -key $workdir/ca.key -out $workdir/ca.csr \ 308 -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=Security Department/CN=example.com" 309 openssl x509 -req -days 1 -in $workdir/ca.csr -signkey $workdir/ca.key -out $workdir/ca.crt 310 311 # Create key 312 openssl genrsa -des3 -passout pass:x -out $workdir/server.pass.key 2048 313 openssl rsa -passin pass:x -in $workdir/server.pass.key -out $workdir/server.key 314 openssl req -new -key $workdir/server.key -out $workdir/server.csr \ 315 -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com" 316 openssl x509 -req -days 1 -in $workdir/server.csr -CA $workdir/ca.crt \ 317 -CAkey $workdir/ca.key -CAserial $workdir/ca.srl -CAcreateserial \ 318 -out $workdir/server.crt 319 320 # Copy key to destination 321 cp $workdir/server.key ${cpath}/key.pem 322 323 # Create fullchain.pem (same format as "simp_le ... -f fullchain.pem" creates) 324 cat $workdir/{server.crt,ca.crt} > "${cpath}/fullchain.pem" 325 326 # Create full.pem for e.g. lighttpd 327 cat $workdir/{server.key,server.crt,ca.crt} > "${cpath}/full.pem" 328 329 # Give key acme permissions 330 chown '${data.user}:${data.group}' "${cpath}/"{key,fullchain,full}.pem 331 chmod ${rights} "${cpath}/"{key,fullchain,full}.pem 332 ''; 333 serviceConfig = { 334 Type = "oneshot"; 335 PermissionsStartOnly = true; 336 PrivateTmp = true; 337 User = data.user; 338 Group = data.group; 339 }; 340 unitConfig = { 341 # Do not create self-signed key when key already exists 342 ConditionPathExists = "!${cpath}/key.pem"; 343 }; 344 before = [ 345 "acme-selfsigned-certificates.target" 346 ]; 347 wantedBy = [ 348 "acme-selfsigned-certificates.target" 349 ]; 350 }; 351 in ( 352 [ { name = "acme-${cert}"; value = acmeService; } ] 353 ++ optional cfg.preliminarySelfsigned { name = "acme-selfsigned-${cert}"; value = selfsignedService; } 354 ++ optional (data.activationDelay != null) { name = "acme-setlive-${cert}"; value = delayService; } 355 ); 356 servicesAttr = listToAttrs services; 357 injectServiceDep = { 358 after = [ "acme-selfsigned-certificates.target" ]; 359 wants = [ "acme-selfsigned-certificates.target" "acme-certificates.target" ]; 360 }; 361 in 362 servicesAttr // 363 (if config.services.nginx.enable then { nginx = injectServiceDep; } else {}) // 364 (if config.services.lighttpd.enable then { lighttpd = injectServiceDep; } else {}); 365 366 systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair 367 ("acme-${cert}") 368 ({ 369 description = "Renew ACME Certificate for ${cert}"; 370 wantedBy = [ "timers.target" ]; 371 timerConfig = { 372 OnCalendar = cfg.renewInterval; 373 Unit = "acme-${cert}.service"; 374 Persistent = "yes"; 375 AccuracySec = "5m"; 376 RandomizedDelaySec = "1h"; 377 }; 378 }) 379 ); 380 381 systemd.targets."acme-selfsigned-certificates" = mkIf cfg.preliminarySelfsigned {}; 382 systemd.targets."acme-certificates" = {}; 383 }) 384 385 ]; 386 387 meta = { 388 maintainers = with lib.maintainers; [ abbradar fpletz globin ]; 389 doc = ./acme.xml; 390 }; 391}