用于加固Discourse安装的Nftables规则

大家好,

这是我在这里的第一个帖子。我的背景:我管理 Linux 服务器已有十多年。大约一周前,我在一台服务器(Debian Bullseye)上安装了 Discourse。到目前为止,我都很喜欢!

现在我想在宿主系统(例如 Web 服务器)上实施常规的加固措施。这包括 nftables 规则集等。我通常使用这些:

#!/usr/sbin/nft -f

####################
# Purge/Flush      #
####################
flush ruleset

####################
# Incoming Traffic #
####################
table inet filter {
   chain input {
      type filter hook input priority 0; policy drop;

      # Allow loopback interface
      iifname lo accept

      # Rate limit ICMPv4|ICMPv6 traffic
      ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem, router-solicitation, router-advertisement } limit rate over 5/second drop
      ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, echo-request, parameter-problem, echo-reply, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert } limit rate over 5/second drop

      # Allow packets to established/related connections
      ct state established,related accept

      # Drop invalid connections
      ct state invalid drop

      # Allow ICMPv4: Ping requests | Error messages | Router selection messages
      ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem, router-solicitation, router-advertisement } accept

      # Allow ICMPv6 traffic (https://tools.ietf.org/html/rfc4890#page-18)
      ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, echo-request, parameter-problem, echo-reply, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert } accept

      # Allow SSH access on port 2222 [rate limit]
      tcp dport 2222 ct state new limit rate 3/minute accept

      # Allow HTTP / HTTPS traffic
      tcp dport { http, https } accept

      # Reject other packets
      ip protocol tcp reject with tcp reset
      ip6 nexthdr tcp reject with tcp reset
   }
####################
# Forward Traffic  #
####################
   chain forward {
      type filter hook forward priority 0; policy drop;
   }
####################
# Outgoing Traffic #
####################
   chain output {
      type filter hook output priority 0; policy accept;

      # Allow loopback interface
      oifname lo accept
   }
}

但是,激活后 Discourse 就无法工作了。我怀疑数据包没有传递到 Discourse 的 Docker 安装中。特别是这个规则可能存在问题:

####################
# Forward Traffic  #
####################
   chain forward {
      type filter hook forward priority 0; policy drop;
   }

但在我开始修改之前,我想问一下是否有人已经处理过这个问题,并为基础系统提供了可用的防火墙规则。这些规则对于 Docker Discourse 安装来说有意义吗?到目前为止,我与 Docker 的接触很少。

更多信息。这些防火墙规则由 Docker (docker-ce) 自动添加:

table ip nat {
	chain DOCKER {
		iifname "docker0" counter packets 0 bytes 0 return
		iifname != "docker0" meta l4proto tcp tcp dport 443 counter packets 155 bytes 7070 dnat to 172.17.0.2:443
		iifname != "docker0" meta l4proto tcp tcp dport 80 counter packets 128 bytes 6263 dnat to 172.17.0.2:80
	}

	chain POSTROUTING {
		type nat hook postrouting priority srcnat; policy accept;
		oifname != "docker0" ip saddr 172.17.0.0/16 counter packets 34 bytes 2188 masquerade
		meta l4proto tcp ip saddr 172.17.0.2 ip daddr 172.17.0.2 tcp dport 443 counter packets 0 bytes 0 masquerade
		meta l4proto tcp ip saddr 172.17.0.2 ip daddr 172.17.0.2 tcp dport 80 counter packets 0 bytes 0 masquerade
	}

	chain PREROUTING {
		type nat hook prerouting priority dstnat; policy accept;
		fib daddr type local counter packets 11439 bytes 550595 jump DOCKER
	}

	chain OUTPUT {
		type nat hook output priority -100; policy accept;
		ip daddr != 127.0.0.0/8 fib daddr type local counter packets 0 bytes 0 jump DOCKER
	}
}
table ip filter {
	chain DOCKER {
		iifname != "docker0" oifname "docker0" meta l4proto tcp ip daddr 172.17.0.2 tcp dport 443 counter packets 155 bytes 7070 accept
		iifname != "docker0" oifname "docker0" meta l4proto tcp ip daddr 172.17.0.2 tcp dport 80 counter packets 128 bytes 6263 accept
	}

	chain DOCKER-ISOLATION-STAGE-1 {
		iifname "docker0" oifname != "docker0" counter packets 588 bytes 44199 jump DOCKER-ISOLATION-STAGE-2
		counter packets 1187 bytes 428425 return
	}

	chain DOCKER-ISOLATION-STAGE-2 {
		oifname "docker0" counter packets 0 bytes 0 drop
		counter packets 588 bytes 44199 return
	}

	chain FORWARD {
		type filter hook forward priority filter; policy drop;
		counter packets 1187 bytes 428425 jump DOCKER-USER
		counter packets 1187 bytes 428425 jump DOCKER-ISOLATION-STAGE-1
		oifname "docker0" ct state related,established counter packets 316 bytes 370893 accept
		oifname "docker0" counter packets 283 bytes 13333 jump DOCKER
		iifname "docker0" oifname != "docker0" counter packets 588 bytes 44199 accept
		iifname "docker0" oifname "docker0" counter packets 0 bytes 0 accept
	}

	chain DOCKER-USER {
		counter packets 1187 bytes 428425 return
	}

	chain INPUT {
		type filter hook input priority filter; policy accept;
	}
}

