diff options
Diffstat (limited to 'hosts/surtr/tls/default.nix')
-rw-r--r-- | hosts/surtr/tls/default.nix | 155 |
1 files changed, 69 insertions, 86 deletions
diff --git a/hosts/surtr/tls/default.nix b/hosts/surtr/tls/default.nix index f1a515db..b1c05888 100644 --- a/hosts/surtr/tls/default.nix +++ b/hosts/surtr/tls/default.nix | |||
@@ -3,111 +3,94 @@ | |||
3 | with lib; | 3 | with lib; |
4 | 4 | ||
5 | let | 5 | let |
6 | inherit (customUtils) mapFilterAttrs; | ||
7 | |||
8 | tsigSecretName = domain: "${domain}_tsig-secret"; | 6 | tsigSecretName = domain: "${domain}_tsig-secret"; |
7 | tsigKey = domain: | ||
8 | let | ||
9 | tsigKeyPath = ./tsig_keys + "/${domain}"; | ||
10 | in assert assertMsg (pathExists tsigKeyPath) "‘${domain}’ does not exist in `tls/tsig_keys` -- is this a new ACME domain and you forgot to generate the TSIG key? If so, run `gup tls/tsig_keys/${domain}`"; tsigKeyPath; | ||
9 | 11 | ||
10 | cfg = config.security.acme; | 12 | cfg = config.security.acme; |
11 | |||
12 | domainOptions = { | ||
13 | options = { | ||
14 | wildcard = mkOption { | ||
15 | type = types.bool; | ||
16 | default = false; | ||
17 | }; | ||
18 | zone = mkOption { | ||
19 | type = types.nullOr types.str; | ||
20 | default = null; | ||
21 | }; | ||
22 | certCfg = mkOption { | ||
23 | type = types.attrs; | ||
24 | default = {}; | ||
25 | }; | ||
26 | }; | ||
27 | }; | ||
28 | in { | 13 | in { |
29 | options = { | 14 | options = { |
30 | security.acme = { | 15 | security.acme = { |
31 | domains = mkOption { | 16 | # This file introduces an additional nixos module option |
32 | type = types.attrsOf (types.submodule domainOptions); | 17 | # `security.acme.rfc2136Domains`. |
18 | # The new option is an attrset of domain names mapping to | ||
19 | # additional settings. | ||
20 | rfc2136Domains = mkOption { | ||
21 | type = types.attrsOf (types.submodule { | ||
22 | options = { | ||
23 | wildcard = mkOption { | ||
24 | type = types.bool; | ||
25 | default = false; | ||
26 | }; | ||
27 | restartUnits = mkOption { | ||
28 | type = types.listOf types.str; | ||
29 | default = []; | ||
30 | }; | ||
31 | }; | ||
32 | }); | ||
33 | default = {}; | 33 | default = {}; |
34 | }; | 34 | }; |
35 | }; | 35 | }; |
36 | }; | 36 | }; |
37 | 37 | ||
38 | config = { | 38 | config = { |
39 | security.acme.domains = genAttrs ["dirty-haskell.org" "141.li" "xmpp.li" "synapse.li" "yggdrasil.li" "praseodym.org" "rheperire.org" "kleen.li" "nights.email" "bouncy.email" "kleen.consulting"] (domain: { wildcard = true; }); | ||
40 | |||
41 | fileSystems."/var/lib/acme" = | ||
42 | { device = "surtr/safe/var-lib-acme"; | ||
43 | fsType = "zfs"; | ||
44 | }; | ||
45 | |||
46 | security.acme = { | 39 | security.acme = { |
40 | # Some default/global ACME settings | ||
41 | |||
47 | acceptTerms = true; | 42 | acceptTerms = true; |
48 | preliminarySelfsigned = true; # DNS challenge is slow | 43 | # DNS challenge is slow |
44 | preliminarySelfsigned = true; | ||
49 | defaults = { | 45 | defaults = { |
50 | email = "phikeebaogobaegh@141.li"; | 46 | email = "phikeebaogobaegh@141.li"; |
51 | keyType = "rsa4096"; # we don't like NIST curves | 47 | # We don't like NIST curves and Let's Encrypt doesn't support |
52 | extraLegoRenewFlags = [ | 48 | # anything better |
53 | # "--preferred-chain" "ISRG Root X1" | 49 | keyType = "rsa4096"; |
54 | # "--always-deactivate-authorizations" "true" | ||
55 | ]; | ||
56 | extraLegoRunFlags = config.security.acme.defaults.extraLegoRenewFlags; | ||
57 | }; | 50 | }; |
58 | certs = | ||
59 | let | ||
60 | domainAttrset = domain: let | ||
61 | tsigPath = ./tsig_keys + "/${domain}"; | ||
62 | isTsig = pathExists tsigPath; | ||
63 | shared = { | ||
64 | inherit domain; | ||
65 | extraDomainNames = optional cfg.domains.${domain}.wildcard "*.${domain}"; | ||
66 | dnsResolver = "127.0.0.1:5353"; | ||
67 | }; | ||
68 | mkRFC2136 = shared // rec { | ||
69 | dnsProvider = "rfc2136"; | ||
70 | credentialsFile = pkgs.writeText "${domain}_credentials.env" '' | ||
71 | RFC2136_NAMESERVER=127.0.0.1:53 | ||
72 | RFC2136_TSIG_ALGORITHM=hmac-sha256. | ||
73 | RFC2136_TSIG_KEY=${domain}_acme_key | ||
74 | RFC2136_TSIG_SECRET_FILE=/run/credentials/acme-${domain}.service/tsig_secret | ||
75 | RFC2136_TTL=0 | ||
76 | RFC2136_PROPAGATION_TIMEOUT=60 | ||
77 | RFC2136_POLLING_INTERVAL=2 | ||
78 | RFC2136_SEQUENCE_INTERVAL=1 | ||
79 | ''; | ||
80 | dnsPropagationCheck = false; | ||
81 | }; | ||
82 | in assert isTsig; mkRFC2136 // cfg.domains.${domain}.certCfg; | ||
83 | in genAttrs (attrNames cfg.domains) domainAttrset; | ||
84 | }; | ||
85 | 51 | ||
86 | sops.secrets = let | 52 | # For each domain specified in |
87 | toTSIGSecret = n: v: | 53 | # `config.security.acme.rfc2136Domains`, configure an additional |
88 | if v == "regular" || v == "symlink" | 54 | # entry in `config.security.acme.certs` containing appropriate |
89 | then nameValuePair (tsigSecretName n) { | 55 | # settings to provision the certificate via DNS-01 |
90 | format = "binary"; | 56 | certs = mapAttrs (domain: domainCfg: { |
91 | sopsFile = ./tsig_keys + "/${n}"; | 57 | inherit domain; |
92 | } else null; | 58 | extraDomainNames = optional domainCfg.wildcard "*.${domain}"; |
93 | in mapFilterAttrs (_: v: v != null) toTSIGSecret (builtins.readDir ./tsig_keys); | 59 | dnsResolver = "127.0.0.1:53"; |
60 | dnsProvider = "rfc2136"; | ||
61 | credentialsFile = pkgs.writeText "${domain}_credentials.env" '' | ||
62 | RFC2136_NAMESERVER=127.0.0.1:53 | ||
63 | RFC2136_TSIG_ALGORITHM=hmac-sha256. | ||
64 | RFC2136_TSIG_KEY=${domain}_acme_key | ||
65 | RFC2136_TSIG_SECRET_FILE=/run/credentials/acme-${domain}.service/${tsigSecretName domain} | ||
66 | RFC2136_TTL=0 | ||
67 | RFC2136_PROPAGATION_TIMEOUT=60 | ||
68 | RFC2136_POLLING_INTERVAL=2 | ||
69 | RFC2136_SEQUENCE_INTERVAL=1 | ||
70 | ''; | ||
71 | dnsPropagationCheck = false; | ||
72 | postRun = mkIf (domainCfg.restartUnits != []) '' | ||
73 | systemctl --no-block try-restart ${escapeShellArgs domainCfg.restartUnits} | ||
74 | ''; | ||
75 | }) cfg.rfc2136Domains; | ||
76 | }; | ||
94 | 77 | ||
95 | systemd.services = | 78 | # Decrypt all `tsig_keys/*` at runtime |
96 | let | 79 | sops.secrets = mapAttrs' (domain: domainCfg: nameValuePair (tsigSecretName domain) { |
97 | serviceAttrset = domain: { | 80 | format = "binary"; |
98 | after = [ "knot.service" ]; | 81 | sopsFile = tsigKey domain; |
99 | bindsTo = [ "knot.service" ]; | 82 | restartUnits = [ "acme-${domain}.service" ]; |
100 | serviceConfig = { | 83 | }) cfg.rfc2136Domains; |
101 | LoadCredential = ["tsig_secret:${config.sops.secrets.${tsigSecretName domain}.path}"]; | ||
102 | SystemCallFilter = mkForce [ "@system-service" "~@privileged" "@chown" ]; | ||
103 | }; | ||
104 | }; | ||
105 | in mapAttrs' (domain: nameValuePair "acme-${domain}") (genAttrs (attrNames config.security.acme.certs) serviceAttrset); | ||
106 | 84 | ||
107 | services.certspotter = { | 85 | # Provide appropriate `tsig_key/*` to systemd service performing |
108 | extraOptions = [ "-verbose" "-num_workers" "4" "-batch_size" "2000" ]; | 86 | # certificate provisioning |
109 | watchList = map (domain: ".${domain}") (attrNames cfg.domains); | 87 | systemd.services = mapAttrs' (domain: domainCfg: nameValuePair "acme-${domain}" { |
110 | logs = "https://www.gstatic.com/ct/log_list/v2/all_logs_list.json"; | 88 | after = [ "knot.service" ]; |
111 | }; | 89 | bindsTo = [ "knot.service" ]; |
90 | serviceConfig = { | ||
91 | LoadCredential = [ "${tsigSecretName domain}:${config.sops.secrets.${tsigSecretName domain}.path}" ]; | ||
92 | SystemCallFilter = mkForce [ "@system-service" "~@privileged" "@chown" ]; | ||
93 | }; | ||
94 | }) cfg.rfc2136Domains; | ||
112 | }; | 95 | }; |
113 | } | 96 | } |