]> git.immae.eu Git - perso/Immae/Config/Nix.git/blob - modules/private/certificates.nix
Fix ISRG root certificate chain
[perso/Immae/Config/Nix.git] / modules / private / certificates.nix
1 { lib, pkgs, config, name, ... }:
2 {
3 options.myServices.certificates = {
4 enable = lib.mkEnableOption "enable certificates";
5 webroot = lib.mkOption {
6 readOnly = true;
7 default = "/var/lib/acme/acme-challenges";
8 };
9 certConfig = lib.mkOption {
10 default = {
11 webroot = lib.mkForce null; # avoids creation of tmpfiles
12 email = "ismael@bouya.org";
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 ];
19 extraLegoRenewFlags = [ "--reuse-key" ];
20 keyType = lib.mkDefault "ec256"; # https://github.com/NixOS/nixpkgs/pull/83121
21 };
22 description = "Default configuration for certificates";
23 };
24 };
25
26 config = lib.mkIf config.myServices.certificates.enable {
27 services.nginx = {
28 recommendedTlsSettings = true;
29 virtualHosts = {
30 "${config.hostEnv.fqdn}" = {
31 acmeRoot = config.myServices.certificates.webroot;
32 useACMEHost = name;
33 forceSSL = true;
34 };
35 };
36 };
37 services.websites.certs = config.myServices.certificates.certConfig;
38 myServices.databasesCerts = config.myServices.certificates.certConfig;
39 myServices.ircCerts = config.myServices.certificates.certConfig;
40
41 security.acme.acceptTerms = true;
42 security.acme.preliminarySelfsigned = true;
43
44 security.acme.certs = {
45 "${name}" = config.myServices.certificates.certConfig // {
46 domain = config.hostEnv.fqdn;
47 };
48 };
49
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
59 systemd.services = lib.attrsets.mapAttrs' (k: v:
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
66
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 //
73 lib.attrsets.mapAttrs' (k: data:
74 lib.attrsets.nameValuePair "acme-${k}" {
75 after = lib.mkAfter [ "bind.service" ];
76 serviceConfig =
77 let
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}";
92 fileMode = if data.allowKeysForGroup then "640" else "600";
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" ''
123 #!${pkgs.runtimeShell} -e
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}
141 fi
142 '');
143 ExecStartPost =
144 let
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" ''
150 cat ${ISRG_Root_X1} | grep -v " CERTIFICATE" | \
151 sed -i.bak -ne "/MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/ {r /dev/stdin" -e ":a; n; /Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5/ { b }; ba };p" chain.pem fullchain.pem full.pem
152 '';
153 script = pkgs.writeScript "acme-post-start" ''
154 #!${pkgs.runtimeShell} -e
155 install -m 0755 -o root -g root -d /var/lib/acme
156 install -m 0${dirFileMode} -o ${data.user} -g ${data.group} -d /var/lib/acme/${k}
157 cd /var/lib/acme/${k}
158
159 # Test that existing cert is older than new cert
160 KEY=${spath}/certificates/${keyName}.key
161 KEY_CHANGED=no
162 if [ -e $KEY -a $KEY -nt key.pem ]; then
163 KEY_CHANGED=yes
164 cp -p ${spath}/certificates/${keyName}.key key.pem
165 cp -p ${spath}/certificates/${keyName}.crt fullchain.pem
166 cp -p ${spath}/certificates/${keyName}.issuer.crt chain.pem
167 ln -sf fullchain.pem cert.pem
168 cat key.pem fullchain.pem > full.pem
169 echo -n "${hashOptions}" > ${spath}/currentDomains
170 fi
171
172 chmod ${fileMode} *.pem
173 chown '${data.user}:${data.group}' *.pem
174 ${fix_ISRG_Root_X1}
175
176 if [ "$KEY_CHANGED" = "yes" ]; then
177 : # noop in case postRun is empty
178 ${data.postRun}
179 fi
180 '';
181 in
182 lib.mkForce "+${script}";
183 };
184 }
185 ) config.security.acme.certs //
186 {
187 httpdProd = lib.mkIf config.services.httpd.Prod.enable
188 { after = [ "acme-selfsigned-certificates.target" ]; wants = [ "acme-selfsigned-certificates.target" ]; };
189 httpdTools = lib.mkIf config.services.httpd.Tools.enable
190 { after = [ "acme-selfsigned-certificates.target" ]; wants = [ "acme-selfsigned-certificates.target" ]; };
191 httpdInte = lib.mkIf config.services.httpd.Inte.enable
192 { after = [ "acme-selfsigned-certificates.target" ]; wants = [ "acme-selfsigned-certificates.target" ]; };
193 };
194 };
195 }