OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      cc6f19b0cf62
DATE      2026-07-11
SUBJECT   caddy: lock origin ingress to cloudflare ranges
FILES     4 CHANGED
HASH      cc6f19b0cf62
DATE      2026-07-11
SUBJECT   caddy: lock origin ingress to
          cloudflare ranges
FILES     4 CHANGED
 

diff --git a/hosts/linode/default.nix b/hosts/linode/default.nix
index aaa8981..0f68958 100644
--- a/hosts/linode/default.nix
+++ b/hosts/linode/default.nix
@@ -9,5 +9,7 @@
     hideLogo = true;
   };
 
+  ooknet.server.webserver.caddy.cloudflareOnly = true;
+
   system.stateVersion = "24.11";
 }

diff --git a/hosts/linode/default.nix b/host
s/linode/default.nix
index aaa8981..0f68958 100644
--- a/hosts/linode/default.nix
+++ b/hosts/linode/default.nix
@@ -9,5 +9,7 @@
     hideLogo = true;
   };
 
+  ooknet.server.webserver.caddy.cloudflareO
nly = true;
+
   system.stateVersion = "24.11";
 }
 

diff --git a/modules/nixos/server/webserver/caddy.nix b/modules/nixos/server/webse
rver/caddy.nix
index 4eab869..f023347 100644
--- a/modules/nixos/server/webserver/caddy.nix
+++ b/modules/nixos/server/webserver/caddy.nix
@@ -4,8 +4,9 @@
   pkgs,
   ...
 }: let
-  inherit (lib) mkIf mkMerge;
+  inherit (lib) mkIf mkMerge concatStringsSep;
   inherit (config.ooknet.server.webserver) caddy;
