at 18.03-beta 13 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 certificates are re-issued. Typically 62 the web server and other servers using certificates need to 63 be reloaded. 64 ''; 65 }; 66 67 plugins = mkOption { 68 type = types.listOf (types.enum [ 69 "cert.der" "cert.pem" "chain.pem" "external.sh" 70 "fullchain.pem" "full.pem" "key.der" "key.pem" "account_key.json" 71 ]); 72 default = [ "fullchain.pem" "full.pem" "key.pem" "account_key.json" ]; 73 description = '' 74 Plugins to enable. With default settings simp_le will 75 store public certificate bundle in <filename>fullchain.pem</filename>, 76 private key in <filename>key.pem</filename> and those two previous 77 files combined in <filename>full.pem</filename> in its state directory. 78 ''; 79 }; 80 81 extraDomains = mkOption { 82 type = types.attrsOf (types.nullOr types.str); 83 default = {}; 84 example = literalExample '' 85 { 86 "example.org" = "/srv/http/nginx"; 87 "mydomain.org" = null; 88 } 89 ''; 90 description = '' 91 A list of extra domain names, which are included in the one certificate to be issued, with their 92 own server roots if needed. 93 ''; 94 }; 95 }; 96 }; 97 98in 99 100{ 101 102 ###### interface 103 104 options = { 105 security.acme = { 106 directory = mkOption { 107 default = "/var/lib/acme"; 108 type = types.str; 109 description = '' 110 Directory where certs and other state will be stored by default. 111 ''; 112 }; 113 114 validMin = mkOption { 115 type = types.int; 116 default = 30 * 24 * 3600; 117 description = "Minimum remaining validity before renewal in seconds."; 118 }; 119 120 renewInterval = mkOption { 121 type = types.str; 122 default = "weekly"; 123 description = '' 124 Systemd calendar expression when to check for renewal. See 125 <citerefentry><refentrytitle>systemd.time</refentrytitle> 126 <manvolnum>7</manvolnum></citerefentry>. 127 ''; 128 }; 129 130 preliminarySelfsigned = mkOption { 131 type = types.bool; 132 default = true; 133 description = '' 134 Whether a preliminary self-signed certificate should be generated before 135 doing ACME requests. This can be useful when certificates are required in 136 a webserver, but ACME needs the webserver to make its requests. 137 138 With preliminary self-signed certificate the webserver can be started and 139 can later reload the correct ACME certificates. 140 ''; 141 }; 142 143 tosHash = mkOption { 144 type = types.string; 145 default = "cc88d8d9517f490191401e7b54e9ffd12a2b9082ec7a1d4cec6101f9f1647e7b"; 146 description = '' 147 SHA256 of the Terms of Services document. This changes once in a while. 148 ''; 149 }; 150 151 production = mkOption { 152 type = types.bool; 153 default = true; 154 description = '' 155 If set to true, use Let's Encrypt's production environment 156 instead of the staging environment. The main benefit of the 157 staging environment is to get much higher rate limits. 158 159 See 160 <literal>https://letsencrypt.org/docs/staging-environment</literal> 161 for more detail. 162 ''; 163 }; 164 165 certs = mkOption { 166 default = { }; 167 type = with types; attrsOf (submodule certOpts); 168 description = '' 169 Attribute set of certificates to get signed and renewed. 170 ''; 171 example = literalExample '' 172 { 173 "example.com" = { 174 webroot = "/var/www/challenges/"; 175 email = "foo@example.com"; 176 extraDomains = { "www.example.com" = null; "foo.example.com" = "/var/www/foo/"; }; 177 }; 178 "bar.example.com" = { 179 webroot = "/var/www/challenges/"; 180 email = "bar@example.com"; 181 }; 182 } 183 ''; 184 }; 185 }; 186 }; 187 188 ###### implementation 189 config = mkMerge [ 190 (mkIf (cfg.certs != { }) { 191 192 systemd.services = let 193 services = concatLists servicesLists; 194 servicesLists = mapAttrsToList certToServices cfg.certs; 195 certToServices = cert: data: 196 let 197 cpath = "${cfg.directory}/${cert}"; 198 rights = if data.allowKeysForGroup then "750" else "700"; 199 cmdline = [ "-v" "-d" data.domain "--default_root" data.webroot "--valid_min" cfg.validMin "--tos_sha256" cfg.tosHash ] 200 ++ optionals (data.email != null) [ "--email" data.email ] 201 ++ concatMap (p: [ "-f" p ]) data.plugins 202 ++ concatLists (mapAttrsToList (name: root: [ "-d" (if root == null then name else "${name}:${root}")]) data.extraDomains) 203 ++ (if cfg.production then [] 204 else ["--server" "https://acme-staging.api.letsencrypt.org/directory"]); 205 acmeService = { 206 description = "Renew ACME Certificate for ${cert}"; 207 after = [ "network.target" "network-online.target" ]; 208 wants = [ "network-online.target" ]; 209 serviceConfig = { 210 Type = "oneshot"; 211 SuccessExitStatus = [ "0" "1" ]; 212 PermissionsStartOnly = true; 213 User = data.user; 214 Group = data.group; 215 PrivateTmp = true; 216 }; 217 path = [ pkgs.simp_le ]; 218 preStart = '' 219 mkdir -p '${cfg.directory}' 220 chown 'root:root' '${cfg.directory}' 221 chmod 755 '${cfg.directory}' 222 if [ ! -d '${cpath}' ]; then 223 mkdir '${cpath}' 224 fi 225 chmod ${rights} '${cpath}' 226 chown -R '${data.user}:${data.group}' '${cpath}' 227 mkdir -p '${data.webroot}/.well-known/acme-challenge' 228 chown -R '${data.user}:${data.group}' '${data.webroot}/.well-known/acme-challenge' 229 ''; 230 script = '' 231 cd '${cpath}' 232 set +e 233 simp_le ${escapeShellArgs cmdline} 234 EXITCODE=$? 235 set -e 236 echo "$EXITCODE" > /tmp/lastExitCode 237 exit "$EXITCODE" 238 ''; 239 postStop = '' 240 if [ -e /tmp/lastExitCode ] && [ "$(cat /tmp/lastExitCode)" = "0" ]; then 241 echo "Executing postRun hook..." 242 ${data.postRun} 243 fi 244 ''; 245 246 before = [ "acme-certificates.target" ]; 247 wantedBy = [ "acme-certificates.target" ]; 248 }; 249 selfsignedService = { 250 description = "Create preliminary self-signed certificate for ${cert}"; 251 preStart = '' 252 if [ ! -d '${cpath}' ] 253 then 254 mkdir -p '${cpath}' 255 chmod ${rights} '${cpath}' 256 chown '${data.user}:${data.group}' '${cpath}' 257 fi 258 ''; 259 script = 260 '' 261 # Create self-signed key 262 workdir="/run/acme-selfsigned-${cert}" 263 ${pkgs.openssl.bin}/bin/openssl genrsa -des3 -passout pass:x -out $workdir/server.pass.key 2048 264 ${pkgs.openssl.bin}/bin/openssl rsa -passin pass:x -in $workdir/server.pass.key -out $workdir/server.key 265 ${pkgs.openssl.bin}/bin/openssl req -new -key $workdir/server.key -out $workdir/server.csr \ 266 -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com" 267 ${pkgs.openssl.bin}/bin/openssl x509 -req -days 1 -in $workdir/server.csr -signkey $workdir/server.key -out $workdir/server.crt 268 269 # Move key to destination 270 mv $workdir/server.key ${cpath}/key.pem 271 mv $workdir/server.crt ${cpath}/fullchain.pem 272 273 # Create full.pem for e.g. lighttpd (same format as "simp_le ... -f full.pem" creates) 274 cat "${cpath}/key.pem" "${cpath}/fullchain.pem" > "${cpath}/full.pem" 275 276 # Clean up working directory 277 rm $workdir/server.csr 278 rm $workdir/server.pass.key 279 280 # Give key acme permissions 281 chmod ${rights} '${cpath}/key.pem' 282 chown '${data.user}:${data.group}' '${cpath}/key.pem' 283 chmod ${rights} '${cpath}/fullchain.pem' 284 chown '${data.user}:${data.group}' '${cpath}/fullchain.pem' 285 chmod ${rights} '${cpath}/full.pem' 286 chown '${data.user}:${data.group}' '${cpath}/full.pem' 287 ''; 288 serviceConfig = { 289 Type = "oneshot"; 290 RuntimeDirectory = "acme-selfsigned-${cert}"; 291 PermissionsStartOnly = true; 292 User = data.user; 293 Group = data.group; 294 }; 295 unitConfig = { 296 # Do not create self-signed key when key already exists 297 ConditionPathExists = "!${cpath}/key.pem"; 298 }; 299 before = [ 300 "acme-selfsigned-certificates.target" 301 ]; 302 wantedBy = [ 303 "acme-selfsigned-certificates.target" 304 ]; 305 }; 306 in ( 307 [ { name = "acme-${cert}"; value = acmeService; } ] 308 ++ 309 (if cfg.preliminarySelfsigned 310 then [ { name = "acme-selfsigned-${cert}"; value = selfsignedService; } ] 311 else [] 312 ) 313 ); 314 servicesAttr = listToAttrs services; 315 injectServiceDep = { 316 after = [ "acme-selfsigned-certificates.target" ]; 317 wants = [ "acme-selfsigned-certificates.target" "acme-certificates.target" ]; 318 }; 319 in 320 servicesAttr // 321 (if config.services.nginx.enable then { nginx = injectServiceDep; } else {}) // 322 (if config.services.lighttpd.enable then { lighttpd = injectServiceDep; } else {}); 323 324 systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair 325 ("acme-${cert}") 326 ({ 327 description = "Renew ACME Certificate for ${cert}"; 328 wantedBy = [ "timers.target" ]; 329 timerConfig = { 330 OnCalendar = cfg.renewInterval; 331 Unit = "acme-${cert}.service"; 332 Persistent = "yes"; 333 AccuracySec = "5m"; 334 RandomizedDelaySec = "1h"; 335 }; 336 }) 337 ); 338 339 systemd.targets."acme-selfsigned-certificates" = mkIf cfg.preliminarySelfsigned {}; 340 systemd.targets."acme-certificates" = {}; 341 }) 342 343 ]; 344 345 meta = { 346 maintainers = with lib.maintainers; [ abbradar fpletz globin ]; 347 doc = ./acme.xml; 348 }; 349}