at 16.09-beta 11 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 automatically 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 email = mkOption { 23 type = types.nullOr types.str; 24 default = null; 25 description = "Contact email address for the CA to be able to reach you."; 26 }; 27 28 user = mkOption { 29 type = types.str; 30 default = "root"; 31 description = "User running the ACME client."; 32 }; 33 34 group = mkOption { 35 type = types.str; 36 default = "root"; 37 description = "Group running the ACME client."; 38 }; 39 40 allowKeysForGroup = mkOption { 41 type = types.bool; 42 default = false; 43 description = "Give read permissions to the specified group to read SSL private certificates."; 44 }; 45 46 postRun = mkOption { 47 type = types.lines; 48 default = ""; 49 example = "systemctl reload nginx.service"; 50 description = '' 51 Commands to run after certificates are re-issued. Typically 52 the web server and other servers using certificates need to 53 be reloaded. 54 ''; 55 }; 56 57 plugins = mkOption { 58 type = types.listOf (types.enum [ 59 "cert.der" "cert.pem" "chain.pem" "external.sh" 60 "fullchain.pem" "full.pem" "key.der" "key.pem" "account_key.json" 61 ]); 62 default = [ "fullchain.pem" "key.pem" "account_key.json" ]; 63 description = '' 64 Plugins to enable. With default settings simp_le will 65 store public certificate bundle in <filename>fullchain.pem</filename> 66 and private key in <filename>key.pem</filename> in its state directory. 67 ''; 68 }; 69 70 extraDomains = mkOption { 71 type = types.attrsOf (types.nullOr types.str); 72 default = {}; 73 example = { 74 "example.org" = "/srv/http/nginx"; 75 "mydomain.org" = null; 76 }; 77 description = '' 78 Extra domain names for which certificates are to be issued, with their 79 own server roots if needed. 80 ''; 81 }; 82 }; 83 }; 84 85in 86 87{ 88 89 ###### interface 90 91 options = { 92 security.acme = { 93 directory = mkOption { 94 default = "/var/lib/acme"; 95 type = types.str; 96 description = '' 97 Directory where certs and other state will be stored by default. 98 ''; 99 }; 100 101 validMin = mkOption { 102 type = types.int; 103 default = 30 * 24 * 3600; 104 description = "Minimum remaining validity before renewal in seconds."; 105 }; 106 107 renewInterval = mkOption { 108 type = types.str; 109 default = "weekly"; 110 description = '' 111 Systemd calendar expression when to check for renewal. See 112 <citerefentry><refentrytitle>systemd.time</refentrytitle> 113 <manvolnum>5</manvolnum></citerefentry>. 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; 133 description = '' 134 Attribute set of certificates to get signed and renewed. 135 ''; 136 options = [ certOpts ]; 137 example = { 138 "example.com" = { 139 webroot = "/var/www/challenges/"; 140 email = "foo@example.com"; 141 extraDomains = { "www.example.com" = null; "foo.example.com" = "/var/www/foo/"; }; 142 }; 143 "bar.example.com" = { 144 webroot = "/var/www/challenges/"; 145 email = "bar@example.com"; 146 }; 147 }; 148 }; 149 }; 150 }; 151 152 ###### implementation 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 ${escapeShellArgs 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}") 279 ({ 280 description = "Renew ACME Certificate for ${cert}"; 281 wantedBy = [ "timers.target" ]; 282 timerConfig = { 283 OnCalendar = cfg.renewInterval; 284 Unit = "acme-${cert}.service"; 285 }; 286 }) 287 ); 288 289 systemd.targets."acme-selfsigned-certificates" = mkIf cfg.preliminarySelfsigned {}; 290 systemd.targets."acme-certificates" = {}; 291 }) 292 293 ]; 294 295 meta = { 296 maintainers = with lib.maintainers; [ abbradar fpletz globin ]; 297 doc = ./acme.xml; 298 }; 299}