diff --git a/modules/nixos/services/caddy-internal/default.nix b/modules/nixos/services/caddy-internal/default.nix new file mode 100644 index 0000000..cea190b --- /dev/null +++ b/modules/nixos/services/caddy-internal/default.nix @@ -0,0 +1,107 @@ +{ + config, + lib, + pkgs, + namespace, + ... +}: +with lib; +let + name = "caddy-internal"; + cfg = config.${namespace}.services.${name}; + net = lib.${namespace}.network; + + caddyPackage = pkgs.caddy.withPlugins { + plugins = [ + "github.com/caddy-dns/cloudflare@v0.2.3" + ]; + hash = "sha256-20o+14cn/eeLuf1c8uGE1ODRZGC0oxocaIVlv4tFSvA="; + }; + + # Build a virtual-host block for one proxy entry. + # Access is restricted to LAN + Nebula subnets; all other clients get 403. + mkProxyBlock = + entryName: proxyCfg: + let + fqdn = "${proxyCfg.subdomain}.${net.domain}"; + in + '' + @${entryName} host ${fqdn} + handle @${entryName} { + @${entryName}_internal { + remote_ip ${net.subnet.lan} ${net.subnet.nebula} + host ${fqdn} + } + handle @${entryName}_internal { + reverse_proxy ${proxyCfg.upstream} + ${proxyCfg.extraCaddyConfig} + } + handle { + respond "Forbidden" 403 + } + } + ''; + + proxyBlocks = lib.concatStringsSep "\n" ( + lib.mapAttrsToList mkProxyBlock (lib.filterAttrs (_: p: p.enable) cfg.proxies) + ); + + caddy-internal = lib.${namespace}.mkModule { + inherit config name; + description = "Internal-only Caddy reverse proxy with HTTPS via Cloudflare DNS challenge"; + options = { + proxies = mkOption { + type = types.attrsOf ( + types.submodule { + options = { + enable = lib.${namespace}.mkBoolOpt true "Whether to enable this proxy entry"; + subdomain = lib.${namespace}.mkOpt types.str "" "Subdomain under ${net.domain}"; + upstream = lib.${namespace}.mkOpt types.str "" "Upstream address (e.g. http://127.0.0.1:8123)"; + extraCaddyConfig = + lib.${namespace}.mkOpt types.lines "" + "Extra Caddyfile directives for this entry"; + }; + } + ); + default = { }; + description = "Internal services to proxy, each restricted to LAN + Nebula subnets"; + }; + }; + moduleConfig = { + services.caddy = { + enable = true; + package = caddyPackage; + environmentFile = config.sops.templates."caddy-internal.env".path; + email = "jalle008@proton.me"; + enableReload = true; + dataDir = "${cfg.configDir}/caddy"; + globalConfig = '' + metrics + http_port 80 + https_port 443 + default_bind 0.0.0.0 + ''; + virtualHosts."*.${net.domain}" = { + extraConfig = '' + tls { + dns cloudflare {$CLOUDFLARE_DNS_API_TOKEN} + } + + ${proxyBlocks} + ''; + }; + }; + + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + }; + }; +in +{ + imports = [ + caddy-internal + ./sops.nix + ]; +} diff --git a/modules/nixos/services/caddy-internal/sops.nix b/modules/nixos/services/caddy-internal/sops.nix new file mode 100644 index 0000000..4fa656e --- /dev/null +++ b/modules/nixos/services/caddy-internal/sops.nix @@ -0,0 +1,39 @@ +{ + config, + lib, + namespace, + ... +}: +let + cfg = config.${namespace}.services.caddy-internal; + + caddyUser = config.users.users.caddy.name; + caddyGroup = config.users.users.caddy.group; + + caddySecret = { + owner = caddyUser; + group = caddyGroup; + sopsFile = lib.snowfall.fs.get-file "secrets/nuc-secrets.yaml"; + restartUnits = [ "caddy.service" ]; + }; +in +{ + config = lib.mkIf cfg.enable { + sops = { + secrets = { + # Add this key to secrets/nuc-secrets.yaml: + # nuc/caddy/cloudflare-dns-api-token: + "nuc/caddy/cloudflare-dns-api-token" = caddySecret; + }; + + templates."caddy-internal.env" = { + content = '' + CLOUDFLARE_DNS_API_TOKEN=${config.sops.placeholder."nuc/caddy/cloudflare-dns-api-token"} + ''; + owner = caddyUser; + group = caddyGroup; + restartUnits = [ "caddy.service" ]; + }; + }; + }; +} diff --git a/secrets/nuc-secrets.yaml b/secrets/nuc-secrets.yaml index a127c8b..5e7baed 100755 --- a/secrets/nuc-secrets.yaml +++ b/secrets/nuc-secrets.yaml @@ -6,6 +6,8 @@ nuc: nuc-nixos-cert: ENC[AES256_GCM,data:lRB9M1D7xMjf/XNxljM7wPitZzY8105Hu6GmmaBgenWIsewIoSfk3tTMwFEe8nzp2jraOzcurTEujl++YOy46S0FDg3bPzdlXBwWZw12F6akfgGTGl8XX7BB7vu9UiQ8stpgUd+6G8NhNmbmVw/0oDnEsSfd4KiJgyf8Hfd9wOBhw+xZEVzHJ+D98ixChH5RB1CyFDpK1qrU9+K7GhsRFEQQdIkr4Xv6ZKnQfcB/uuwQ9Po1vFyfZRmRbRrcYFWIhJSoBr7YY5Kn5LXJxpWPDLZGNe7zibE11J09gXBRXB2Oni2G7TEsNiMlY2w4SA4aBpVngyitw9So7hzwSPX+JCYZnHr3nBCDvkWaECvju9i6pclnuuEN4PFXghaxy3QOweWA4l1DF1jrgdQ+XU8VJk4H,iv:ZdGsr5AldRJYvoG+tXW2cnS7iEs4C14qjp7hQRNdKcI=,tag:fC/DbuFeSWA0vtn9fTV7bg==,type:str] nuc-nixos-key: ENC[AES256_GCM,data:7VhntGkRDoXulB9FNelvF0YwxriuVCpUCb43V33LlCRI8dizGIGtayr4g4hwg88rdgGBZMIzpG0MrR65DhLriR+yJAuvgHBuNHb/IOt9x0Jjo4a5g9r4wwMjcj8TBFM0FiFh5oSysY/S6VYJif7aBTnqGWvrwyfWIuDOD7MWfQ==,iv:rVlATexxX7k9jrk6i11+Wgy6GhWKxTYkBBlGAqOAtMQ=,tag:YHELFmDNyHq1amjYQgTWTQ==,type:str] ca-cert: ENC[AES256_GCM,data:ZXmAZdQ0BSGJB5IZ2VJx9IxrrNTqmEYGYKua0gk61/EOnebMp5yg+swKl94+pmFWtqwKlaH+jaChAqYchONHGxOt55AAAlhGzM7BzoUseWdjTf0mFRq4Kr49Tjsz1iOK5XHr8aLESF2E3RkBRi5r0MzstutuagSO59Dj74ZT176sMYWiT7yjPXBgxlLuROQGHBV1/l+N8AMt9M2OLp/0+QYcwSrDh3u8Ts82d9YMODcbNbnCaeo64xmHLW7jJBkDTeH89rfWA5hE+haqUDv/BWe1mBvkV0YIJceyfPZdku0+hOdUIw1iXOTH9Q3KTL5FR9i7lRrCz/ZJzOVrNA==,iv:Td7TBKn+5/1V3WblVwaWjYTOZXMvJ/SMSoUnrNXdAOU=,tag:6YDpTM9kADs2KHKERNeVFQ==,type:str] + caddy: + cloudflare-dns-api-token: ENC[AES256_GCM,data:QMbo5KFej5MVIpWeWr+B4msS1dUPTtinKNJER44FPiKMNiFzkfGa4w==,iv:h18CzpdvPNOx/IWKEyuNlGmltdBeTUx4i8VMs0Dz8z0=,tag:Ke8nakp2VA/YSAH5Mvf9sQ==,type:str] sops: shamir_threshold: 1 age: @@ -153,8 +155,8 @@ sops: WFJONHNsUjJuditvVEgxQ0Y1RVhXQ1kKwBM8ljdCTTbjdasCdtLj4wZ+fX2XQIXf IMgacJ5kxYHaYpNpY5wyK2kHzPY9Ovz75WyXicPj0SCojhoKvMAWXQ== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-03-24T14:25:23Z" - mac: ENC[AES256_GCM,data:H50AiSWZ3gzFw4EOfwQE2Z7D2Uj4ZqMbMdcOEY3umoA0wzN2JRBJuIU599tV+bRI4SqlJ/vsSDuoLEqxbDR1ZJfLTLzFxFOfr8kkj7bir1tkRWZPY6pkNBsIteWeaJktXyCodCRxaDuDiKNWlD3+tA5+X3Wjhg4RcAAQ+F18gkk=,iv:SffLSph1FL9Yg915VctG+TRJ/3aoJ3D1wBFiPS2MbAc=,tag:JQJ+rRPftBAeyKtUD+JQ2A==,type:str] + lastmodified: "2026-04-09T19:46:52Z" + mac: ENC[AES256_GCM,data:sQm090y8Txfg/DATQZxhYTPFSKGne5tk9534EOmddLQo4PWFRy5FdssCU7L9gCrcWy2x/hRPq57vNgOSvXrU/JNS3Q/lC2xR8G5cFiwFTg+efXoiS4dnJnAkO8i+IROuh3kbAy0rsECcW8yQS2A1aEXi52hfEkMFrCd9nfX0BmY=,iv:HdjMqLqcztzntb2ChVxcj91KcnP3jYFNhHR8ezgoOkk=,tag:4yPrEKw+I+ECAWrCq0M+2Q==,type:str] pgp: - created_at: "2026-02-06T15:34:31Z" enc: |- @@ -177,4 +179,4 @@ sops: -----END PGP MESSAGE----- fp: CBCB9B18A6B8930B0B6ABFD1CCB8CBEB30633684 unencrypted_suffix: _unencrypted - version: 3.12.1 + version: 3.12.2 diff --git a/systems/x86_64-linux/nuc-nixos/default.nix b/systems/x86_64-linux/nuc-nixos/default.nix index d2563de..64c780d 100755 --- a/systems/x86_64-linux/nuc-nixos/default.nix +++ b/systems/x86_64-linux/nuc-nixos/default.nix @@ -64,6 +64,26 @@ in security.tpm.enable = true; services = { + caddy-internal = { + enable = true; + proxies = { + esphome = { + subdomain = "esphome"; + upstream = "http://127.0.0.1:${toString net.ports.nuc.esphome}"; + }; + otbr = { + subdomain = "otbr"; + upstream = "http://127.0.0.1:${toString net.ports.nuc.otbr}"; + }; + # hass is currently proxied by the NAS Caddy (modules/nixos/services/caddy). + # To migrate it here, remove the @hass block from that module and add: + # hass = { + # subdomain = "hass"; + # upstream = "http://127.0.0.1:${toString net.ports.nuc.homeAssistant}"; + # }; + }; + }; + home-assistant = { enable = true; automation = {