此致敬礼

1 个赞

@OrkoGrayskull,你在搜索中找到这个了吗?

那里有更多关于 Docker 如何与防火墙交互的背景信息。

2 个赞

我已经阅读了该线程并自己测试了一些东西。Docker 会带来自己的 iptables 规则,并在安装后或在 docker 服务(重新)启动/加载时应用它们:

service docker restart

没有这些规则集,Discourse 的安装将无法正常工作。这意味着:在安装、重建或重启容器时,必须存在这些规则。否则会出现错误消息:

Error response from daemon: driver failed programming external connectivity on endpoint app (e4d4d3cc812a11862ee6aaa6ab453e61b95da1e6d90f9a76a71959148d228476):  (iptables failed: iptables --wait -t nat -A DOCKER -p tcp -d 0/0 --dport 443 -j DNAT --to-destination 172.17.0.2:443 ! -i docker0: iptables: No chain/target/match by that name.
 (exit status 1))

但是,一旦 Discourse 安装完成或容器在更新后重启,Docker 规则就显然不再需要了。然后可以加载以下 nftables 规则:

#!/usr/sbin/nft -f

#################### 
# Purge/Flush      # 
#################### 
flush ruleset

#################### 
# Incoming Traffic # 
#################### 
table inet filter { 	    
   chain input { 		       
      type filter hook input priority 0; policy drop;
      
      # Allow loopback interface 		       
      iifname lo accept

      # Rate limit ICMPv4|ICMPv6 traffic       
      ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem, router-solicitation, router-advertisement } limit rate over 5/second drop       
      ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, echo-request, parameter-problem, echo-reply, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert } limit rate over 5/second drop

      # Allow packets to established/related connections        
      ct state established,related accept         
  
      # Drop invalid connections        
      ct state invalid drop
      
      # Allow ICMPv4: Ping requests | Error messages | Router selection messages       
      ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem, router-solicitation, router-advertisement } accept              
   
      # Allow ICMPv6 traffic (https://tools.ietf.org/html/rfc4890#page-18)       
      ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, echo-request, parameter-problem, echo-reply, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert } accept

      # Allow SSH access on port 7777 [rate limit]       
      tcp dport 7777 ct state new limit rate 3/minute accept

      # Allow HTTP / HTTPS traffic
      tcp dport { http, https } accept
      
      # Reject other packets       
      ip protocol tcp reject with tcp reset
      ip6 nexthdr tcp reject with tcp reset    
   }
#################### 
# Forward Traffic  # 
####################    
   chain forward { 		       
      type filter hook forward priority 0; policy drop; 	    
   } 
#################### 
# Outgoing Traffic # 
####################    
   chain output { 		       
      type filter hook output priority 0; policy accept;  	
      
      # Allow loopback interface 		       
      oifname lo accept	    
   } 
}

这又引出了另一个问题:该怎么办?

好的。我想我现在明白了。使用 Discourse (Docker) 的有效 ntftables 规则集。

在初始安装 Discourse 后,在 Debian (Bullseye) 上执行以下操作:

[1] 安装后保存 Docker 规则集:

nft flush ruleset
systemctl restart docker
iptables-save > iptables-docker.conf
iptables-restore-translate -f iptables-docker.conf > docker.nft

