My nix-darwin and NixOS config
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(server): route Nextcloud via Cloudflare tunnel

Move Nextcloud from tailnet-only to public Cloudflare tunnel routing.
Adds Cloudflare IP ranges to Nextcloud trusted_proxies and updates
split-dns/caddy config to reflect the new architecture.

Also updates nixpkgs flake input (tailscale 1.96.5).

👾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta Code <noreply@letta.com>

+45 -31
+3 -3
flake.lock
··· 312 312 }, 313 313 "nixpkgs_5": { 314 314 "locked": { 315 - "lastModified": 1775595990, 316 - "narHash": "sha256-OEf7YqhF9IjJFYZJyuhAypgU+VsRB5lD4DuiMws5Ltc=", 315 + "lastModified": 1775811116, 316 + "narHash": "sha256-t+HZK42pB6N+i5RGbuy7Xluez/VvWbembBdvzsc23Ss=", 317 317 "owner": "nixos", 318 318 "repo": "nixpkgs", 319 - "rev": "4e92bbcdb030f3b4782be4751dc08e6b6cb6ccf2", 319 + "rev": "54170c54449ea4d6725efd30d719c5e505f1c10e", 320 320 "type": "github" 321 321 }, 322 322 "original": {
+1 -1
modules/server/infra/network/caddy.nix
··· 90 90 91 91 # ── ACME wildcard cert for tailnet vhosts ───────────────────────────────── 92 92 # Uses Cloudflare DNS-01 so no port needs to be opened. Covers all 93 - # *.ewancroft.uk tailnet services (Nextcloud, Immich, Jellyfin, Grafana). 93 + # *.ewancroft.uk tailnet services (Immich, Jellyfin, Grafana, Vaultwarden). 94 94 # 95 95 # Prerequisite: create and sops-encrypt secrets/cloudflare-acme.env 96 96 # containing the raw token value only (no KEY= prefix, no trailing newline).
+4 -1
modules/server/infra/network/cloudflare-tunnel.nix
··· 31 31 32 32 # Build ingress routes based on enabled services. 33 33 # Only route publicly-accessible services through the CF tunnel. 34 - # Nextcloud, Immich, and Jellyfin are tailnet-only — they are reachable 34 + # Immich and Jellyfin are tailnet-only — they are reachable 35 35 # via split DNS (CoreDNS) directly over the Tailscale network. 36 36 ingressRoutes = 37 37 lib.optionalAttrs cfg.services.pds.enable ( ··· 65 65 } 66 66 // lib.optionalAttrs cfg.services.sharkey.enable { 67 67 ${cfg.sharkey.hostname} = "http://127.0.0.1:${toString cfg.sharkey.caddyPort}"; 68 + } 69 + // lib.optionalAttrs cfg.services.nextcloud.enable { 70 + ${cfg.nextcloud.hostname} = "http://127.0.0.1:${toString cfg.nextcloud.caddyPort}"; 68 71 } 69 72 // lib.optionalAttrs config.services.umami.enable { 70 73 "analytics.ewancroft.uk" = "http://127.0.0.1:3011";
+4 -6
modules/server/infra/network/split-dns.nix
··· 2 2 # Split DNS — CoreDNS resolver for *.ewancroft.uk on the Tailscale interface. 3 3 # 4 4 # Why this exists: 5 - # Services like Immich, Jellyfin, and Nextcloud are only accessible via 6 - # the Cloudflare Tunnel publicly. Cloudflare imposes a ~100 MiB upload 7 - # limit. For tailnet-connected clients (phones, laptops) we want to reach 8 - # these services *directly* over the tailnet, bypassing Cloudflare. 5 + # Services like Immich and Jellyfin are only accessible via the tailnet. 6 + # For tailnet-connected clients (phones, laptops) we want to reach these 7 + # services *directly* over the tailnet. 9 8 # 10 9 # How it works: 11 10 # CoreDNS listens on the server's Tailscale IP (port 53). When a tailnet ··· 32 31 33 32 # Build host entries for all enabled Tailnet-routed services. 34 33 # Only services accessed directly over the tailnet belong here — externally 35 - # accessible services (Forgejo, PDS) must resolve via public DNS to Cloudflare. 34 + # accessible services (Forgejo, PDS, Nextcloud) must resolve via public DNS to Cloudflare. 36 35 hostEntries = lib.concatStringsSep "\n " ( 37 36 lib.optional cfg.services.immich.enable "${tsIP} ${cfg.immich.hostname}" 38 - ++ lib.optional cfg.services.nextcloud.enable "${tsIP} ${cfg.nextcloud.hostname}" 39 37 ++ lib.optional cfg.services.jellyfin.enable "${tsIP} ${cfg.jellyfin.hostname}" 40 38 ++ lib.optional cfg.services.vaultwarden.enable "${tsIP} ${cfg.vaultwarden.hostname}" 41 39 ++ [ "${tsIP} ${cfg.server.grafana.hostname}" ]
+33 -20
modules/server/services/nextcloud/nextcloud.nix
··· 26 26 # 27 27 # The admin password is only used on first install. After that you can 28 28 # rotate it via the Nextcloud web UI and the file is no longer read. 29 + # 30 + # DNS / Cloudflare: 31 + # Route traffic via Cloudflare tunnel. Add a CNAME record in Cloudflare 32 + # DNS pointing cloud.ewancroft.uk to <tunnel-id>.cfargotunnel.com. 29 33 ############################################################################## 30 34 { 31 35 config, ··· 73 77 # Store all Nextcloud state (config, apps, data) on the /srv volume. 74 78 home = nc.dataDir; 75 79 76 - # Nextcloud is tailnet-only — TLS is provided by Tailscale's WireGuard. 77 - # https = false suppresses nginx SSL config; URLs are plain HTTP. 80 + # Cloudflare tunnel terminates TLS — nginx serves plain HTTP. 78 81 https = false; 79 82 80 83 # Nginx listens on localhost only — Caddy is the only external entry point. ··· 97 100 }; 98 101 99 102 settings = { 100 - # Serve over HTTPS — Caddy terminates TLS with a self-signed cert; 101 - # Tailscale/WireGuard provides end-to-end encryption on the tailnet. 103 + # Serve over HTTPS — Cloudflare tunnel terminates TLS. 102 104 overwriteprotocol = "https"; 103 105 "overwrite.cli.url" = "https://${nc.hostname}"; 104 106 ··· 106 108 107 109 # Trust localhost (nginx) and Cloudflare tunnel as reverse proxies. 108 110 # Fixes the "reverse proxy header configuration is incorrect" warning. 111 + # Cloudflare IP ranges are documented at: 112 + # https://www.cloudflare.com/ips-v4/ and https://www.cloudflare.com/ips-v6/ 109 113 trusted_proxies = [ 110 114 "127.0.0.1" 111 115 "::1" 116 + "173.245.48.0/20" 117 + "103.21.244.0/22" 118 + "103.22.200.0/22" 119 + "103.31.4.0/22" 120 + "141.101.64.0/18" 121 + "108.162.192.0/18" 122 + "190.93.240.0/20" 123 + "188.114.96.0/20" 124 + "197.234.240.0/22" 125 + "198.41.128.0/17" 126 + "162.158.0.0/15" 127 + "104.16.0.0/13" 128 + "104.24.0.0/14" 129 + "172.64.0.0/13" 130 + "131.0.72.0/22" 131 + "2400:cb00::/32" 132 + "2606:4700::/32" 133 + "2803:f800::/32" 134 + "2405:b500::/32" 135 + "2405:8100::/32" 136 + "2a06:98c0::/29" 137 + "2c0f:f248::/32" 112 138 ]; 113 139 114 140 # Run heavy background jobs (cleanup, preview generation etc.) at 2am. ··· 146 172 }; 147 173 148 174 # Pin nginx to localhost — Caddy is the sole external entry point. 149 - # forceSSL/addSSL disabled; HSTS header explicitly removed since we serve 150 - # plain HTTP over the tailnet (WireGuard handles encryption). 151 175 services.nginx.virtualHosts."${nc.hostname}" = { 152 176 listen = [ 153 177 { ··· 179 203 }; 180 204 }; 181 205 182 - # Tailscale direct route — bypasses Cloudflare (no upload size limit). 183 - # Let's Encrypt wildcard cert (*.ewancroft.uk) via Cloudflare DNS-01. 184 - # Reachable at https://${nc.hostname} from any tailnet device once split-dns 185 - # is configured (see modules/split-dns.nix). 186 - services.caddy.virtualHosts."http://${nc.hostname}" = lib.mkIf (cfg.server.tailscaleIP != "") { 206 + # Caddy vhost — plain HTTP on caddyPort, proxied via Cloudflare tunnel. 207 + # TLS is terminated by Cloudflare; Caddy never sees HTTPS. 208 + services.caddy.virtualHosts.":${caddyPort}" = { 187 209 extraConfig = '' 188 - bind ${cfg.server.tailscaleIP} 189 - redir https://${nc.hostname}{uri} permanent 190 - ''; 191 - }; 192 - 193 - services.caddy.virtualHosts."https://${nc.hostname}" = lib.mkIf (cfg.server.tailscaleIP != "") { 194 - extraConfig = '' 195 - bind ${cfg.server.tailscaleIP} 196 - tls ${cfg.server.acmeCertDir}/fullchain.pem ${cfg.server.acmeCertDir}/key.pem 197 210 handle /.meta/* { 198 211 uri strip_prefix /.meta 199 212 root * ${metaFiles}