main
1{den, ...}: {
2 den.aspects.services.caddy = {
3 includes = [
4 den.aspects.services.caddy.reverse-proxy-collector
5 ];
6
7 persist = {
8 directories = [
9 {
10 directory = "/var/lib/caddy";
11 user = "caddy";
12 group = "caddy";
13 }
14 ];
15 };
16
17 nixos = {
18 services.caddy = {
19 enable = true;
20 enableReload = true;
21
22 globalConfig = ''
23 http_port 80
24 https_port 443
25 '';
26 };
27
28 networking.firewall.allowedTCPPorts = [80 443];
29 };
30 };
31
32 den.aspects.services.caddy.reverse-proxy-collector = {host, ...}: {
33 nixos = {
34 reverseProxy,
35 config,
36 lib,
37 ...
38 }: let
39 inherit
40 (import ./_lib.nix {inherit lib;})
41 fmtAddr
42 mkEntry
43 entryBlock
44 fallbackBlock
45 templateName
46 ;
47
48 # Resolve how Caddy on the current host should reach the host that
49 # submitted the reverse proxy (the source host).
50 #
51 # Priority:
52 # 1. Same host -> loopback.
53 # 2. Source has a clear text -> use it directly.
54 # 3. Source has a secret IP -> import it from a vaultix template.
55 # 4. Otherwise -> abort with a helpful message.
56 resolveAddress = srcHost: let
57 src = srcHost.address;
58
59 v4ClearText = src.ipv4.clearText != null;
60 v6ClearText = src.ipv6.clearText != null;
61
62 v4Secret = src.ipv4.secret.name != null && src.ipv4.secret.file != null;
63 v6Secret = src.ipv6.secret.name != null && src.ipv6.secret.file != null;
64 in
65 if srcHost.name == host.name
66 then {
67 kind = "inline";
68 address = "127.0.0.1";
69 }
70 else if v4ClearText
71 then {
72 kind = "inline";
73 address = fmtAddr false src.ipv4.clearText;
74 }
75 else if v6ClearText
76 then {
77 kind = "inline";
78 address = fmtAddr true src.ipv6.clearText;
79 }
80 else if v4Secret
81 then {
82 kind = "import";
83 isV6 = false;
84 secretName = src.ipv4.secret.name;
85 secretFile = src.ipv4.secret.file;
86 }
87 else if v6Secret
88 then {
89 kind = "import";
90 isV6 = true;
91 secretName = src.ipv6.secret.name;
92 secretFile = src.ipv6.secret.file;
93 }
94 else
95 abort ''
96 Caddy on host '${host.name}' cannot reverse proxy to host '${srcHost.name}': no usable address is configured.
97
98 Please configure an address for host '${srcHost.name}' (see `address` in modules/hosts/schema.nix), one of:
99 - clear text IPv4/IPv6 (address.ipv4.clearText / address.ipv6.clearText)
100 - secret IPv4/IPv6 (address.ipv4.secret / address.ipv6.secret)
101 '';
102
103 entries =
104 lib.concatMap (
105 r:
106 lib.mapAttrsToList (mkEntry (resolveAddress r.source.host)) (
107 lib.filterAttrs (_: conf: !(conf.tailscale or false)) r.value
108 )
109 )
110 (lib.filter (r: r.source.host.name == host.name) reverseProxy);
111
112 importEntries = lib.filter (e: e.resolution.kind == "import") entries;
113
114 virtualHosts = let
115 grouped = lib.groupBy (e: e.domain) entries;
116
117 mkVirtualHost = name: group: let
118 noPath = lib.filter (e: e.path == null) group;
119 withPath = lib.filter (e: e.path != null) group;
120 noPathCount = builtins.length noPath;
121 in
122 assert lib.assertMsg (noPathCount <= 1)
123 "Multiple entries without a path for domain '${name}': only one root (pathless) entry per domain is allowed, but found ${toString noPathCount}."; let
124 pathLines = lib.concatMap (entryBlock config) withPath;
125 fallbackLines =
126 if noPathCount == 1
127 then [(fallbackBlock config (builtins.head noPath))]
128 else [];
129 blocks = pathLines ++ fallbackLines;
130 in {
131 inherit name;
132 value.extraConfig = lib.concatLines (["encode zstd gzip"] ++ blocks);
133 };
134 in
135 lib.listToAttrs (lib.mapAttrsToList mkVirtualHost grouped);
136
137 reverseProxyTemplates = lib.listToAttrs (map (e: {
138 name = templateName e.domain;
139 value = {
140 content = ''
141 reverse_proxy http://${fmtAddr e.resolution.isV6 config.vaultix.placeholder.${e.resolution.secretName}}:${toString e.port}
142 '';
143 owner = config.services.caddy.user;
144 group = config.services.caddy.group;
145 mode = "0400";
146 };
147 })
148 importEntries);
149
150 reverseProxySecrets = lib.listToAttrs (map (e: {
151 name = e.resolution.secretName;
152 value.file = e.resolution.secretFile;
153 })
154 importEntries);
155 in {
156 services.caddy.virtualHosts = virtualHosts;
157 vaultix.secrets = reverseProxySecrets;
158 vaultix.templates = reverseProxyTemplates;
159 };
160 };
161}