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 certs = mkOption { 118 default = { }; 119 type = types.loaOf types.optionSet; 120 description = '' 121 Attribute set of certificates to get signed and renewed. 122 ''; 123 options = [ certOpts ]; 124 example = { 125 "example.com" = { 126 webroot = "/var/www/challenges/"; 127 email = "foo@example.com"; 128 extraDomains = { "www.example.com" = null; "foo.example.com" = "/var/www/foo/"; }; 129 }; 130 "bar.example.com" = { 131 webroot = "/var/www/challenges/"; 132 email = "bar@example.com"; 133 }; 134 }; 135 }; 136 }; 137 }; 138 139 ###### implementation 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 = "ACME cert renewal for ${cert} using simp_le"; 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}") 194 ({ 195 description = "timer for ACME cert renewal of ${cert}"; 196 wantedBy = [ "timers.target" ]; 197 timerConfig = { 198 OnCalendar = cfg.renewInterval; 199 Unit = "acme-${cert}.service"; 200 }; 201 }) 202 ); 203 }) 204 205 { meta.maintainers = with lib.maintainers; [ abbradar fpletz globin ]; 206 meta.doc = ./acme.xml; 207 } 208 ]; 209 210}