-{ 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.duplyBackup.profiles.system.excludeFile = ''
- + ${config.security.acme.directory}
+ + ${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" ]; };
};
};
}