+++ /dev/null
-{ 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" ]; };
- };
- };
-}