Nftables-Regeln zur Härtung der Discourse-Installation

Hallo zusammen,

dies ist mein erster Beitrag hier. Mein Hintergrund: Ich verwalte seit über einem Jahrzehnt Linux-Server. Vor etwa einer Woche habe ich eine Discourse-Installation auf einem Server (Debian Bullseye) vorgenommen. Bisher gefällt es mir sehr gut!

Nun wollte ich die üblichen Härtungsmaßnahmen auf dem Hostsystem (z. B. Webserver) implementieren. Dazu gehören unter anderem nftables-Regelsätze. Diese verwende ich im Allgemeinen:

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

Nach der Aktivierung funktionierte Discourse jedoch nicht mehr. Ich vermute, dass die Pakete nicht an die Docker-Installation von Discourse weitergeleitet werden. Diese Regel ist wahrscheinlich problematisch:

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

Aber bevor ich jetzt anfange zu basteln, wollte ich fragen, ob sich schon jemand mit diesem Problem beschäftigt hat und funktionierende Firewall-Regeln für das Basissystem hat. Machen diese Regeln für eine Docker-Discourd-Installation überhaupt Sinn? Mit Docker hatte ich bisher wenig zu tun.

Weitere Informationen. Diese Firewall-Regeln werden automatisch von Docker (docker-ce) hinzugefügt:

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;
	}
}

Viele Grüße

1 „Gefällt mir“

Hey @OrkoGrayskull, hast du das hier bei deiner Suche gefunden:

Dort gibt es ein wenig mehr Hintergrundinformationen darüber, wie Docker mit Firewalls interagiert.

2 „Gefällt mir“

Ich habe mir den Thread jetzt durchgelesen und selbst etwas getestet. Docker bringt eigene iptables-Regeln mit und wendet diese nach der Installation oder beim (neu)starten/laden des Docker-Dienstes an:

service docker restart

Ohne diese Regelwerke funktioniert die Installation von Discourse nicht. Das bedeutet: Bei der Installation, beim Rebuild oder beim Neustart des Containers müssen diese Regeln vorhanden sein. Andernfalls kommt die Fehlermeldung:

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

Sobald die Discourse-Installation aber abgeschlossen ist oder der Container nach einem Update neu gestartet wurde, sind die Docker-Regeln anscheinend nicht mehr nötig. Dann können folgende nftables-Regeln geladen werden:

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

Das bringt mich nun zu einer weiteren Frage: Was tun?

Ok. Ich glaube, ich habe es jetzt verstanden. Funktionierende ntftables-Regelsätze mit Discourse (Docker).

Nach der initialen Discourse-Installation gehen Sie unter Debian (Bullseye) wie folgt vor:

[1] Speichern Sie den Docker-Regelsatz nach der Installation:

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

[2] Wenden Sie den Regelsatz an und zeigen Sie ihn an:

nft -f docker.nft
nft list ruleset

Dies ist der exportierte Regelsatz nach der Discourse-Installation:

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] Integrieren/Anpassen des Regelsatzes:
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] Deaktivieren Sie die Docker-iptables-Regeln nach dem Neustart des Docker-Dienstes:
nano /etc/docker/daemon.json

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

[5] Wenden Sie Ihren Regelsatz an:

nft -f /etc/nftables.conf

Das ist alles. Überprüft. Funktioniert. Vielleicht hilft das jemandem.

1 „Gefällt mir“

Danke @OrkoGrayskull!

Gibt es inzwischen Updates dazu, da /etc/docker/daemon.json nun firewall-backend auf nftables setzen kann und ich vermute, dass Sie jetzt Debian Trixie oder Bookworm verwenden – ich experimentiere damit…

Ich habe festgestellt, dass in /etc/sysctl.conf net.ipv4.ip_forward=1 gesetzt werden muss.

Die Docker-Dokumentation zu nftables besagt:

Ändern Sie die Tabellen von Docker nicht direkt, da die Änderungen wahrscheinlich verloren gehen. Docker erwartet, dass es die volle Kontrolle über seine Tabellen hat.

Daher hoffe ich, die Docker-Regeln nicht anfassen zu müssen…

Dies ist, was ich derzeit habe, einige Regeln, die ich über Ansible hinzugefügt habe (dieser Server leitet SMTP-Verkehr für Discourse weiter, deshalb wird Port 25 benötigt):

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"
        }
}

Und die Regeln von Docker / Discourse (beachten Sie, dass dieser Server keine IPv6-Adresse hat):

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;
        }
}

Und alles außer den Ports 80 und 443 funktioniert einwandfrei… :roll_eyes: