{ lib, pkgs, config, name, ... }: { options.myServices.certificates = { enable = lib.mkEnableOption "enable certificates"; webroot = lib.mkOption { readOnly = true; default = "/var/lib/acme/acme-challenges"; }; certConfig = lib.mkOption { default = { webroot = lib.mkForce null; # avoids creation of tmpfiles email = "ismael@bouya.org"; postRun = builtins.concatStringsSep "\n" [ (lib.optionalString config.services.httpd.Prod.enable "systemctl reload httpdProd.service") (lib.optionalString config.services.httpd.Tools.enable "systemctl reload httpdTools.service") (lib.optionalString config.services.httpd.Inte.enable "systemctl reload httpdInte.service") (lib.optionalString config.services.nginx.enable "systemctl reload nginx.service") ]; extraLegoRenewFlags = [ "--reuse-key" ]; keyType = lib.mkDefault "ec256"; # https://github.com/NixOS/nixpkgs/pull/83121 }; description = "Default configuration for certificates"; }; }; config = lib.mkIf config.myServices.certificates.enable { services.nginx = { recommendedTlsSettings = true; virtualHosts = { "${config.hostEnv.fqdn}" = { acmeRoot = config.myServices.certificates.webroot; useACMEHost = name; forceSSL = true; }; }; }; services.websites.certs = config.myServices.certificates.certConfig; myServices.databasesCerts = config.myServices.certificates.certConfig; myServices.ircCerts = config.myServices.certificates.certConfig; security.acme.acceptTerms = true; security.acme.preliminarySelfsigned = true; security.acme.certs = { "${name}" = config.myServices.certificates.certConfig // { domain = config.hostEnv.fqdn; }; }; users.users.acme = { uid = config.ids.uids.acme; group = "acme"; description = "Acme user"; }; users.groups.acme = { gid = config.ids.gids.acme; }; systemd.services = lib.attrsets.mapAttrs' (k: v: lib.attrsets.nameValuePair "acme-selfsigned-${k}" { wantedBy = [ "acme-selfsigned-certificates.target" ]; script = lib.mkAfter '' cp $workdir/server.crt ${config.security.acme.certs."${k}".directory}/cert.pem chown '${v.user}:${v.group}' ${config.security.acme.certs."${k}".directory}/cert.pem chmod ${if v.allowKeysForGroup then "750" else "700"} ${config.security.acme.certs."${k}".directory}/cert.pem cp $workdir/ca.crt ${config.security.acme.certs."${k}".directory}/chain.pem chown '${v.user}:${v.group}' ${config.security.acme.certs."${k}".directory}/chain.pem chmod ${if v.allowKeysForGroup then "750" else "700"} ${config.security.acme.certs."${k}".directory}/chain.pem ''; } ) config.security.acme.certs // lib.attrsets.mapAttrs' (k: data: lib.attrsets.nameValuePair "acme-${k}" { after = lib.mkAfter [ "bind.service" ]; serviceConfig = let cfg = config.security.acme; hashOptions = let domains = builtins.concatStringsSep "," ( [ data.domain ] ++ (builtins.attrNames data.extraDomains) ); certOptions = builtins.concatStringsSep "," [ (if data.ocspMustStaple then "must-staple" else "no-must-staple") ]; in builtins.hashString "sha256" (builtins.concatStringsSep ";" [ data.keyType domains certOptions ]); accountsDir = "accounts-${data.keyType}"; lpath = "acme/${k}"; apath = "/var/lib/${lpath}"; spath = "/var/lib/acme/.lego/${k}"; fileMode = if data.allowKeysForGroup then "640" else "600"; dirFileMode = if data.allowKeysForGroup then "750" else "700"; globalOpts = [ "-d" data.domain "--email" data.email "--path" "." "--key-type" data.keyType ] ++ lib.optionals (cfg.acceptTerms) [ "--accept-tos" ] ++ lib.optionals (data.dnsProvider != null && !data.dnsPropagationCheck) [ "--dns.disable-cp" ] ++ lib.concatLists (lib.mapAttrsToList (name: root: [ "-d" name ]) data.extraDomains) ++ (if data.dnsProvider != null then [ "--dns" data.dnsProvider ] else [ "--http" "--http.webroot" config.myServices.certificates.webroot ]) ++ lib.optionals (cfg.server != null || data.server != null) ["--server" (if data.server == null then cfg.server else data.server)]; certOpts = lib.optionals data.ocspMustStaple [ "--must-staple" ]; runOpts = lib.escapeShellArgs (globalOpts ++ [ "run" ] ++ certOpts); renewOpts = lib.escapeShellArgs (globalOpts ++ [ "renew" "--days" (builtins.toString cfg.validMinDays) ] ++ certOpts ++ data.extraLegoRenewFlags); forceRenewOpts = lib.escapeShellArgs (globalOpts ++ [ "renew" "--days" "999" ] ++ certOpts ++ data.extraLegoRenewFlags); keyName = builtins.replaceStrings ["*"] ["_"] data.domain; in { User = lib.mkForce "acme"; Group = lib.mkForce "acme"; WorkingDirectory = lib.mkForce spath; StateDirectory = lib.mkForce "acme/.lego/${k} acme/.lego/${accountsDir}"; ExecStartPre = let script = pkgs.writeScript "acme-prestart" '' #!${pkgs.runtimeShell} -e install -m 0755 -o acme -g acme -d ${config.myServices.certificates.webroot} ''; in lib.mkForce "+${script}"; ExecStart = lib.mkForce (pkgs.writeScript "acme-start" '' #!${pkgs.runtimeShell} -e # lego doesn't check key type after initial creation, we # need to check for him if [ -L ${spath}/accounts -o -d ${spath}/accounts ]; then if [ -L ${spath}/accounts -a "$(readlink ${spath}/accounts)" != ../${accountsDir} ]; then ln -sfn ../${accountsDir} ${spath}/accounts mv -f ${spath}/certificates/${keyName}.key ${spath}/certificates/${keyName}.key.old fi else ln -s ../${accountsDir} ${spath}/accounts fi # check if domain changed: lego doesn't check by itself 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 ${pkgs.lego}/bin/lego ${runOpts} elif [ ! -f ${spath}/currentDomains -o "$(cat ${spath}/currentDomains)" != "${hashOptions}" ]; then ${pkgs.lego}/bin/lego ${forceRenewOpts} else ${pkgs.lego}/bin/lego ${renewOpts} fi ''); ExecStartPost = let ISRG_Root_X1 = pkgs.fetchurl { url = "https://letsencrypt.org/certs/isrgrootx1.pem"; sha256 = "1la36n2f31j9s03v847ig6ny9lr875q3g7smnq33dcsmf2i5gd92"; }; fix_ISRG_Root_X1 = pkgs.writeScript "fix-pem" '' for file in chain fullchain full; do if grep -q MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA "$file.pem"; then cat ${ISRG_Root_X1} | grep -v " CERTIFICATE" | \ sed -i.bak -ne "/MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/ {r /dev/stdin" -e ":a; n; /Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5/ { b }; ba };p" $file.pem fi done ''; script = pkgs.writeScript "acme-post-start" '' #!${pkgs.runtimeShell} -e install -m 0755 -o root -g root -d /var/lib/acme install -m 0${dirFileMode} -o ${data.user} -g ${data.group} -d /var/lib/acme/${k} cd /var/lib/acme/${k} # Test that existing cert is older than new cert KEY=${spath}/certificates/${keyName}.key KEY_CHANGED=no if [ -e $KEY -a $KEY -nt key.pem ]; then KEY_CHANGED=yes cp -p ${spath}/certificates/${keyName}.key key.pem cp -p ${spath}/certificates/${keyName}.crt fullchain.pem cp -p ${spath}/certificates/${keyName}.issuer.crt chain.pem ln -sf fullchain.pem cert.pem cat key.pem fullchain.pem > full.pem echo -n "${hashOptions}" > ${spath}/currentDomains fi ${fix_ISRG_Root_X1} chmod ${fileMode} *.pem chown '${data.user}:${data.group}' *.pem if [ "$KEY_CHANGED" = "yes" ]; then : # noop in case postRun is empty ${data.postRun} fi ''; in lib.mkForce "+${script}"; }; } ) config.security.acme.certs // { httpdProd = lib.mkIf config.services.httpd.Prod.enable { after = [ "acme-selfsigned-certificates.target" ]; wants = [ "acme-selfsigned-certificates.target" ]; }; httpdTools = lib.mkIf config.services.httpd.Tools.enable { after = [ "acme-selfsigned-certificates.target" ]; wants = [ "acme-selfsigned-certificates.target" ]; }; httpdInte = lib.mkIf config.services.httpd.Inte.enable { after = [ "acme-selfsigned-certificates.target" ]; wants = [ "acme-selfsigned-certificates.target" ]; }; }; }; }