[2] 应用并显示规则集:

nft -f docker.nft
nft list ruleset

这是 Discourse 安装后的导出规则集:

table ip filter {
	chain INPUT {
		type filter hook input priority filter; policy accept;
	}

	chain FORWARD {
		type filter hook forward priority filter; policy accept;
		counter jump DOCKER-USER
		counter jump DOCKER-ISOLATION-STAGE-1
		oifname "docker0" ct state established,related counter accept
		oifname "docker0" counter DOCKER
		iifname "docker0" oifname != "docker0" counter accept
		iifname "docker0" oifname "docker0" counter accept
	}

	chain OUTPUT {
		type filter hook output priority filter; policy accept;
	}

	chain DOCKER {
		iifname != "docker0" oifname "docker0" ip daddr 172.17.0.2 tcp dport 443 counter accept
		iifname != "docker0" oifname "docker0" ip daddr 172.17.0.2 tcp dport 80 counter accept
	}

	chain DOCKER-ISOLATION-STAGE-1 {
		iifname "docker0" oifname != "docker0" counter packets 0 bytes 0 jump DOCKER-ISOLATION-STAGE-2
		counter return
	}

	chain DOCKER-ISOLATION-STAGE-2 {
		oifname "docker0" drop
		counter return
	}

	chain DOCKER-USER {
		counter return
	}
}
table ip nat {
	chain PREROUTING {
		type nat hook prerouting priority dstnat; policy accept;
		fib daddr type local counter jump DOCKER
	}

	chain INPUT {
		type nat hook input priority 100; policy accept;
	}

	chain OUTPUT {
		type nat hook output priority -100; policy accept;
		ip daddr != 127.0.0.0/8 fib daddr type local counter jump DOCKER
	}

	chain POSTROUTING {
		type nat hook postrouting priority srcnat; policy accept;
		oifname != "docker0" ip saddr 172.17.0.0/16 counter masquerade
		ip saddr 172.17.0.2 ip daddr 172.17.0.2 tcp dport 443 masquerade
		ip saddr 172.17.0.2 ip daddr 172.17.0.2 tcp dport 80 masquerade
	}

	chain DOCKER {
		iifname "docker0" counter return
		iifname != "docker0" tcp dport 443 counter dnat to 172.17.0.2:443
		iifname != "docker0" tcp dport 80 counter dnat to 172.17.0.2:80
	}
}

[3] 集成/调整规则集:
nano /etc/nftables.conf

#!/usr/sbin/nft -f

####################
# Purge/Flush      #
####################
flush ruleset

####################
# IPv4/IPv6        #
####################
table inet filter {
####################
# Incoming Traffic #
####################	    
   chain input {       
      type filter hook input priority 0; policy drop;
      
      # Allow loopback interface	       
      iifname lo accept

      # Rate limit ICMPv4|ICMPv6 traffic 
      ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem, router-solicitation, router-advertisement } limit rate over 5/second drop 
      ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, echo-request, parameter-problem, echo-reply, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert } limit rate over 5/second drop

      # Allow packets to established/related connections    
      ct state established,related accept   
  
      # Drop invalid connections  
      ct state invalid drop
      
      # Allow ICMPv4: Ping requests | Error messages | Router selection messages       
      ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem, router-solicitation, router-advertisement } accept              
   
      # Allow ICMPv6 traffic (https://tools.ietf.org/html/rfc4890#page-18)       
      ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, echo-request, parameter-problem, echo-reply, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert } accept

      # Allow SSH access on port 7777 [rate limit]       
      tcp dport 7777 ct state new limit rate 3/minute accept

      # Allow HTTP / HTTPS traffic [needed for IPv6 Discourse access]
      tcp dport { http, https } accept
      
      # Reject other packets       
      ip protocol tcp reject with tcp reset
      ip6 nexthdr tcp reject with tcp reset    
   }
#################### 
# Outgoing Traffic # 
####################    
   chain output { 		       
      type filter hook output priority 0; policy accept;  
      
      # Allow loopback interface 		       
      oifname lo accept	    
   } 
}

#################### 
# IPv6             # 
#################### 
table ip6 filter { 
#################### 
# Forward Traffic  # 
####################   
   chain forward {       
      type filter hook forward priority 0; policy drop;    
   } 
} 

