diff --git a/lib/fudo/acme-certs.nix b/lib/fudo/acme-certs.nix index 3bc0c0a..f62f356 100644 --- a/lib/fudo/acme-certs.nix +++ b/lib/fudo/acme-certs.nix @@ -1,109 +1,108 @@ -{ config, lib, pkgs, ... } @ toplevel: +{ config, lib, pkgs, ... }@toplevel: with lib; let hostname = config.instance.hostname; - domainOpts = { name, ... }: let - domain = name; - in { - options = with types; { - admin-email = mkOption { - type = str; - description = "Domain administrator email."; - default = "admin@${domain}"; - }; - - extra-domains = mkOption { - type = listOf str; - description = "List of domains to add to this certificate."; - default = []; - }; - - local-copies = let - localCopyOpts = { name, ... }: let - copy = name; - in { - options = with types; let - target-path = "/run/ssl-certificates/${domain}/${copy}"; - in { - user = mkOption { - type = str; - description = "User to which this copy belongs."; - }; - - group = mkOption { - type = nullOr str; - description = "Group to which this copy belongs."; - default = null; - }; - - service = mkOption { - type = str; - description = "systemd job to copy certs."; - default = "fudo-acme-${domain}-${copy}-certs.service"; - }; - - certificate = mkOption { - type = str; - description = "Full path to the local copy certificate."; - default = "${target-path}/cert.pem"; - }; - - full-certificate = mkOption { - type = str; - description = "Full path to the local copy certificate."; - default = "${target-path}/fullchain.pem"; - }; - - chain = mkOption { - type = str; - description = "Full path to the local copy certificate."; - default = "${target-path}/chain.pem"; - }; - - private-key = mkOption { - type = str; - description = "Full path to the local copy certificate."; - default = "${target-path}/key.pem"; - }; - - dependent-services = mkOption { - type = listOf str; - description = "List of systemd services depending on this copy."; - default = [ ]; - }; - - part-of = mkOption { - type = listOf str; - description = "List of systemd targets to which this copy belongs."; - default = [ ]; - }; - }; + domainOpts = { name, ... }: + let domain = name; + in { + options = with types; { + admin-email = mkOption { + type = str; + description = "Domain administrator email."; + default = "admin@${domain}"; + }; + + extra-domains = mkOption { + type = listOf str; + description = "List of domains to add to this certificate."; + default = [ ]; + }; + + local-copies = let + localCopyOpts = { name, ... }: + let copy = name; + in { + options = with types; + let target-path = "/run/ssl-certificates/${domain}/${copy}"; + in { + user = mkOption { + type = str; + description = "User to which this copy belongs."; + }; + + group = mkOption { + type = nullOr str; + description = "Group to which this copy belongs."; + default = null; + }; + + service = mkOption { + type = str; + description = "systemd job to copy certs."; + default = "fudo-acme-${domain}-${copy}-certs.service"; + }; + + certificate = mkOption { + type = str; + description = "Full path to the local copy certificate."; + default = "${target-path}/cert.pem"; + }; + + full-certificate = mkOption { + type = str; + description = "Full path to the local copy certificate."; + default = "${target-path}/fullchain.pem"; + }; + + chain = mkOption { + type = str; + description = "Full path to the local copy certificate."; + default = "${target-path}/chain.pem"; + }; + + private-key = mkOption { + type = str; + description = "Full path to the local copy certificate."; + default = "${target-path}/key.pem"; + }; + + dependent-services = mkOption { + type = listOf str; + description = + "List of systemd services depending on this copy."; + default = [ ]; + }; + + part-of = mkOption { + type = listOf str; + description = + "List of systemd targets to which this copy belongs."; + default = [ ]; + }; + }; + }; + in mkOption { + type = attrsOf (submodule localCopyOpts); + description = "Map of copies to make for use by services."; + default = { }; }; - in mkOption { - type = attrsOf (submodule localCopyOpts); - description = "Map of copies to make for use by services."; - default = {}; }; }; - }; - head-or-null = lst: if (lst == []) then null else head lst; + head-or-null = lst: if (lst == [ ]) then null else head lst; rm-service-ext = filename: - head-or-null (builtins.match "^(.+)\.service$" filename); + head-or-null (builtins.match "^(.+).service$" filename); - concatMapAttrs = f: attrs: - foldr (a: b: a // b) {} (mapAttrsToList f attrs); + concatMapAttrs = f: attrs: foldr (a: b: a // b) { } (mapAttrsToList f attrs); cfg = config.fudo.acme; hasLocalDomains = hasAttr hostname cfg.host-domains; - localDomains = if hasLocalDomains then - cfg.host-domains.${hostname} else {}; + localDomains = if hasLocalDomains then cfg.host-domains.${hostname} else { }; + + optionalStringOr = str: default: if (str != null) then str else default; - optionalStringOr = str: default: - if (str != null) then str else default; - in { options.fudo.acme = with types; { host-domains = mkOption { @@ -121,22 +120,22 @@ in { # default = "/run/acme-challenge"; }; }; - + config = { - security.acme.certs = mapAttrs (domain: domainOpts: { - # email = domainOpts.admin-email; - # webroot = cfg.challenge-path; - # group = "nginx"; - # extraDomainNames = domainOpts.extra-domains; - }) localDomains; + security.acme.certs = mapAttrs (domain: domainOpts: + { + # email = domainOpts.admin-email; + # webroot = cfg.challenge-path; + # group = "nginx"; + # extraDomainNames = domainOpts.extra-domains; + }) localDomains; # Assume that if we're acquiring SSL certs, we have a real IP for the # host. nginx must have an acme dir for security.acme to work. services.nginx = mkIf hasLocalDomains { enable = true; recommendedTlsSettings = true; - virtualHosts = let - server-path = "/.well-known/acme-challenge"; + virtualHosts = let server-path = "/.well-known/acme-challenge"; in (mapAttrs (domain: domainOpts: { # THIS IS A HACK. Getting redundant paths. So if {domain} is configured # somewhere else, assume ACME is already set. @@ -152,7 +151,7 @@ in { serverName = "_"; default = true; locations = { - ${server-path} = { + "${server-path}" = { root = cfg.challenge-path; extraConfig = "auth_basic off;"; }; @@ -163,80 +162,86 @@ in { }; networking.firewall.allowedTCPPorts = [ 80 443 ]; - + systemd = { tmpfiles = mkIf hasLocalDomains { rules = let - copies = concatMapAttrs (domain: domainOpts: - domainOpts.local-copies) localDomains; + copies = concatMapAttrs (domain: domainOpts: domainOpts.local-copies) + localDomains; perms = copyOpts: if (copyOpts.group != null) then "0550" else "0500"; copy-paths = mapAttrsToList (copy: copyOpts: let - dir-entry = copyOpts: file: "d \"${dirOf file}\" ${perms copyOpts} ${copyOpts.user} ${optionalStringOr copyOpts.group "-"} - -"; + dir-entry = copyOpts: file: + '' + d "${dirOf file}" ${perms copyOpts} ${copyOpts.user} ${ + optionalStringOr copyOpts.group "-" + } - -''; in map (dir-entry copyOpts) [ copyOpts.certificate copyOpts.full-certificate copyOpts.chain copyOpts.private-key ]) copies; - in (unique (concatMap (i: unique i) copy-paths)) ++ [ - "d \"${cfg.challenge-path}\" 755 acme nginx - -" - ]; + in (unique (concatMap (i: unique i) copy-paths)) + ++ [ ''d "${cfg.challenge-path}" 755 acme nginx - -'' ]; }; services = concatMapAttrs (domain: domainOpts: - concatMapAttrs (copy: copyOpts: let - key-perms = copyOpts: if (copyOpts.group != null) then "0440" else "0400"; - source = config.security.acme.certs.${domain}.directory; - target = copyOpts.path; - owners = - if (copyOpts.group != null) then + concatMapAttrs (copy: copyOpts: + let + key-perms = copyOpts: + if (copyOpts.group != null) then "0440" else "0400"; + source = config.security.acme.certs.${domain}.directory; + target = copyOpts.path; + owners = if (copyOpts.group != null) then "${copyOpts.user}:${copyOpts.group}" - else copyOpts.user; - dirs = unique [ - (dirOf copyOpts.certificate) - (dirOf copyOpts.full-certificate) - (dirOf copyOpts.chain) - (dirOf copyOpts.private-key) - ]; - install-certs = pkgs.writeShellScript "fudo-install-${domain}-${copy}-certs.sh" '' - ${concatStringsSep "\n" (map (dir: '' - mkdir -p ${dir} - chown ${owners} ${dir} - '') dirs)} - cp ${source}/cert.pem ${copyOpts.certificate} - chmod 0444 ${copyOpts.certificate} - chown ${owners} ${copyOpts.certificate} + else + copyOpts.user; + dirs = unique [ + (dirOf copyOpts.certificate) + (dirOf copyOpts.full-certificate) + (dirOf copyOpts.chain) + (dirOf copyOpts.private-key) + ]; + install-certs = + pkgs.writeShellScript "fudo-install-${domain}-${copy}-certs.sh" '' + ${concatStringsSep "\n" (map (dir: '' + mkdir -p ${dir} + chown ${owners} ${dir} + '') dirs)} + cp ${source}/cert.pem ${copyOpts.certificate} + chmod 0444 ${copyOpts.certificate} + chown ${owners} ${copyOpts.certificate} - cp ${source}/full.pem ${copyOpts.full-certificate} - chmod 0444 ${copyOpts.full-certificate} - chown ${owners} ${copyOpts.full-certificate} + cp ${source}/full.pem ${copyOpts.full-certificate} + chmod 0444 ${copyOpts.full-certificate} + chown ${owners} ${copyOpts.full-certificate} - cp ${source}/chain.pem ${copyOpts.chain} - chmod 0444 ${copyOpts.chain} - chown ${owners} ${copyOpts.chain} + cp ${source}/chain.pem ${copyOpts.chain} + chmod 0444 ${copyOpts.chain} + chown ${owners} ${copyOpts.chain} - cp ${source}/key.pem ${copyOpts.private-key} - chmod ${key-perms copyOpts} ${copyOpts.private-key} - chown ${owners} ${copyOpts.private-key} - ''; + cp ${source}/key.pem ${copyOpts.private-key} + chmod ${key-perms copyOpts} ${copyOpts.private-key} + chown ${owners} ${copyOpts.private-key} + ''; - service-name = rm-service-ext copyOpts.service; - in { - ${service-name} = { - description = "Copy ${domain} ACME certs for ${copy}."; - after = [ "acme-${domain}.service" ]; - before = copyOpts.dependent-services; - wantedBy = [ "multi-user.target" ] ++ copyOpts.dependent-services; - partOf = copyOpts.part-of; - serviceConfig = { - Type = "simple"; - ExecStart = install-certs; - RemainAfterExit = true; - StandardOutput = "journal"; + service-name = rm-service-ext copyOpts.service; + in { + ${service-name} = { + description = "Copy ${domain} ACME certs for ${copy}."; + after = [ "acme-${domain}.service" ]; + before = copyOpts.dependent-services; + wantedBy = [ "multi-user.target" ] ++ copyOpts.dependent-services; + partOf = copyOpts.part-of; + serviceConfig = { + Type = "simple"; + ExecStart = install-certs; + RemainAfterExit = true; + StandardOutput = "journal"; + }; }; - }; - }) domainOpts.local-copies) localDomains; + }) domainOpts.local-copies) localDomains; }; }; }