Commit b21dfd1
Changed files (1)
modules
services
webserver
modules/services/webserver/caddy.nix
@@ -35,34 +35,150 @@
};
den.aspects.services.webserver.caddy.reverse-proxy-collector = {host, ...}: {
- nixos = {reverseProxy, ...}: let
- _processedRP = lib.mergeAttrsList (map (
+ nixos = {
+ reverseProxy,
+ config,
+ ...
+ }: let
+ # Wrap IPv6 addresses in brackets so they can be used in a URL authority.
+ fmtAddr = isV6: addr:
+ if isV6
+ then "[${addr}]"
+ else addr;
+
+ # Resolve how Caddy on the current host should reach the host that
+ # submitted the reverse proxy (the source host).
+ #
+ # Priority:
+ # 1. Same host -> loopback.
+ # 2. Both ends have Tailscale -> use the source's Tailscale IP.
+ # 3. Source has a clear text -> use it directly.
+ # 4. Source has a secret IP -> import it from a vaultix template.
+ # 5. Otherwise -> abort with a helpful message.
+ resolveAddress = srcHost: let
+ src = srcHost.address;
+ cur = host.address;
+
+ v4TailscaleBoth = src.ipv4.tailscale != null && cur.ipv4.tailscale != null;
+ v6TailscaleBoth = src.ipv6.tailscale != null && cur.ipv6.tailscale != null;
+
+ v4ClearText = src.ipv4.clearText != null;
+ v6ClearText = src.ipv6.clearText != null;
+
+ v4Secret = src.ipv4.secret.name != null && src.ipv4.secret.file != null;
+ v6Secret = src.ipv6.secret.name != null && src.ipv6.secret.file != null;
+ in
+ if srcHost.name == host.name
+ then {
+ kind = "inline";
+ address = "127.0.0.1";
+ }
+ else if v4TailscaleBoth
+ then {
+ kind = "inline";
+ address = fmtAddr false src.ipv4.tailscale;
+ }
+ else if v6TailscaleBoth
+ then {
+ kind = "inline";
+ address = fmtAddr true src.ipv6.tailscale;
+ }
+ else if v4ClearText
+ then {
+ kind = "inline";
+ address = fmtAddr false src.ipv4.clearText;
+ }
+ else if v6ClearText
+ then {
+ kind = "inline";
+ address = fmtAddr true src.ipv6.clearText;
+ }
+ else if v4Secret
+ then {
+ kind = "import";
+ isV6 = false;
+ secretName = src.ipv4.secret.name;
+ secretFile = src.ipv4.secret.file;
+ }
+ else if v6Secret
+ then {
+ kind = "import";
+ isV6 = true;
+ secretName = src.ipv6.secret.name;
+ secretFile = src.ipv6.secret.file;
+ }
+ else
+ abort ''
+ Caddy on host '${host.name}' cannot reverse proxy to host '${srcHost.name}': no usable address is configured.
+
+ Please configure an address for host '${srcHost.name}' (see `address` in modules/hosts/schema.nix), one of:
+ - Tailscale IPv4/IPv6 on both '${srcHost.name}' and '${host.name}' (address.ipv4.tailscale / address.ipv6.tailscale)
+ - clear text IPv4/IPv6 (address.ipv4.clearText / address.ipv6.clearText)
+ - secret IPv4/IPv6 (address.ipv4.secret / address.ipv6.secret)
+ '';
+
+ sanitize = lib.replaceStrings ["." ":" "/" "*"] ["-" "-" "-" "-"];
+ templateName = domain: "caddy-reverse-proxy-${sanitize domain}";
+
+ entries =
+ lib.concatMap (
r:
- lib.mapAttrs (
- _: conf:
- {
- address =
- if (r.source.host.name == host.name)
- then "127.0.0.1"
- else abort "Not implemented for outside host webserver";
- https = false;
- }
- // conf
+ lib.mapAttrsToList (
+ domain: conf: {
+ inherit domain;
+ https = conf.https or false;
+ port = conf.port;
+ resolution = resolveAddress r.source.host;
+ }
)
r.value
)
- reverseProxy);
+ reverseProxy;
- virtualHosts =
- lib.mapAttrs (domain: conf: {
- extraConfig = ''
- encode zstd gzip
- reverse_proxy http${lib.optionalString conf.https "s"}://${conf.address}:${toString conf.port}
- '';
+ importEntries = lib.filter (e: e.resolution.kind == "import") entries;
+
+ virtualHosts = lib.listToAttrs (map (e: {
+ name = e.domain;
+ value = {
+ extraConfig =
+ if e.resolution.kind == "import"
+ then ''
+ encode zstd gzip
+ import ${config.vaultix.templates.${templateName e.domain}.path}
+ ''
+ else ''
+ encode zstd gzip
+ reverse_proxy http${lib.optionalString e.https "s"}://${e.resolution.address}:${toString e.port}
+ '';
+ };
+ })
+ entries);
+
+ # Each secret IP is rendered into a Caddyfile snippet through vaultix, so
+ # the plain text address never lands in the Nix store. Caddy then splices
+ # the snippet in with the `import` directive.
+ reverseProxyTemplates = lib.listToAttrs (map (e: {
+ name = templateName e.domain;
+ value = {
+ content = ''
+ reverse_proxy http${lib.optionalString e.https "s"}://${fmtAddr e.resolution.isV6 config.vaultix.placeholder.${e.resolution.secretName}}:${toString e.port}
+ '';
+ owner = config.services.caddy.user;
+ group = config.services.caddy.group;
+ mode = "0400";
+ };
+ })
+ importEntries);
+
+ reverseProxySecrets = lib.listToAttrs (map (e: {
+ name = e.resolution.secretName;
+ value.file = e.resolution.secretFile;
})
- _processedRP;
+ importEntries);
in {
services.caddy.virtualHosts = virtualHosts;
+ vaultix.secrets = reverseProxySecrets;
+ vaultix.templates = reverseProxyTemplates;
};
};
}