#################### 
# IPv4             # 
#################### 
table ip filter { 
#################### 
# Forward Traffic  # 
####################    
   chain forward {       
      type filter hook forward priority 0; policy drop;       
      jump DOCKER-USER       
      jump DOCKER-ISOLATION-STAGE-1       
      oifname "docker0" ct state established,related accept       
      oifname "docker0" jump DOCKER       
      iifname "docker0" oifname != "docker0" accept       
      iifname "docker0" oifname "docker0" accept    
   } 
#################### 
# Docker Traffic   # 
####################    
   chain DOCKER {       
      iifname != "docker0" oifname "docker0" ip daddr 172.17.0.2 tcp dport 443 accept       
      iifname != "docker0" oifname "docker0" ip daddr 172.17.0.2 tcp dport 80 accept    
   }    
   chain DOCKER-ISOLATION-STAGE-1 {       
      iifname "docker0" oifname != "docker0" jump DOCKER-ISOLATION-STAGE-2       
      return    
   }    
   chain DOCKER-ISOLATION-STAGE-2 {       
      oifname "docker0" drop       
      return    
   }    
   chain DOCKER-USER {       
      return    
   } 
}

#################### 
# NAT IPv4         # 
#################### 
table ip nat { 
#################### 
# Docker NAT       # 
####################    
   chain PREROUTING {       
      type nat hook prerouting priority dstnat; policy accept;       
      fib daddr type local jump DOCKER    
   }
   chain INPUT {      
      type nat hook input priority 100; policy accept;    
   }
   chain OUTPUT {       
      type nat hook output priority -100; policy accept;       
      ip daddr != 127.0.0.0/8 fib daddr type local jump DOCKER    
   }
   chain POSTROUTING {       
      type nat hook postrouting priority srcnat; policy accept;       
      oifname != "docker0" ip saddr 172.17.0.0/16 masquerade       
      ip saddr 172.17.0.2 ip daddr 172.17.0.2 tcp dport 443 masquerade       
      ip saddr 172.17.0.2 ip daddr 172.17.0.2 tcp dport 80 masquerade    
   }
   chain DOCKER {       
      iifname "docker0" return       
      iifname != "docker0" tcp dport 443 dnat to 172.17.0.2:443       
      iifname != "docker0" tcp dport 80 dnat to 172.17.0.2:80    
   } 
}

[4] 重启 docker 服务后禁用 Docker iptables 规则:
nano /etc/docker/daemon.json

{
   "iptables": false
}
systemctl restart docker
nft list ruleset

[5] 应用您的规则集:

nft -f /etc/nftables.conf

就是这样。已检查。有效。也许这对某人有帮助。

1 个赞

感谢 @OrkoGrayskull

关于这个问题,现在 /etc/docker/daemon.json 可以设置 firewall-backendnftables,而且我猜你现在运行的是 Debian Trixie 或 Bookworm —— 我正在试验这个…… 是否有任何新进展?

我发现在 /etc/sysctl.conf 中需要设置 net.ipv4.ip_forward=1

Docker with nftables 文档 中有提到:

不要直接修改 Docker 的表,因为修改可能会丢失,Docker 希望完全拥有其表。

所以我希望不要触碰 Docker 的规则……

这是我目前拥有的,一些是我通过 Ansible 添加的规则(这台服务器有 Postfix 转发 SMTP 到 Discourse,所以需要端口 25):

