From cfda3cfc35445979225850f686f338e6d4ace372 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Isma=C3=ABl=20Bouya?= Date: Sat, 23 May 2020 03:35:03 +0200 Subject: [PATCH] Fix acme certificate generation --- modules/myids.nix | 2 + modules/private/certificates.nix | 151 ++++++++++++++----- modules/private/monitoring/status.nix | 2 +- modules/private/system/dilion.nix | 2 +- modules/private/websites/papa/maison_bbc.nix | 4 +- 5 files changed, 116 insertions(+), 45 deletions(-) diff --git a/modules/myids.nix b/modules/myids.nix index 79610af..1a1a5d6 100644 --- a/modules/myids.nix +++ b/modules/myids.nix @@ -3,6 +3,7 @@ # Check that there is no clash with nixos/modules/misc/ids.nix config = { ids.uids = { + acme = 388; backup = 389; vhost = 390; openarc = 391; @@ -16,6 +17,7 @@ }; ids.gids = { nagios = 11; # commented in the ids file + acme = 388; backup = 389; vhost = 390; openarc = 391; 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 // { diff --git a/modules/private/monitoring/status.nix b/modules/private/monitoring/status.nix index e0bc0e1..4f5f4bb 100644 --- a/modules/private/monitoring/status.nix +++ b/modules/private/monitoring/status.nix @@ -29,7 +29,7 @@ recommendedGzipSettings = true; recommendedProxySettings = true; virtualHosts."status.immae.eu" = { - acmeRoot = config.security.acme.certs."${name}".webroot; + acmeRoot = config.myServices.certificates.webroot; useACMEHost = name; forceSSL = true; locations."/".proxyPass = "http://unix:/run/naemon-status/socket.sock:/"; diff --git a/modules/private/system/dilion.nix b/modules/private/system/dilion.nix index b9e83f3..4860d07 100644 --- a/modules/private/system/dilion.nix +++ b/modules/private/system/dilion.nix @@ -1,5 +1,5 @@ { privateFiles }: -{ config, pkgs, ... }: +{ config, pkgs, name, ... }: { boot.kernelPackages = pkgs.linuxPackages_latest; myEnv = import "${privateFiles}/environment.nix" // { inherit privateFiles; }; diff --git a/modules/private/websites/papa/maison_bbc.nix b/modules/private/websites/papa/maison_bbc.nix index 5fbc62f..aa6d866 100644 --- a/modules/private/websites/papa/maison_bbc.nix +++ b/modules/private/websites/papa/maison_bbc.nix @@ -35,7 +35,7 @@ in { root = varDir; extraConfig = [ '' - Alias /.well-known/acme-challenge ${config.security.acme.certs.papa.webroot}/.well-known/acme-challenge + Alias /.well-known/acme-challenge ${config.myServices.certificates.webroot}/.well-known/acme-challenge RedirectMatch 301 ^/((?!(\.well-known|add.php).*$).*)$ https://maison.bbc.bouya.org/$1 DirectoryIndex index.php index.htm index.html @@ -45,7 +45,7 @@ in { SetHandler "proxy:unix:${config.services.phpfpm.pools.papa_maison_bbc.socket}|fcgi://localhost" - + Options Indexes FollowSymLinks AllowOverride None Require all granted -- 2.41.0