aboutsummaryrefslogblamecommitdiff
path: root/modules/private/certificates.nix
blob: 9879946d07b0da37ce600d506a09f78ac382bd4e (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                                 
 

                                                      



                                                

                               
                                                                 
                                   





                                                                                                       
                                                
                                                                                      




                                                             
                                                           

                                    

                                    
                                                            



                             
      


                                                                          
 
                                     
                                               
 
                           
                                                                
                                     

        
 








                                 
                                                    





                                                                                                                       
 





                                                                                                                        

                                              
                                               
                       
             













                                                                                                                
                                                                       





























                                                                                                                                                          
                                       
















                                                                                                                                                                                                     
                


                           




                                                                                  





                                                                                                                                                                                                             
                   

















                                                                                                   
 
                                     

                                                          
 







                                                     


                                   





                                                                                                                  
      

    
{ 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" ]; };
    };
  };
}