table inet ansible_firewall {
        chain inbound_ipv4 {
                icmp type echo-request limit rate 5/second accept comment "Accept IPv4 rate limited ICMP pings"
        }

        chain inbound_ipv6 {
                icmpv6 type { nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept comment "Accept IPv6 neighbour discovery"
                icmpv6 type echo-request limit rate 5/second accept comment "Accept IPv6 rate limited ICMP pings"
        }

        chain inbound {
                type filter hook input priority filter; policy drop;
                ct state vmap { invalid : drop, established : accept, related : accept } comment "Allow traffic from established and related packets, drop invalid"
                iifname "lo" accept comment "Allow loopback traffic"
                meta protocol vmap { ip : jump inbound_ipv4, ip6 : jump inbound_ipv6 } comment "Jump to chain according to layer 3 protocol using a verdict map"
                tcp dport 22 accept comment "Allow SSH on port TCP/22"
                tcp dport { 80, 443 } accept comment "Allow HTTP on port TCP/80 and HTTPS on port TCP/443"
        }

        chain forward {
                type filter hook forward priority filter; policy drop;
        }

        chain output {
                type filter hook output priority filter; policy accept;
                oifname "lo" accept comment "Allow outgoing from loopback interface"
        }
}

以及来自 Docker / Discourse 的规则(注意这台服务器没有 IPv6 地址):

table ip docker-bridges {
        map filter-forward-in-jumps {
                type ifname : verdict
                elements = { "docker0" : jump filter-forward-in__docker0 }
        }

        map filter-forward-out-jumps {
                type ifname : verdict
                elements = { "docker0" : jump filter-forward-out__docker0 }
        }

        map nat-postrouting-in-jumps {
                type ifname : verdict
                elements = { "docker0" : jump nat-postrouting-in__docker0 }
        }

        map nat-postrouting-out-jumps {
                type ifname : verdict
                elements = { "docker0" : jump nat-postrouting-out__docker0 }
        }

        chain filter-FORWARD {
                type filter hook forward priority filter; policy accept;
                oifname vmap @filter-forward-in-jumps
                iifname vmap @filter-forward-out-jumps
        }

        chain nat-OUTPUT {
                type nat hook output priority -100; policy accept;
                ip daddr != 127.0.0.0/8 fib daddr type local counter jump nat-prerouting-and-output
        }

        chain nat-POSTROUTING {
                type nat hook postrouting priority srcnat; policy accept;
                iifname vmap @nat-postrouting-out-jumps
                oifname vmap @nat-postrouting-in-jumps
        }

        chain nat-PREROUTING {
                type nat hook prerouting priority dstnat; policy accept;
                fib daddr type local counter jump nat-prerouting-and-output
        }

        chain nat-prerouting-and-output {
                iifname != "docker0" tcp dport 80 counter dnat to 172.17.0.2:80 comment "DNAT"
                iifname != "docker0" tcp dport 443 counter dnat to 172.17.0.2:443 comment "DNAT"
        }

        chain raw-PREROUTING {
                type filter hook prerouting priority raw; policy accept;
                ip daddr 172.17.0.2 iifname != "docker0" counter drop comment "DROP DIRECT ACCESS"
        }

        chain filter-forward-in__docker0 {
                ct state established,related counter accept
                iifname "docker0" counter accept comment "ICC"
                ip daddr 172.17.0.2 tcp dport 80 counter accept
                ip daddr 172.17.0.2 tcp dport 443 counter accept
                counter drop comment "UNPUBLISHED PORT DROP"
        }

        chain filter-forward-out__docker0 {
                ct state established,related counter accept
                counter accept comment "OUTGOING"
        }

        chain nat-postrouting-in__docker0 {
        }

        chain nat-postrouting-out__docker0 {
                oifname != "docker0" ip saddr 172.17.0.0/16 counter masquerade comment "MASQUERADE"
        }
}
table ip6 docker-bridges {
        map filter-forward-in-jumps {
                type ifname : verdict
        }

        map filter-forward-out-jumps {
                type ifname : verdict
        }

        map nat-postrouting-in-jumps {
                type ifname : verdict
        }

        map nat-postrouting-out-jumps {
                type ifname : verdict
        }

        chain filter-FORWARD {
                type filter hook forward priority filter; policy accept;
                oifname vmap @filter-forward-in-jumps
                iifname vmap @filter-forward-out-jumps
        }

        chain nat-OUTPUT {
                type nat hook output priority -100; policy accept;
                ip6 daddr != ::1 fib daddr type local counter jump nat-prerouting-and-output
        }

        chain nat-POSTROUTING {
                type nat hook postrouting priority srcnat; policy accept;
                iifname vmap @nat-postrouting-out-jumps
                oifname vmap @nat-postrouting-in-jumps
        }

        chain nat-PREROUTING {
                type nat hook prerouting priority dstnat; policy accept;
                fib daddr type local counter jump nat-prerouting-and-output
        }

        chain nat-prerouting-and-output {
        }

        chain raw-PREROUTING {
                type filter hook prerouting priority raw; policy accept;
        }
}

除了端口 80 和 443 之外的其他一切都正常…… :roll_eyes: