Commit 1c3bdd3
Changed files (2)
modules
services
webserver
modules/services/webserver/caddy.nix
@@ -126,10 +126,24 @@
sanitize = lib.replaceStrings ["." ":" "/" "*"] ["-" "-" "-" "-"];
templateName = domain: "caddy-reverse-proxy-${sanitize domain}";
+ normalizePath = p: let
+ p' =
+ if lib.hasPrefix "/" p
+ then p
+ else "/${p}";
+ in
+ lib.removeSuffix "/" p';
+
mkEntry = tailscale: resolution: domain: conf: {
inherit domain resolution tailscale;
https = conf.https or false;
port = conf.port;
+ path =
+ if conf ? path && conf.path != null
+ then normalizePath conf.path
+ else null;
+ stripPath = conf.stripPath or true;
+ upstream = conf.upstream or null;
};
publicEntries =
@@ -154,56 +168,92 @@
proxyLine = e:
if e.resolution.kind == "import"
then "import ${config.vaultix.templates.${templateName e.domain}.path}"
- else "reverse_proxy http${lib.optionalString e.https "s"}://${e.resolution.address}:${toString e.port}";
-
- entryLines = e:
- if !e.tailscale
- then [(proxyLine e)]
- else if (host.hasAspect den.aspects.services.tailscale-nginx-auth)
- then [
- ''
- forward_auth unix//run/tailscale.nginx-auth.sock {
- uri /auth
- header_up Remote-Addr {remote_host}
- header_up Remote-Port {remote_port}
- header_up Original-URI {uri}
- copy_headers {
- Tailscale-User>X-Webauth-User
- Tailscale-Name>X-Webauth-Name
- Tailscale-Login>X-Webauth-Login
- Tailscale-Tailnet>X-Webauth-Tailnet
- Tailscale-Profile-Picture>X-Webauth-Profile-Picture
- }
- }
- ''
- (proxyLine e)
- ]
- else
- abort ''
- Caddy on host '${host.name}' cannot use Tailscale reverse proxy: the tailscale-nginx-auth aspect is not enabled.
+ else let
+ scheme =
+ if e.https
+ then "https"
+ else "http";
+ upstream =
+ if e.upstream != null
+ then e.upstream
+ else "";
+ in "reverse_proxy ${scheme}://${e.resolution.address}:${toString e.port}${upstream}";
- Add `den.aspects.services.tailscale-nginx-auth` to the host's aspects to enable
- Tailscale authentication for reverse-proxied services.
- '';
+ entryLines = e: [(proxyLine e)];
importEntries = lib.filter (e: e.resolution.kind == "import") entries;
- # Tailscale-only vhosts are served over plain HTTP: their domains have no
- # public DNS, so Caddy's automatic HTTPS would fail trying to reach a
- # public ACME CA. And self-hosted Headscale did'nt support HTTPS serve yet.
- # The `http://` scheme disables automatic HTTPS for the site
- # (Tailscale/WireGuard already encrypts the transport). Public vhosts keep
- # automatic HTTPS.
siteName = e:
if e.tailscale && !host.settings.services.webserver.caddy.tailscaleUseHttps
then "http://${e.domain}"
else e.domain;
- virtualHosts = lib.listToAttrs (map (e: {
- name = siteName e;
- value.extraConfig = lib.concatLines (["encode zstd gzip"] ++ entryLines e);
- })
- entries);
+ forwardAuthBlock = ''
+ forward_auth unix//run/tailscale.nginx-auth.sock {
+ uri /auth
+ header_up Remote-Addr {remote_host}
+ header_up Remote-Port {remote_port}
+ header_up Original-URI {uri}
+ copy_headers {
+ Tailscale-User>X-Webauth-User
+ Tailscale-Name>X-Webauth-Name
+ Tailscale-Login>X-Webauth-Login
+ Tailscale-Tailnet>X-Webauth-Tailnet
+ Tailscale-Profile-Picture>X-Webauth-Profile-Picture
+ }
+ }
+ '';
+
+ entryBlock = e: let
+ lines = entryLines e;
+ directive =
+ if e.stripPath
+ then "handle_path"
+ else "handle";
+ mkBlock = p: "${directive} ${p} {\n${lib.concatMapStrings (l: "\t${l}\n") lines}}";
+ in [
+ (mkBlock e.path)
+ (mkBlock "${e.path}/*")
+ ];
+
+ fallbackBlock = e: let
+ lines = entryLines e;
+ in "handle {\n${lib.concatMapStrings (l: "\t${l}\n") lines}}";
+
+ virtualHosts = let
+ grouped = lib.groupBy (e: siteName e) entries;
+
+ mkVirtualHost = name: group: let
+ hasTailscale = lib.any (e: e.tailscale) group;
+ noPath = lib.filter (e: e.path == null) group;
+ withPath = lib.filter (e: e.path != null) group;
+ noPathCount = builtins.length noPath;
+ in
+ assert lib.assertMsg (noPathCount <= 1)
+ "Multiple entries without a path for domain '${name}': only one root (pathless) entry per domain is allowed, but found ${toString noPathCount}."; let
+ hasAuth = host.hasAspect den.aspects.services.tailscale-nginx-auth;
+ in
+ assert lib.assertMsg (!hasTailscale || hasAuth) (
+ "Caddy on host '${host.name}' cannot use Tailscale reverse proxy: the tailscale-nginx-auth aspect is not enabled.\n\n"
+ + "Add `den.aspects.services.tailscale-nginx-auth` to the host's aspects to enable "
+ + "Tailscale authentication for reverse-proxied services."
+ ); let
+ authLines =
+ if hasTailscale
+ then [forwardAuthBlock]
+ else [];
+ pathLines = lib.concatMap entryBlock withPath;
+ fallbackLines =
+ if noPathCount == 1
+ then [(fallbackBlock (builtins.head noPath))]
+ else [];
+ blocks = authLines ++ pathLines ++ fallbackLines;
+ in {
+ inherit name;
+ value.extraConfig = lib.concatLines (["encode zstd gzip"] ++ blocks);
+ };
+ in
+ lib.listToAttrs (lib.mapAttrsToList mkVirtualHost grouped);
reverseProxyTemplates = lib.listToAttrs (map (e: {
name = templateName e.domain;
modules/services/webserver/quirks.nix
@@ -3,10 +3,11 @@
#
# reverseProxy = {
# "example.com" = {
- # # address = "127.0.0.1"
# port = 12345;
- # https = true;
- # aliases = ["example.org"];
+ # # https = true; # optional: proxy HTTPS site
+ # # path = "/app"; # optional: only proxy requests under this path prefix
+ # # stripPath = true; # optional (default true): strip path prefix before forwarding
+ # # upstream = "/api"; # optional: prepend a path to the backend URL
# }
# }
den.quirks.reverseProxy.description = "Reverse proxies for web services";