Reglas de nftables para endurecer la instalación de Discourse

Hola a todos,

esta es mi primera publicación aquí. Mi experiencia: He estado administrando servidores Linux durante más de una década. Hace aproximadamente una semana, realicé una instalación de Discourse en un servidor (Debian Bullseye). ¡Me gusta mucho hasta ahora!

Ahora quería implementar las medidas de seguridad habituales en el sistema host (por ejemplo, servidor web). Esto incluye, entre otras cosas, conjuntos de reglas de nftables. Generalmente uso estos:

#!/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
   }
}

Sin embargo, después de la activación, Discourse dejó de funcionar. Sospecho que los paquetes no se están pasando a la instalación de Docker de Discourse. Esta regla en particular es probablemente problemática:

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

Pero antes de empezar a experimentar, quería preguntar si alguien ya se ha enfrentado a este problema y tiene reglas de firewall funcionales para el sistema base. ¿Tienen sentido estas reglas para una instalación de Discourse en Docker? Hasta ahora he tenido poco que ver con Docker.

Más información. Estas reglas de firewall se agregan automáticamente por 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;
	}
}

Saludos

Oye @OrkoGrayskull, ¿encontraste esto en tus búsquedas? Will UFW limit Discourse too? - #12 by supermathie Hay un poco más de contexto allí sobre cómo docker interactúa con los firewalls.

2 Me gusta

He leído el hilo y he probado algo yo mismo. Docker trae sus propias reglas de iptables y las aplica después de la instalación o cuando el servicio de docker se reinicia/carga:

service docker restart

Sin estos conjuntos de reglas, la instalación de Discourse no funcionará. Significa que al instalar, reconstruir o reiniciar el contenedor, estas reglas deben estar presentes. De lo contrario, aparecerá el mensaje de error:

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))

Sin embargo, una vez que la instalación de Discourse está completa o el contenedor se reinicia después de una actualización, las reglas de Docker aparentemente ya no son necesarias. Entonces, se pueden cargar las siguientes reglas de 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	    
   } 
}

Esto me lleva a otra pregunta: ¿qué hacer?

Ok. Creo que ya lo tengo. Reglas de ntftables funcionando con Discourse (Docker).

Después de la instalación inicial de Discourse, haz lo siguiente en Debian (Bullseye):

[1] Guarda el conjunto de reglas de Docker después de la instalación:

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

[2] Aplica y muestra el conjunto de reglas:

nft -f docker.nft
nft list ruleset

Este es el conjunto de reglas exportado después de la instalación de 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] Integra/Adapta el conjunto de reglas:
nano /etc/nftables.conf

#!/usr/sbin/nft -f

####################
# Purga/Vacía      #
####################
flush ruleset

####################
# IPv4/IPv6        #
####################
table inet filter {
####################
# Tráfico entrante #
####################	    
   chain input {       
      type filter hook input priority 0; policy drop;
      
      # Permitir interfaz de loopback	       
      iifname lo accept

      # Limitar tasa de tráfico 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

      # Permitir paquetes a conexiones establecidas/relacionadas    
      ct state established,related accept   
  
      # Descartar conexiones inválidas  
      ct state invalid drop
      
      # Permitir ICMPv4: Peticiones Ping | Mensajes de error | Mensajes de selección de router       
      ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem, router-solicitation, router-advertisement } accept              
   
      # Permitir tráfico 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

      # Permitir acceso SSH en el puerto 7777 [límite de tasa]       
      tcp dport 7777 ct state new limit rate 3/minute accept

      # Permitir tráfico HTTP / HTTPS [necesario para acceso IPv6 a Discourse]
      tcp dport { http, https } accept
      
      # Rechazar otros paquetes       
      ip protocol tcp reject with tcp reset
      ip6 nexthdr tcp reject with tcp reset    
   }
#################### 
# Tráfico saliente # 
####################    
   chain output { 		       
      type filter hook output priority 0; policy accept;  
      
      # Permitir interfaz de loopback 		       
      oifname lo accept	    
   } 
}

#################### 
# IPv6             # 
#################### 
table ip6 filter { 
#################### 
# Tráfico de reenvío # 
####################   
   chain forward {       
      type filter hook forward priority 0; policy drop;    
   } 
} 

#################### 
# IPv4             # 
#################### 
table ip filter { 
#################### 
# Tráfico de reenvío # 
####################    
   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    
   } 
#################### 
# Tráfico de 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 de 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] Deshabilita las reglas iptables de Docker después de reiniciar el servicio de docker:
nano /etc/docker/daemon.json

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

[5] Aplica tu conjunto de reglas:

nft -f /etc/nftables.conf

Eso es todo. Comprobado. Funciona. Quizás esto ayude a alguien.