Правила Nftables для усиления защиты установки Discourse

Всем привет,

это мой первый пост здесь. Немного о моем опыте: я администрирую Linux-серверы уже более десяти лет. Примерно неделю назад я установил Discourse на сервер (Debian Bullseye). Пока всё отлично!

Теперь я хочу применить стандартные меры по укреплению безопасности на хост-системе (например, веб-сервере). В частности, это наборы правил nftables. Обычно я использую следующие:

#!/usr/sbin/nft -f

#################### 
# Очистка/Сброс      # 
#################### 
flush ruleset

#################### 
# Входящий трафик # 
#################### 
table inet filter { 	    
   chain input { 		       
      type filter hook input priority 0; policy drop;
      
      # Разрешить интерфейс loopback 		       
      iifname lo accept

      # Ограничение скорости для ICMPv4|ICMPv6       
      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

      # Разрешить пакеты для установленных/связанных соединений        
      ct state established,related accept         
  
      # Отбросить некорректные соединения        
      ct state invalid drop
      
      # Разрешить ICMPv4: запросы Ping | Сообщения об ошибках | Сообщения о выборе маршрутизатора       
      ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem, router-solicitation, router-advertisement } accept              
   
      # Разрешить трафик ICMPv6 (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

      # Разрешить SSH-доступ на порту 2222 [ограничение скорости]       
      tcp dport 2222 ct state new limit rate 3/minute accept

      # Разрешить HTTP / HTTPS трафик
      tcp dport { http, https } accept
      
      # Отклонить остальные пакеты       
      ip protocol tcp reject with tcp reset
      ip6 nexthdr tcp reject with tcp reset    
   }
#################### 
# Пропускаемый трафик  # 
####################    
   chain forward { 		       
      type filter hook forward priority 0; policy drop; 	    
   } 
#################### 
# Исходящий трафик # 
####################    
   chain output { 		       
      type filter hook output priority 0; policy accept;  	
      
      # Разрешить интерфейс loopback 		       
      oifname lo accept	    
   } 
}

Однако после активации правил Discourse перестал работать. Я подозреваю, что пакеты не передаются в Docker-установку Discourse. Скорее всего, проблема именно в этом правиле:

#################### 
# Пропускаемый трафик  # 
####################    
   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;		
	}
}

Всего доброго

Привет, @OrkoGrayskull, нашли ли вы

в своих поисках? Там есть немного больше предыстории о том, как Docker взаимодействует с фаерволами.

Я уже прочитал тему и сам кое-что проверил. 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	    
   } 
}

Теперь у меня возникает ещё один вопрос: что делать?

Хорошо. Думаю, теперь я всё понял. Рабочий набор правил nftables для Discourse (Docker).

После первоначальной установки 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

####################
# Очистка/Сброс    #
####################
flush ruleset

####################
# IPv4/IPv6        #
####################
table inet filter {
####################
# Входящий трафик  #
####################	    
   chain input {       
      type filter hook input priority 0; policy drop;
      
      # Разрешить интерфейс локальной петли	       
      iifname lo accept

      # Ограничить частоту трафика ICMPv4|ICMPv6 
      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

      # Разрешить пакеты для установленных/связанных подключений    
      ct state established,related accept   
  
      # Отбросить некорректные подключения  
      ct state invalid drop
      
      # Разрешить ICMPv4: Запросы ping | Сообщения об ошибках | Сообщения выбора маршрутизатора       
      ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem, router-solicitation, router-advertisement } accept              
   
      # Разрешить трафик ICMPv6 (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

      # Разрешить доступ SSH на порт 7777 [ограничение частоты]       
      tcp dport 7777 ct state new limit rate 3/minute accept

      # Разрешить трафик HTTP / HTTPS [необходимо для доступа к Discourse по IPv6]
      tcp dport { http, https } accept
      
      # Отклонить остальные пакеты       
      ip protocol tcp reject with tcp reset
      ip6 nexthdr tcp reject with tcp reset    
   }
#################### 
# Исходящий трафик # 
####################    
   chain output { 		       
      type filter hook output priority 0; policy accept;  	
      
      # Разрешить интерфейс локальной петли 		       
      oifname lo accept	    
   } 
}

#################### 
# IPv6             # 
#################### 
table ip6 filter { 
#################### 
# Пересылка трафика  # 
####################   
   chain forward {       
      type filter hook forward priority 0; policy drop;    
   } 
} 

#################### 
# IPv4             # 
#################### 
table ip filter { 
#################### 
# Пересылка трафика  # 
####################    
   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   # 
####################    
   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 { 
#################### 
# NAT Docker       # 
####################    
   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] Отключите правила iptables Docker после перезапуска службы docker:
nano /etc/docker/daemon.json

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

[5] Примените ваш набор правил:

nft -f /etc/nftables.conf

Вот и всё. Проверено. Работает. Возможно, это кому-то поможет.

Спасибо @OrkoGrayskull!

Есть ли какие-либо обновления по этой теме теперь, когда в /etc/docker/daemon.json можно установить firewall-backend в значение nftables, и я полагаю, вы теперь используете Debian Trixie или Bookworm? Я экспериментирую с этим…

Я обнаружил, что в файле /etc/sysctl.conf необходимо установить net.ipv4.ip_forward=1.

В документации Docker с nftables указано:

Не изменяйте таблицы Docker напрямую, так как изменения, скорее всего, будут потеряны. Docker ожидает полного контроля над своими таблицами.

Поэтому я надеюсь не трогать правила Docker…

Вот что у меня сейчас есть, некоторые правила я добавил через Ansible (этот сервер пересылает SMTP через Postfix в 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: