X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=modules%2Fprivate%2Fcertificates.nix;h=c568783b622c17ecb32ca43257070e6daaa3d50e;hb=cfda3cfc35445979225850f686f338e6d4ace372;hp=9de3e6d7b1c82cdae432f6e72a7f6b05b655dad7;hpb=8415083eb6acc343dfa404dbbc12fa0171a48a20;p=perso%2FImmae%2FConfig%2FNix.git diff --git a/modules/private/certificates.nix b/modules/private/certificates.nix index 9de3e6d..c568783 100644 --- a/modules/private/certificates.nix +++ b/modules/private/certificates.nix @@ -1,56 +1,189 @@ -{ lib, pkgs, config, ... }: +{ 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 = "${config.security.acme.directory}/acme-challenge"; + webroot = lib.mkForce null; # avoids creation of tmpfiles email = "ismael@bouya.org"; - postRun = '' - systemctl reload httpdTools.service httpdInte.service httpdProd.service - ''; - plugins = [ "cert.pem" "chain.pem" "fullchain.pem" "full.pem" "key.pem" "account_key.json" ]; + 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.backup.profiles.system.excludeFile = '' - + ${config.security.acme.directory} + services.duplyBackup.profiles.system.excludeFile = '' + + ${config.myServices.certificates.webroot} ''; + 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 = { - "eldiron" = config.myServices.certificates.certConfig // { - domain = "eldiron.immae.eu"; + "${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}" (lib.mkBefore { script = - (lib.optionalString (builtins.elem "cert.pem" v.plugins) '' - cp $workdir/server.crt ${config.security.acme.directory}/${k}/cert.pem - chown '${v.user}:${v.group}' ${config.security.acme.directory}/${k}/cert.pem - chmod ${if v.allowKeysForGroup then "750" else "700"} ${config.security.acme.directory}/${k}/cert.pem - '') + - (lib.optionalString (builtins.elem "chain.pem" v.plugins) '' - cp $workdir/ca.crt ${config.security.acme.directory}/${k}/chain.pem - chown '${v.user}:${v.group}' ${config.security.acme.directory}/${k}/chain.pem - chmod ${if v.allowKeysForGroup then "750" else "700"} ${config.security.acme.directory}/${k}/chain.pem - '') - ; }) - ) config.security.acme.certs // { - httpdProd.after = [ "acme-selfsigned-certificates.target" ]; - httpdProd.wants = [ "acme-selfsigned-certificates.target" ]; - httpdTools.after = [ "acme-selfsigned-certificates.target" ]; - httpdTools.wants = [ "acme-selfsigned-certificates.target" ]; - httpdInte.after = [ "acme-selfsigned-certificates.target" ]; - httpdInte.wants = [ "acme-selfsigned-certificates.target" ]; + 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 + 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 + + 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" ]; }; }; }; }