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