main
  1{den, ...}: {
  2  den.aspects.services.caddy.tailscale = {host, ...}: {
  3    nixos = {
  4      reverseProxy,
  5      config,
  6      lib,
  7      pkgs,
  8      ...
  9    }: let
 10      tailnetDomain = den.aspects.services.caddy.tailnetDomain;
 11
 12      inherit
 13        (import ./_lib.nix {inherit lib;})
 14        mkEntry
 15        entryBlock
 16        fallbackBlock
 17        forwardAuthBlock
 18        ;
 19
 20      entries = lib.unique (
 21        lib.concatMap (
 22          r:
 23            lib.mapAttrsToList (mkEntry {
 24              kind = "inline";
 25              address = "127.0.0.1";
 26            }) (lib.filterAttrs (_: conf: conf.tailscale or false) r.value)
 27        )
 28        (lib.filter (r: r.source.host.name == host.name) reverseProxy)
 29      );
 30
 31      enabled = builtins.length entries > 0;
 32
 33      # Wildcard DNS-01 cert; subdomain sites below reuse it (Caddy 2.10+).
 34      certificateVirtualHost = {
 35        hostName = "*.${tailnetDomain}";
 36        extraConfig = ''
 37          tls {
 38            dns cloudflare {env.CF_API_TOKEN}
 39            resolvers 1.1.1.1 1.0.0.1
 40          }
 41          abort
 42        '';
 43      };
 44
 45      mkVirtualHost = name: group: let
 46        noPath = lib.filter (e: e.path == null) group;
 47        withPath = lib.filter (e: e.path != null) group;
 48        noPathCount = builtins.length noPath;
 49      in
 50        assert lib.assertMsg (noPathCount <= 1) (
 51          "Multiple entries without a path for domain '${name}':"
 52          + "only one root (pathless) entry per domain is allowed,"
 53          + "but found ${toString noPathCount}."
 54        ); let
 55          pathLines = lib.concatMap (entryBlock config) withPath;
 56          fallbackLines =
 57            if noPathCount == 1
 58            then [(fallbackBlock config (builtins.head noPath))]
 59            else [];
 60          blocks = pathLines ++ fallbackLines;
 61        in {
 62          inherit name;
 63          value.extraConfig = lib.concatLines (["encode zstd gzip" forwardAuthBlock] ++ blocks);
 64        };
 65
 66      virtualHosts = let
 67        grouped = lib.groupBy (e: e.domain) entries;
 68        siteHosts = lib.mapAttrsToList mkVirtualHost grouped;
 69      in
 70        lib.listToAttrs (siteHosts
 71          ++ [
 72            {
 73              name = "tailnet-wildcard";
 74              value = certificateVirtualHost;
 75            }
 76          ]);
 77    in
 78      lib.mkIf enabled {
 79        services.caddy.virtualHosts = virtualHosts;
 80
 81        systemd.services.caddy.serviceConfig.EnvironmentFile = [config.vaultix.templates.caddy-tailnet-env.path];
 82
 83        vaultix.secrets.tailnet-cf-token.file = ./tailscale-cloudflare-token.age;
 84        vaultix.templates.caddy-tailnet-env = {
 85          content = ''
 86            CF_API_TOKEN=${config.vaultix.placeholder.tailnet-cf-token}
 87          '';
 88          owner = config.services.caddy.user;
 89          group = config.services.caddy.group;
 90          mode = "0400";
 91        };
 92
 93        systemd.sockets.tailscale-nginx-auth = {
 94          description = "Tailscale NGINX Authentication socket";
 95          partOf = ["tailscale-nginx-auth.service"];
 96          wantedBy = ["sockets.target"];
 97          listenStreams = ["/run/tailscale.nginx-auth.sock"];
 98        };
 99
100        systemd.services.tailscale-nginx-auth = {
101          description = "Tailscale NGINX Authentication service";
102          requires = ["tailscale-nginx-auth.socket"];
103          wants = ["tailscale-wait-online.service"];
104          after = ["tailscale-wait-online.service"];
105
106          serviceConfig = {
107            ExecStart = "${pkgs.tailscale-nginx-auth}/bin/tailscale.nginx-auth";
108            DynamicUser = true;
109            BindPaths = ["/run/tailscale/tailscaled.sock"];
110            PrivateDevices = true;
111            ProtectHome = true;
112            RestrictAddressFamilies = ["AF_UNIX"];
113            Restart = "on-failure";
114          };
115        };
116      };
117  };
118}