+  cf = import ./cloudflare-ips.nix;
 in {
   config = mkIf caddy.enable {
     users.groups.www = {};
@@ -13,6 +14,25 @@ in {
     # metrics scraping via tailscale
     networking.firewall.interfaces."tailscale0".allowedTCPPorts = [2019];
 
+    # origin lockdown: drop direct (non-cloudflare) hits on 80/443 so the vps
+    # cant be reached bypassing cloudflare. dedicated chain at higher priority
+    # than the firewall (drop is terminal, and firewall extraInputRules run
+    # AFTER the port accepts so a drop there wouldnt fire). lo + tailscale0
+    # exempt so loopback and tailnet admin access to the web UI still work.
+    networking.nftables.tables = mkIf caddy.cloudflareOnly {
+      cf-origin-lock = {
+        family = "inet";
+        content = ''
+          chain input {
+            type filter hook input priority -10; policy accept;
+            iifname { "lo", "tailscale0" } accept
+            tcp dport { 80, 443 } ip  saddr != { ${concatStringsSep ", " cf.v4} }
 drop
+            tcp dport { 80, 443 } ip6 saddr != { ${concatStringsSep ", " cf.v6} }
 drop
+          }
+        '';
+      };
+    };
+
     services.caddy = mkMerge [
       {
         enable = true;

diff --git a/modules/nixos/server/webserver/
caddy.nix b/modules/nixos/server/webserver/c
addy.nix
index 4eab869..f023347 100644
--- a/modules/nixos/server/webserver/caddy.n
ix
+++ b/modules/nixos/server/webserver/caddy.n
ix
@@ -4,8 +4,9 @@
   pkgs,
   ...
 }: let
-  inherit (lib) mkIf mkMerge;
+  inherit (lib) mkIf mkMerge concatStringsS
ep;
   inherit (config.ooknet.server.webserver) 
caddy;
+  cf = import ./cloudflare-ips.nix;
 in {
   config = mkIf caddy.enable {
     users.groups.www = {};
@@ -13,6 +14,25 @@ in {
     # metrics scraping via tailscale
     networking.firewall.interfaces."tailsca
le0".allowedTCPPorts = [2019];
 
+    # origin lockdown: drop direct (non-clo
udflare) hits on 80/443 so the vps
+    # cant be reached bypassing cloudflare.
 dedicated chain at higher priority
+    # than the firewall (drop is terminal, 
and firewall extraInputRules run
+    # AFTER the port accepts so a drop ther
e wouldnt fire). lo + tailscale0
+    # exempt so loopback and tailnet admin 
access to the web UI still work.
+    networking.nftables.tables = mkIf caddy
.cloudflareOnly {
+      cf-origin-lock = {
+        family = "inet";
+        content = ''
+          chain input {
+            type filter hook input priority
 -10; policy accept;
+            iifname { "lo", "tailscale0" } 
accept
+            tcp dport { 80, 443 } ip  saddr
 != { ${concatStringsSep ", " cf.v4} } drop
+            tcp dport { 80, 443 } ip6 saddr
 != { ${concatStringsSep ", " cf.v6} } drop
+          }
+        '';
+      };
+    };
+
     services.caddy = mkMerge [
       {
         enable = true;
 

diff --git a/modules/nixos/server/webserver/cloudflare-ips.nix b/modules/nixos/ser
ver/webserver/cloudflare-ips.nix
new file mode 100644
index 0000000..6af515a
--- /dev/null
+++ b/modules/nixos/server/webserver/cloudflare-ips.nix
@@ -0,0 +1,30 @@
+# cloudflare edge ip ranges (https://www.cloudflare.com/ips/)
+# used to lock the origin firewall to cloudflare; update if cloudflare changes th
em
+{
+  v4 = [
+    "173.245.48.0/20"
+    "103.21.244.0/22"
+    "103.22.200.0/22"
+    "103.31.4.0/22"
+    "141.101.64.0/18"
+    "108.162.192.0/18"
+    "190.93.240.0/20"
+    "188.114.96.0/20"
+    "197.234.240.0/22"
+    "198.41.128.0/17"
+    "162.158.0.0/15"
+    "104.16.0.0/13"
+    "104.24.0.0/14"
+    "172.64.0.0/13"
+    "131.0.72.0/22"
+  ];
+  v6 = [
+    "2400:cb00::/32"
+    "2606:4700::/32"
+    "2803:f800::/32"
+    "2405:b500::/32"
+    "2405:8100::/32"
+    "2a06:98c0::/29"
+    "2c0f:f248::/32"
+  ];
+}

diff --git a/modules/nixos/server/webserver/
cloudflare-ips.nix b/modules/nixos/server/we
bserver/cloudflare-ips.nix
new file mode 100644
index 0000000..6af515a
--- /dev/null
+++ b/modules/nixos/server/webserver/cloudfl
are-ips.nix
@@ -0,0 +1,30 @@
+# cloudflare edge ip ranges (https://www.cl
oudflare.com/ips/)
+# used to lock the origin firewall to cloud
flare; update if cloudflare changes them
+{
+  v4 = [
+    "173.245.48.0/20"
+    "103.21.244.0/22"
+    "103.22.200.0/22"
+    "103.31.4.0/22"
+    "141.101.64.0/18"
+    "108.162.192.0/18"
+    "190.93.240.0/20"
+    "188.114.96.0/20"
+    "197.234.240.0/22"
+    "198.41.128.0/17"
+    "162.158.0.0/15"
+    "104.16.0.0/13"
+    "104.24.0.0/14"
+    "172.64.0.0/13"
+    "131.0.72.0/22"
+  ];
+  v6 = [
+    "2400:cb00::/32"
+    "2606:4700::/32"
+    "2803:f800::/32"
+    "2405:b500::/32"
+    "2405:8100::/32"
+    "2a06:98c0::/29"
+    "2c0f:f248::/32"
+  ];
+}
 

diff --git a/modules/options/server/default.nix b/modules/options/server/default.n
ix
index b1a1395..6e77f21 100644
--- a/modules/options/server/default.nix
+++ b/modules/options/server/default.nix
@@ -27,6 +27,8 @@ in {
       caddy = {
         enable = mkEnableOption "";
         cloudflare.enable = mkEnableOption "";
+        # lock 80/443 to cloudflare ip ranges (origin cant be reached directly)
+        cloudflareOnly = mkEnableOption "restricting 80/443 ingress to Cloudflare
 ranges";
       };
     };
     database = {

diff --git a/modules/options/server/default.
nix b/modules/options/server/default.nix
index b1a1395..6e77f21 100644
--- a/modules/options/server/default.nix
+++ b/modules/options/server/default.nix
@@ -27,6 +27,8 @@ in {
       caddy = {
         enable = mkEnableOption "";
         cloudflare.enable = mkEnableOption 
"";
+        # lock 80/443 to cloudflare ip rang
es (origin cant be reached directly)
+        cloudflareOnly = mkEnableOption "re
stricting 80/443 ingress to Cloudflare range
s";
       };
     };
     database = {
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET