From e909dcd86642da6d381fea3c0229437eafde85d0 Mon Sep 17 00:00:00 2001 From: SebastianStork Date: Thu, 29 May 2025 01:05:42 +0200 Subject: [PATCH] Add caddy module with tailscale integration --- hosts/alto/default.nix | 46 +++++----- hosts/cirrus/default.nix | 26 ++---- modules/system/services/caddy.nix | 89 +++++++++++++++++++ .../system/services/tailscale/caddy-serve.nix | 66 -------------- modules/system/services/tailscale/default.nix | 39 +------- .../services/tailscale/serve-funnel.nix | 45 ++++++++++ 6 files changed, 164 insertions(+), 147 deletions(-) create mode 100644 modules/system/services/caddy.nix delete mode 100644 modules/system/services/tailscale/caddy-serve.nix create mode 100644 modules/system/services/tailscale/serve-funnel.nix diff --git a/hosts/alto/default.nix b/hosts/alto/default.nix index 56a3db4..dc01f39 100644 --- a/hosts/alto/default.nix +++ b/hosts/alto/default.nix @@ -1,8 +1,4 @@ { config, ... }: -let - tsDomain = config.custom.services.tailscale.domain; - portOf = service: config.custom.services.${service}.port; -in { system.stateVersion = "24.11"; @@ -20,28 +16,6 @@ in isFunnel = true; target = toString ./hedgedoc-redirect.html; }; - - caddyServe = { - nextcloud = { - subdomain = "cloud"; - port = portOf "nextcloud"; - }; - actualbudget = { - subdomain = "budget"; - port = portOf "actualbudget"; - }; - }; - }; - - nextcloud = { - enable = true; - domain = "cloud.${tsDomain}"; - backups.enable = true; - }; - actualbudget = { - enable = true; - domain = "budget.${tsDomain}"; - backups.enable = true; }; syncthing = { @@ -50,6 +24,26 @@ in isServer = true; backups.enable = true; }; + + nextcloud = { + enable = true; + domain = "cloud.${config.custom.services.tailscale.domain}"; + backups.enable = true; + }; + actualbudget = { + enable = true; + domain = "budget.${config.custom.services.tailscale.domain}"; + backups.enable = true; + }; + + caddy.virtualHosts = { + nextcloud = { + inherit (config.custom.services.nextcloud) domain port; + }; + actualbudget = { + inherit (config.custom.services.actualbudget) domain port; + }; + }; }; }; } diff --git a/hosts/cirrus/default.nix b/hosts/cirrus/default.nix index fd95317..3411dce 100644 --- a/hosts/cirrus/default.nix +++ b/hosts/cirrus/default.nix @@ -30,23 +30,15 @@ enable = true; domain = "git.sstork.dev"; }; + + caddy.virtualHosts = { + hedgedoc = { + inherit (config.custom.services.hedgedoc) domain port; + }; + forgejo = { + inherit (config.custom.services.forgejo) domain port; + }; + }; }; }; - - services.caddy = { - enable = true; - virtualHosts = { - ${config.custom.services.hedgedoc.domain}.extraConfig = '' - reverse_proxy localhost:${toString config.custom.services.hedgedoc.port} - ''; - ${config.custom.services.forgejo.domain}.extraConfig = '' - reverse_proxy localhost:${toString config.custom.services.forgejo.port} - ''; - }; - }; - - networking.firewall.allowedTCPPorts = [ - 80 - 443 - ]; } diff --git a/modules/system/services/caddy.nix b/modules/system/services/caddy.nix new file mode 100644 index 0000000..afd2df5 --- /dev/null +++ b/modules/system/services/caddy.nix @@ -0,0 +1,89 @@ +{ + config, + pkgs, + lib, + ... +}: +let + caddyWithTailscale = pkgs.caddy.withPlugins { + plugins = [ "github.com/tailscale/caddy-tailscale@v0.0.0-20250207163903-69a970c84556" ]; + hash = "sha256-wt3+xCsT83RpPySbL7dKVwgqjKw06qzrP2Em+SxEPto="; + }; + + allVirtualHosts = + config.custom.services.caddy.virtualHosts |> lib.filterAttrs (_: value: value.enable); + + isTailscaleDomain = domain: domain |> lib.hasSuffix config.custom.services.tailscale.domain; + + tailscaleHostsExist = lib.any (v: isTailscaleDomain v.domain) (lib.attrValues allVirtualHosts); + nonTailscaleHostsExist = lib.any (v: !isTailscaleDomain v.domain) (lib.attrValues allVirtualHosts); + + getSubdomain = domain: domain |> lib.splitString "." |> lib.head; +in +{ + options.custom.services.caddy.virtualHosts = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule ( + { name, ... }: + { + options = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + }; + domain = lib.mkOption { + type = lib.types.nonEmptyStr; + default = name; + }; + port = lib.mkOption { + type = lib.types.port; + default = null; + }; + }; + } + ) + ); + default = { }; + }; + + config = lib.mkIf (allVirtualHosts != { }) ( + lib.mkMerge [ + { + services.caddy = { + enable = true; + virtualHosts = lib.mapAttrs' ( + _: v: + lib.nameValuePair v.domain { + extraConfig = lib.concatStrings [ + (lib.optionalString (isTailscaleDomain v.domain) '' + bind tailscale/${getSubdomain v.domain} + tailscale_auth + '') + "reverse_proxy localhost:${toString v.port}" + ]; + } + ) allVirtualHosts; + }; + + networking.firewall.allowedTCPPorts = lib.mkIf nonTailscaleHostsExist [ + 80 + 443 + ]; + } + + (lib.mkIf tailscaleHostsExist { + sops.secrets."service-tailscale-auth-key".owner = config.services.caddy.user; + + services.caddy = { + package = caddyWithTailscale; + enableReload = false; + globalConfig = '' + tailscale { + auth_key {file.${config.sops.secrets."service-tailscale-auth-key".path}} + } + ''; + }; + }) + ] + ); +} diff --git a/modules/system/services/tailscale/caddy-serve.nix b/modules/system/services/tailscale/caddy-serve.nix deleted file mode 100644 index ba4d5bf..0000000 --- a/modules/system/services/tailscale/caddy-serve.nix +++ /dev/null @@ -1,66 +0,0 @@ -{ - config, - pkgs, - lib, - ... -}: -let - nodes = config.custom.services.tailscale.caddyServe |> lib.filterAttrs (_: value: value.enable); - - caddy-tailscale = pkgs.caddy.withPlugins { - plugins = [ "github.com/tailscale/caddy-tailscale@v0.0.0-20250207163903-69a970c84556" ]; - hash = "sha256-wt3+xCsT83RpPySbL7dKVwgqjKw06qzrP2Em+SxEPto="; - }; -in -{ - options.custom.services.tailscale.caddyServe = lib.mkOption { - type = lib.types.attrsOf ( - lib.types.submodule ( - { name, ... }: - { - options = { - enable = lib.mkEnableOption "" // { - default = true; - }; - subdomain = lib.mkOption { - type = lib.types.nonEmptyStr; - default = name; - }; - port = lib.mkOption { - type = lib.types.nullOr lib.types.port; - default = null; - }; - }; - } - ) - ); - default = { }; - }; - - config = lib.mkIf (nodes != { }) { - sops.secrets."service-tailscale-auth-key".owner = config.services.caddy.user; - - services.caddy = { - enable = true; - package = caddy-tailscale; - enableReload = false; - - globalConfig = '' - tailscale { - auth_key {file.${config.sops.secrets."service-tailscale-auth-key".path}} - } - ''; - - virtualHosts = lib.mapAttrs' ( - _: value: - lib.nameValuePair "https://${value.subdomain}.${config.custom.services.tailscale.domain}" { - extraConfig = '' - bind tailscale/${value.subdomain} - tailscale_auth - reverse_proxy localhost:${toString value.port} - ''; - } - ) nodes; - }; - }; -} diff --git a/modules/system/services/tailscale/default.nix b/modules/system/services/tailscale/default.nix index dcfae37..eea3ba7 100644 --- a/modules/system/services/tailscale/default.nix +++ b/modules/system/services/tailscale/default.nix @@ -1,9 +1,4 @@ -{ - config, - pkgs, - lib, - ... -}: +{ config, lib, ... }: let cfg = config.custom.services.tailscale; in @@ -16,13 +11,6 @@ in }; ssh.enable = lib.mkEnableOption ""; exitNode.enable = lib.mkEnableOption ""; - serve = { - isFunnel = lib.mkEnableOption ""; - target = lib.mkOption { - type = lib.types.nullOr lib.types.nonEmptyStr; - default = null; - }; - }; }; config = lib.mkIf cfg.enable { @@ -40,30 +28,5 @@ in "--advertise-exit-node=${lib.boolToString cfg.exitNode.enable}" ]; }; - - systemd.services = - let - mode = if cfg.serve.isFunnel then "funnel" else "serve"; - in - { - "tailscaled-${mode}" = lib.mkIf (cfg.serve.target != null) { - after = [ - "tailscaled.service" - "tailscaled-autoconnect.service" - ]; - wants = [ "tailscaled.service" ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - ExecStartPre = "${lib.getExe pkgs.tailscale} cert --min-validity 120h ${config.networking.hostName}.${cfg.domain}"; - ExecStart = "${lib.getExe pkgs.tailscale} ${mode} --bg ${cfg.serve.target}"; - ExecStop = "${lib.getExe pkgs.tailscale} ${mode} reset"; - Restart = "on-failure"; - }; - }; - - tailscaled-set.after = [ "tailscaled-autoconnect.service" ]; - }; }; } diff --git a/modules/system/services/tailscale/serve-funnel.nix b/modules/system/services/tailscale/serve-funnel.nix new file mode 100644 index 0000000..5d8ce33 --- /dev/null +++ b/modules/system/services/tailscale/serve-funnel.nix @@ -0,0 +1,45 @@ +{ + config, + pkgs, + lib, + ... +}: +let + cfg = config.custom.services.tailscale; +in +{ + options.custom.services.tailscale.serve = { + isFunnel = lib.mkEnableOption ""; + target = lib.mkOption { + type = lib.types.nullOr lib.types.nonEmptyStr; + default = null; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services = + let + mode = if cfg.serve.isFunnel then "funnel" else "serve"; + in + { + "tailscaled-${mode}" = lib.mkIf (cfg.serve.target != null) { + after = [ + "tailscaled.service" + "tailscaled-autoconnect.service" + ]; + wants = [ "tailscaled.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStartPre = "${lib.getExe pkgs.tailscale} cert --min-validity 120h ${config.networking.hostName}.${cfg.domain}"; + ExecStart = "${lib.getExe pkgs.tailscale} ${mode} --bg ${cfg.serve.target}"; + ExecStop = "${lib.getExe pkgs.tailscale} ${mode} reset"; + Restart = "on-failure"; + }; + }; + + tailscaled-set.after = [ "tailscaled-autoconnect.service" ]; + }; + }; +}