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 =
 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      enabled = builtins.length entries > 0;
 31
 32      # Wildcard site: only obtains the *.net.trin.one certificate via DNS-01.
 33      # Concrete subdomain sites below reuse it automatically (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 = {
 80          package = pkgs.caddy.withPlugins {
 81            plugins = [
 82              "github.com/caddy-dns/cloudflare@v0.2.4"
 83            ];
 84            hash = "sha256-7GoH8YLCoPmPExQxoga2FHB58zQDoZVf1BBwkVi0SsQ=";
 85          };
 86          virtualHosts = virtualHosts;
 87        };
 88
 89        systemd.services.caddy.serviceConfig.EnvironmentFile = [config.vaultix.templates.caddy-tailnet-env.path];
 90
 91        vaultix.secrets.tailnet-cf-token.file = ./tailscale-cloudflare-token.age;
 92        vaultix.templates.caddy-tailnet-env = {
 93          content = ''
 94            CF_API_TOKEN=${config.vaultix.placeholder.tailnet-cf-token}
 95          '';
 96          owner = config.services.caddy.user;
 97          group = config.services.caddy.group;
 98          mode = "0400";
 99        };
100
101        systemd.sockets.tailscale-nginx-auth = {
102          description = "Tailscale NGINX Authentication socket";
103          partOf = ["tailscale-nginx-auth.service"];
104          wantedBy = ["sockets.target"];
105          listenStreams = ["/run/tailscale.nginx-auth.sock"];
106        };
107
108        systemd.services.tailscale-nginx-auth = {
109          description = "Tailscale NGINX Authentication service";
110          requires = ["tailscale-nginx-auth.socket"];
111          after = ["tailscaled.service"];
112
113          serviceConfig = {
114            ExecStart = "${pkgs.tailscale-nginx-auth}/bin/tailscale.nginx-auth";
115            DynamicUser = true;
116            BindPaths = ["/run/tailscale/tailscaled.sock"];
117            PrivateDevices = true;
118            ProtectHome = true;
119            RestrictAddressFamilies = ["AF_UNIX"];
120            Restart = "on-failure";
121          };
122        };
123      };
124  };
125}