]> git.immae.eu Git - perso/Immae/Config/Nix.git/blame - modules/private/certificates.nix
Fix issue in ISRG script that is not idempotent
[perso/Immae/Config/Nix.git] / modules / private / certificates.nix
CommitLineData
6e9f30f4 1{ lib, pkgs, config, name, ... }:
3013caf1 2{
8415083e
IB
3 options.myServices.certificates = {
4 enable = lib.mkEnableOption "enable certificates";
cfda3cfc
IB
5 webroot = lib.mkOption {
6 readOnly = true;
7 default = "/var/lib/acme/acme-challenges";
8 };
3013caf1
IB
9 certConfig = lib.mkOption {
10 default = {
cfda3cfc 11 webroot = lib.mkForce null; # avoids creation of tmpfiles
3013caf1 12 email = "ismael@bouya.org";
6e9f30f4
IB
13 postRun = builtins.concatStringsSep "\n" [
14 (lib.optionalString config.services.httpd.Prod.enable "systemctl reload httpdProd.service")
15 (lib.optionalString config.services.httpd.Tools.enable "systemctl reload httpdTools.service")
16 (lib.optionalString config.services.httpd.Inte.enable "systemctl reload httpdInte.service")
17 (lib.optionalString config.services.nginx.enable "systemctl reload nginx.service")
18 ];
f5761aac 19 extraLegoRenewFlags = [ "--reuse-key" ];
cfda3cfc 20 keyType = lib.mkDefault "ec256"; # https://github.com/NixOS/nixpkgs/pull/83121
3013caf1
IB
21 };
22 description = "Default configuration for certificates";
23 };
24 };
25
8415083e 26 config = lib.mkIf config.myServices.certificates.enable {
6e9f30f4
IB
27 services.nginx = {
28 recommendedTlsSettings = true;
3ffa15ba
IB
29 virtualHosts = {
30 "${config.hostEnv.fqdn}" = {
cfda3cfc 31 acmeRoot = config.myServices.certificates.webroot;
3ffa15ba
IB
32 useACMEHost = name;
33 forceSSL = true;
34 };
35 };
6e9f30f4 36 };
8415083e
IB
37 services.websites.certs = config.myServices.certificates.certConfig;
38 myServices.databasesCerts = config.myServices.certificates.certConfig;
39 myServices.ircCerts = config.myServices.certificates.certConfig;
7df420c2 40
258dd18b 41 security.acme.acceptTerms = true;
5400b9b6 42 security.acme.preliminarySelfsigned = true;
3013caf1 43
5400b9b6 44 security.acme.certs = {
6e9f30f4 45 "${name}" = config.myServices.certificates.certConfig // {
619e4f46 46 domain = config.hostEnv.fqdn;
3013caf1
IB
47 };
48 };
017cb76f 49
cfda3cfc
IB
50 users.users.acme = {
51 uid = config.ids.uids.acme;
52 group = "acme";
53 description = "Acme user";
54 };
55 users.groups.acme = {
56 gid = config.ids.gids.acme;
57 };
58
017cb76f 59 systemd.services = lib.attrsets.mapAttrs' (k: v:
2fe37e49
IB
60 lib.attrsets.nameValuePair "acme-selfsigned-${k}" {
61 wantedBy = [ "acme-selfsigned-certificates.target" ];
62 script = lib.mkAfter ''
63 cp $workdir/server.crt ${config.security.acme.certs."${k}".directory}/cert.pem
64 chown '${v.user}:${v.group}' ${config.security.acme.certs."${k}".directory}/cert.pem
65 chmod ${if v.allowKeysForGroup then "750" else "700"} ${config.security.acme.certs."${k}".directory}/cert.pem
258dd18b 66
2fe37e49
IB
67 cp $workdir/ca.crt ${config.security.acme.certs."${k}".directory}/chain.pem
68 chown '${v.user}:${v.group}' ${config.security.acme.certs."${k}".directory}/chain.pem
69 chmod ${if v.allowKeysForGroup then "750" else "700"} ${config.security.acme.certs."${k}".directory}/chain.pem
70 '';
71 }
72 ) config.security.acme.certs //
5400b9b6
IB
73 lib.attrsets.mapAttrs' (k: data:
74 lib.attrsets.nameValuePair "acme-${k}" {
37465bc7 75 after = lib.mkAfter [ "bind.service" ];
cfda3cfc 76 serviceConfig =
364b709f 77 let
cfda3cfc
IB
78 cfg = config.security.acme;
79 hashOptions = let
80 domains = builtins.concatStringsSep "," (
81 [ data.domain ] ++ (builtins.attrNames data.extraDomains)
82 );
83 certOptions = builtins.concatStringsSep "," [
84 (if data.ocspMustStaple then "must-staple" else "no-must-staple")
85 ];
86 in
87 builtins.hashString "sha256" (builtins.concatStringsSep ";" [ data.keyType domains certOptions ]);
88 accountsDir = "accounts-${data.keyType}";
89 lpath = "acme/${k}";
90 apath = "/var/lib/${lpath}";
91 spath = "/var/lib/acme/.lego/${k}";
364b709f 92 fileMode = if data.allowKeysForGroup then "640" else "600";
cfda3cfc
IB
93 dirFileMode = if data.allowKeysForGroup then "750" else "700";
94 globalOpts = [ "-d" data.domain "--email" data.email "--path" "." "--key-type" data.keyType ]
95 ++ lib.optionals (cfg.acceptTerms) [ "--accept-tos" ]
96 ++ lib.optionals (data.dnsProvider != null && !data.dnsPropagationCheck) [ "--dns.disable-cp" ]
97 ++ lib.concatLists (lib.mapAttrsToList (name: root: [ "-d" name ]) data.extraDomains)
98 ++ (if data.dnsProvider != null then [ "--dns" data.dnsProvider ] else [ "--http" "--http.webroot" config.myServices.certificates.webroot ])
99 ++ lib.optionals (cfg.server != null || data.server != null) ["--server" (if data.server == null then cfg.server else data.server)];
100 certOpts = lib.optionals data.ocspMustStaple [ "--must-staple" ];
101 runOpts = lib.escapeShellArgs (globalOpts ++ [ "run" ] ++ certOpts);
102 renewOpts = lib.escapeShellArgs (globalOpts ++
103 [ "renew" "--days" (builtins.toString cfg.validMinDays) ] ++
104 certOpts ++ data.extraLegoRenewFlags);
105 forceRenewOpts = lib.escapeShellArgs (globalOpts ++
106 [ "renew" "--days" "999" ] ++
107 certOpts ++ data.extraLegoRenewFlags);
108 keyName = builtins.replaceStrings ["*"] ["_"] data.domain;
109 in {
110 User = lib.mkForce "acme";
111 Group = lib.mkForce "acme";
112 WorkingDirectory = lib.mkForce spath;
113 StateDirectory = lib.mkForce "acme/.lego/${k} acme/.lego/${accountsDir}";
114 ExecStartPre =
115 let
116 script = pkgs.writeScript "acme-prestart" ''
117 #!${pkgs.runtimeShell} -e
118 install -m 0755 -o acme -g acme -d ${config.myServices.certificates.webroot}
119 '';
120 in
121 lib.mkForce "+${script}";
122 ExecStart = lib.mkForce (pkgs.writeScript "acme-start" ''
364b709f 123 #!${pkgs.runtimeShell} -e
cfda3cfc
IB
124 # lego doesn't check key type after initial creation, we
125 # need to check for him
126 if [ -L ${spath}/accounts -o -d ${spath}/accounts ]; then
127 if [ -L ${spath}/accounts -a "$(readlink ${spath}/accounts)" != ../${accountsDir} ]; then
128 ln -sfn ../${accountsDir} ${spath}/accounts
129 mv -f ${spath}/certificates/${keyName}.key ${spath}/certificates/${keyName}.key.old
130 fi
131 else
132 ln -s ../${accountsDir} ${spath}/accounts
133 fi
134 # check if domain changed: lego doesn't check by itself
135 if [ ! -e ${spath}/certificates/${keyName}.crt -o ! -e ${spath}/certificates/${keyName}.key -o ! -e "${spath}/accounts/acme-v02.api.letsencrypt.org/${data.email}/account.json" ]; then
136 ${pkgs.lego}/bin/lego ${runOpts}
137 elif [ ! -f ${spath}/currentDomains -o "$(cat ${spath}/currentDomains)" != "${hashOptions}" ]; then
138 ${pkgs.lego}/bin/lego ${forceRenewOpts}
139 else
140 ${pkgs.lego}/bin/lego ${renewOpts}
364b709f 141 fi
cfda3cfc
IB
142 '');
143 ExecStartPost =
144 let
ad6d50d9
IB
145 ISRG_Root_X1 = pkgs.fetchurl {
146 url = "https://letsencrypt.org/certs/isrgrootx1.pem";
147 sha256 = "1la36n2f31j9s03v847ig6ny9lr875q3g7smnq33dcsmf2i5gd92";
148 };
149 fix_ISRG_Root_X1 = pkgs.writeScript "fix-pem" ''
4ec2d441
IB
150 for file in chain fullchain full; do
151 if grep -q MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA "$file.pem"; then
152 cat ${ISRG_Root_X1} | grep -v " CERTIFICATE" | \
153 sed -i.bak -ne "/MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/ {r /dev/stdin" -e ":a; n; /Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5/ { b }; ba };p" $file.pem
154 fi
155 done
ad6d50d9 156 '';
cfda3cfc
IB
157 script = pkgs.writeScript "acme-post-start" ''
158 #!${pkgs.runtimeShell} -e
159 install -m 0755 -o root -g root -d /var/lib/acme
160 install -m 0${dirFileMode} -o ${data.user} -g ${data.group} -d /var/lib/acme/${k}
161 cd /var/lib/acme/${k}
162
163 # Test that existing cert is older than new cert
164 KEY=${spath}/certificates/${keyName}.key
165 KEY_CHANGED=no
166 if [ -e $KEY -a $KEY -nt key.pem ]; then
167 KEY_CHANGED=yes
168 cp -p ${spath}/certificates/${keyName}.key key.pem
169 cp -p ${spath}/certificates/${keyName}.crt fullchain.pem
170 cp -p ${spath}/certificates/${keyName}.issuer.crt chain.pem
171 ln -sf fullchain.pem cert.pem
172 cat key.pem fullchain.pem > full.pem
173 echo -n "${hashOptions}" > ${spath}/currentDomains
174 fi
364b709f 175
4ec2d441 176 ${fix_ISRG_Root_X1}
cfda3cfc
IB
177 chmod ${fileMode} *.pem
178 chown '${data.user}:${data.group}' *.pem
364b709f 179
cfda3cfc
IB
180 if [ "$KEY_CHANGED" = "yes" ]; then
181 : # noop in case postRun is empty
182 ${data.postRun}
183 fi
184 '';
185 in
186 lib.mkForce "+${script}";
187 };
5400b9b6
IB
188 }
189 ) config.security.acme.certs //
190 {
6e9f30f4
IB
191 httpdProd = lib.mkIf config.services.httpd.Prod.enable
192 { after = [ "acme-selfsigned-certificates.target" ]; wants = [ "acme-selfsigned-certificates.target" ]; };
193 httpdTools = lib.mkIf config.services.httpd.Tools.enable
194 { after = [ "acme-selfsigned-certificates.target" ]; wants = [ "acme-selfsigned-certificates.target" ]; };
195 httpdInte = lib.mkIf config.services.httpd.Inte.enable
196 { after = [ "acme-selfsigned-certificates.target" ]; wants = [ "acme-selfsigned-certificates.target" ]; };
017cb76f 197 };
3013caf1
IB
198 };
199}