X-Git-Url: https://git.immae.eu/?p=perso%2FImmae%2FConfig%2FNix.git;a=blobdiff_plain;f=modules%2Fprivate%2Fcertificates.nix;h=c568783b622c17ecb32ca43257070e6daaa3d50e;hp=bbe4c3bbf1c093510aed37622d30aa149ab3729b;hb=cfda3cfc35445979225850f686f338e6d4ace372;hpb=db343436f0e678ef3a97e6f8ac559ffa0507e422 diff --git a/modules/private/certificates.nix b/modules/private/certificates.nix index bbe4c3b..c568783 100644 --- a/modules/private/certificates.nix +++ b/modules/private/certificates.nix @@ -2,9 +2,13 @@ { options.myServices.certificates = { enable = lib.mkEnableOption "enable certificates"; + webroot = lib.mkOption { + readOnly = true; + default = "/var/lib/acme/acme-challenges"; + }; certConfig = lib.mkOption { default = { - webroot = "/var/lib/acme/acme-challenges"; + 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") @@ -13,6 +17,7 @@ (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"; }; @@ -20,13 +25,13 @@ config = lib.mkIf config.myServices.certificates.enable { services.duplyBackup.profiles.system.excludeFile = '' - + /var/lib/acme/acme-challenges + + ${config.myServices.certificates.webroot} ''; services.nginx = { recommendedTlsSettings = true; virtualHosts = { "${config.hostEnv.fqdn}" = { - acmeRoot = config.security.acme.certs."${name}".webroot; + acmeRoot = config.myServices.certificates.webroot; useACMEHost = name; forceSSL = true; }; @@ -45,6 +50,15 @@ }; }; + 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" ]; @@ -62,50 +76,105 @@ lib.attrsets.mapAttrs' (k: data: lib.attrsets.nameValuePair "acme-${k}" { after = lib.mkAfter [ "bind.service" ]; - serviceConfig.ExecStartPre = - let - script = pkgs.writeScript "acme-pre-start" '' - #!${pkgs.runtimeShell} -e - mkdir -p '${data.webroot}/.well-known/acme-challenge' - chmod a+w '${data.webroot}/.well-known/acme-challenge' - #doesn't work for multiple concurrent runs - #chown -R '${data.user}:${data.group}' '${data.webroot}/.well-known/acme-challenge' - ''; - in - "+${script}"; - # This is a workaround to - # https://github.com/NixOS/nixpkgs/issues/84409 - # https://github.com/NixOS/nixpkgs/issues/84633 - serviceConfig.RemainAfterExit = lib.mkForce false; - serviceConfig.WorkingDirectory = lib.mkForce "/var/lib/acme/${k}/.lego"; - serviceConfig.StateDirectory = lib.mkForce "acme/${k}/.lego acme/${k} acme/.lego/${k} acme/.lego/accounts"; - serviceConfig.ExecStartPost = + serviceConfig = let - keyName = builtins.replaceStrings ["*"] ["_"] data.domain; + 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"; - spath = "/var/lib/acme/${k}/.lego"; - script = pkgs.writeScript "acme-post-start" '' + 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 - cd /var/lib/acme/${k} - - # Test that existing cert is older than new cert - KEY=${spath}/certificates/${keyName}.key - if [ -e $KEY -a $KEY -nt key.pem ]; then - 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 - - ${data.postRun} + # 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 + 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 - chmod ${fileMode} *.pem - chown '${data.user}:${data.group}' *.pem - ''; - in - lib.mkForce "+${script}"; + 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 // {