summaryrefslogtreecommitdiff
path: root/hosts/surtr/tls/default.nix
diff options
context:
space:
mode:
Diffstat (limited to 'hosts/surtr/tls/default.nix')
-rw-r--r--hosts/surtr/tls/default.nix156
1 files changed, 156 insertions, 0 deletions
diff --git a/hosts/surtr/tls/default.nix b/hosts/surtr/tls/default.nix
new file mode 100644
index 00000000..a1548feb
--- /dev/null
+++ b/hosts/surtr/tls/default.nix
@@ -0,0 +1,156 @@
1{ config, lib, customUtils, pkgs, ... }:
2
3with lib;
4
5let
6 inherit (customUtils) mapFilterAttrs;
7
8 tsigSecretName = domain: "${domain}_tsig-secret";
9
10 cfg = config.security.acme;
11 knotCfg = config.services.knot;
12
13 knotDNSCredentials = domain: let
14 zone = if cfg.domains.${domain}.zone == null then domain else cfg.domains.${domain}.zone;
15 in pkgs.writeText "lego-credentials" ''
16 EXEC_PATH=${knotDNSExec zone}/bin/update-dns.sh
17 EXEC_PROPAGATION_TIMEOUT=300
18 EXEC_POLLING_INTERVAL=5
19 '';
20 knotDNSExec = zone: pkgs.writeScriptBin "update-dns.sh" ''
21 #!${pkgs.zsh}/bin/zsh -xe
22
23 mode=$1
24 fqdn=$2
25 challenge=$3
26
27 owner=''${fqdn%".${zone}."}
28
29 commited=
30 function abort() {
31 [[ -n "''${commited}" ]] || ${knotCfg.cliWrappers}/bin/knotc zone-abort "${zone}"
32 }
33
34 ${knotCfg.cliWrappers}/bin/knotc zone-begin "${zone}"
35 trap abort EXIT
36
37 case "''${mode}" in
38 present)
39 if ${knotCfg.cliWrappers}/bin/knotc zone-get ${zone} "''${owner}" TXT; then
40 ${knotCfg.cliWrappers}/bin/knotc zone-unset ${zone} "''${owner}" TXT '""'
41 fi
42 ${knotCfg.cliWrappers}/bin/knotc zone-set ${zone} "''${owner}" 30 TXT "''${challenge}"
43 ;;
44 cleanup)
45 ${knotCfg.cliWrappers}/bin/knotc zone-unset ${zone} "''${owner}" TXT "''${challenge}"
46 ${knotCfg.cliWrappers}/bin/knotc zone-set ${zone} "''${owner}" 30 TXT '""'
47 ;;
48 *)
49 exit 2
50 ;;
51 esac
52
53 ${knotCfg.cliWrappers}/bin/knotc zone-commit "${zone}"
54 commited=yes
55 '';
56
57 domainOptions = {
58 options = {
59 wildcard = mkOption {
60 type = types.bool;
61 default = false;
62 };
63 zone = mkOption {
64 type = types.nullOr types.str;
65 default = null;
66 };
67 certCfg = mkOption {
68 type = types.attrs;
69 default = {};
70 };
71 };
72 };
73in {
74 options = {
75 security.acme = {
76 domains = mkOption {
77 type = types.attrsOf (types.submodule domainOptions);
78 default = {};
79 };
80 };
81 };
82
83 config = {
84 security.acme.domains = genAttrs ["dirty-haskell.org" "141.li" "xmpp.li" "yggdrasil.li" "praseodym.org" "rheperire.org" "kleen.li" "nights.email"] (domain: { wildcard = true; });
85
86 fileSystems."/var/lib/acme" =
87 { device = "surtr/safe/var-lib-acme";
88 fsType = "zfs";
89 };
90
91 security.acme = {
92 acceptTerms = true;
93 preliminarySelfsigned = true; # DNS challenge is slow
94 defaults = {
95 email = "phikeebaogobaegh@141.li";
96 keyType = "rsa4096"; # we don't like NIST curves
97 # extraLegoFlags = ["--preferred-chain" "ISRG Root X1"];
98 };
99 certs =
100 let
101 domainAttrset = domain: let
102 tsigPath = ./tsig_keys + "/${domain}";
103 tsigSecret = config.sops.secrets.${tsigSecretName domain};
104 isTsig = pathExists tsigPath;
105 shared = {
106 inherit domain;
107 extraDomainNames = optional cfg.domains.${domain}.wildcard "*.${domain}";
108 dnsResolver = "127.0.0.1:5353";
109 };
110 mkKnotc = shared // {
111 dnsProvider = "exec";
112 credentialsFile = knotDNSCredentials domain;
113 };
114 mkRFC2136 = let
115 tsigInfo = readYaml tsigPath;
116 in shared // {
117 dnsProvider = "rfc2136";
118 credentialsFile = pkgs.writeText "${domain}_credentials.env" ''
119 RFC2136_NAMESERVER=127.0.0.1:53
120 RFC2136_TSIG_ALGORITHM=hmac-sha256.
121 RFC2136_TSIG_KEY=${domain}_acme
122 RFC2136_TSIG_SECRET_FILE=${tsigSecret.path}
123 RFC2136_PROPAGATION_TIMEOUT=300
124 RFC2136_POLLING_INTERVAL=5
125 RFC2136_TTL=300
126 '';
127 };
128 in // cfg.domains.${domain}.certCfg;
129 in genAttrs (attrNames cfg.domains) domainAttrset;
130 };
131
132 sops.secrets = let
133 toTSIGSecret = n: v:
134 if v == "regular" || v == "symlink"
135 then nameValuePair (tsigSecretName n) {
136 format = "binary";
137 owner = if config.security.acme.useRoot then "root" else "acme";
138 group = "acme";
139 sopsFile = ./tsig_keys + "/${n}";
140 } else null;
141 in mapFilterAttrs (_: v: v != null) toTSIGSecret (readDir ./tsig_keys);
142
143 systemd.services =
144 let
145 serviceAttrset = domain: {
146 after = [ "knot.service" ];
147 bindsTo = [ "knot.service" ];
148 serviceConfig = {
149 ReadWritePaths = ["/run/knot/knot.sock"];
150 SupplementaryGroups = ["knot"];
151 RestrictAddressFamilies = ["AF_UNIX"];
152 };
153 };
154 in mapAttrs' (domain: nameValuePair "acme-${domain}") (genAttrs (attrNames config.security.acme.certs) serviceAttrset);
155 };
156}