Docker Netzwerke isolieren und per Firewall kontrollieren

Basierend auf den Informationen des Posts https://dr3st.de/docker-networking-ohne-und-mit-isolation, soll es hier um praktische Beispiele für die Isolation von Containern und die gezielte Freischaltung einzelner Dienste gehen.
Die Grundidee ist, einem oder mehreren Containern des gleichen Kontexts entsprechende Zugriffe auf andere Container / Netze zu geben.
Dies schützt davor, dass ein "gehackter" Container nicht per se auf andere Dienste zugreifen kann. Man minimiert somit die Gefahr einer Kaskade weiterer Attacken.
Konkrete Beispiele für die Isolation, die einzelne Freigaben benötigen können Dienste wie Datenbanken, SMTP Server, HTTP/S Requests ins Internet sein.
Alle Zugriffe untereinander, oder an andere Netze, können somit gezielt freigegeben werden.
Dies ist zwar anfänglich aufwendiger und es werden auch Probleme bei der Kommunikation auftreten, allerdings kann man die Kommunikation besser prüfen und auch Telefonie "nach Hause" erkennen.
Schema
In diesem Beispiel werden mittels Docker 3 Bridges angelegt, jede mit einem eigenen /24er Netz, und entsprechenden Containern die eine IP aus dem jeweiligen Netz bekommen. Diese werden statisch festgelegt und sind somit eindeutig und bleibend, selbst wenn die Container neu erstellt werden.
Ein /24 ist verhältnismäßig groß, dient aber der einfacheren Handhabung.
Wichtiger Hinweis (!):
Die hier gezeigten docker-compose.yml Dateien sind nur exemplarisch und nicht ohne Anpassung zu verwenden. Das liegt unter anderem an fehlenden Volume-Mounts und Environment-Variablen! Werden diese Dateien dennoch genutzt, ist mit anschließendem Datenverlust zu rechnen!
Es geht bei diesen Dateien um die Netzwerk-Konfiguration!
Mariadb
# docker/mariadb/docker-compose.yml
---
version: "3"
services:
mysql:
image: mariadb:10.4
container_name: mariadb
environment:
TZ: "UTC"
MYSQL_ROOT_PASSWORD: "xxxxxxxxxxx"
volumes:
- /srv/containers/mariadb/data:/var/lib/mysql:rw
restart: unless-stopped
networks:
default:
ipv4_address: 172.25.1.2
networks:
default:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.25.1.0/24
driver_opts:
com.docker.network.bridge.name: br_mariadb
Nextcloud
# docker/nextcloud/docker-compose.yml
---
version: '3'
services:
web:
container_name: nextcloud
image: nextcloud:25-fpm
networks:
default:
ipv4_address: 172.25.2.10
push:
container_name: nextcloud_push
image: nextcloud:25-fpm
networks:
default:
ipv4_address: 172.25.2.11
networks:
default:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.25.2.0/24
driver_opts:
com.docker.network.bridge.name: br_nextcloud
Mailserver
# docker/mailserver/docker-compose.yml
---
version: '3'
services:
postfix:
container_name: dovecot
image: dovecot_image
networks:
default:
ipv4_address: 172.25.3.2
postfix:
container_name: postfix
image: postfix_image
networks:
default:
ipv4_address: 172.25.3.3
networks:
default:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.25.3.0/24
driver_opts:
com.docker.network.bridge.name: br_mailserver
Firewall
Erklärung
Container im eigenen Layer2-Netz (Bridge) können untereinander sprechen, ohne dass eine Filterung durch die Firewall möglich ist. Sollen Container außerhalb des eigenen Netzwerks miteinander sprechen, müssen Pakete geroutet werden. Dies übernimmt der Host, der auf den Bridges selbst eine IP-Adresse (.1) gebunden hat und als Standardgateway für die Container fungiert.
Ein Beispiel:
Nextcloud_web (172.25.2.10) muss mit der Mariadb (172.25.1.2) sprechen und sendet seine Anfragen über das Standardgateway (172.25.2.1). Der Server (Host-Namespace) routet über seine Bridge (br_mariadb) in das Layer2- Segment der MariaDB und erreicht darüber den Container.
Kurz zur Erklärung was Linux Namespaces sind: https://en.wikipedia.org/wiki/Linux_namespaces (siehe Network Namespace).
Möchte man das Routing nutzen, sollte dies aktiviert werden (mit root Rechten):
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.d/99-routing-enable.conf
sysctl -p /etc/sysctl.d/99-routing-enable.conf
Danach ist die Kommunikation grundlegend erst mal erlaubt, sofern Docker mit eigenen Firewall-Regeln dies nicht unterbindet. Grundsätzlich empfehle ich die Firewall-Regeln durch Docker zu deaktivieren (/etc/docker/daemon.json):
{
"iptables": false
}
Falls bereits Inhalte in der Datei hinterlegt sind, muss das JSON Format berücksichtigt werden!
Danach kann Docker restarted werden, allerdings werden existente Firewall-Regeln nicht abgeräumt. Es kann also ein Reboot / Aufräumen nötig sein.
NFTables Beispiel
Nun werden mittels nftables Regeln hinterlegt, die eine Kommunikation zwischen Containern erlaubt. Diese hinterlegt man beispielsweise in der /etc/nftables.conf.
Sollte nftables nicht installiert sein:
# Ubuntu / Debian
apt-get update; apt-get install nftables
# Archlinux
pacman -Syu nftables
In der nftables.conf:
#!/usr/bin/nft -f
flush ruleset
table inet filter {
chain forward {
type filter hook forward priority 0;
# hergestellte Verbindungen werden direkt durchgelassen
ct state { established, related } accept
# erlaubt nextcloud_web mariadb zu erreichen, auf Port 3306
iifname br_nextcloud ip saddr 172.25.2.10 \
oifname br_mariadb ip daddr 172.25.1.2 tcp dport 3306 accept
# erlaube SMTP in das Internet (eth0 ist Internet-Interface)
# hierbei wird nicht nur der Versand (25), sondern auch
# SSL (465) und Submission (587) freigeschaltet
iifname br_nextcloud ip saddr 172.25.2.10 \
oifname eth0 tcp dport { 25, 465, 587 } accept
# Versand über Postfix Container
iifname br_nextcloud ip saddr 172.25.2.10 \
oifname br_mailserver ip daddr 172.25.3.2 tcp dport 465 accept
# Abrufen der Mails mittels Nextcloud Webmail Plugin (via IMAP)
iifname br_nextcloud ip saddr 172.25.2.10 \
oifname br_mailserver ip daddr 172.25.3.2 tcp dport { 143, 993 } accept
# Mariadb Verbindung von Dovecot und Postfix an Mariadb
iifname br_mailserver ip saddr { 172.25.3.0/24 } \
oifname br_mariadb ip daddr 172.25.1.2 tcp dport 3306
}
}
Hierbei handelt es sich nur um Beispiele und die Freischaltungen können sehr individuell sein.
Eine weitere Möglichkeit um nicht immer mehr Regeln zu erzeugen, ist die Verwendung von Mappings und Sets.
Sets können sowohl anonym (mittels "{}") , als auch benamt angelegt werden.
Der Vorteil von Sets mit Namen ist, dass diese zur Laufzeit angepasst werden können, ohne die komplette Firewall-Konfiguration neuzuladen. Hierbei entspricht das Anpassen dem Hinzufügen und Löschen von Inhalten.
Hier ein Beispiel für die Freischaltung von Containern. Wichtig sind die Kommata hinter den Einträgen (elements). Kommentare in eigenen Zeilen sind nur möglich, wenn ein Komma vorransteht, weil es sich dann um ein "leeres" Element handelt.
Sets alleine schalten noch nichts frei, sondern sind Sammlungen von Keys die aus verschiedenen Datentypen bestehen können. Bis zu 5 Typen kann man kombinieren und prinzipiell sind alle NFT-Datentypen möglich. Diese werden dann von Regeln geprüft, ob die Kombination im Set enthalten ist. Was am Ende passiert, hängt vom Urteil der Regel ab (accept, drop, ...).
#!/usr/bin/nft -f
flush ruleset
table inet filter {
# erlaube Transfer in das Internet für bestimmte Ports
# Kombination aus SRC_IP + SRC_INTERFACE + DEST_IP + PROTOCOL + PORT
set fwd_docker_to_wan {
type ipv4_addr . ifname . ipv4_addr . inet_proto . inet_service
flags interval
counter
elements = {
, # SMTP Versand
172.25.3.2 . "br_mailserver" . 0.0.0.0/0 . tcp . 25,
, # HTTPS zum Aktualisieren von Addons
172.25.3.2 . "br_nextcloud" . 0.0.0.0/0 . tcp . 443, # weiterer Kommentar
}
# erlaubt Traffic zwischen Containern
# Kombination aus SRC_IP + DEST_IP + PROTOCOL + PORT
set fwd_docker_accept {
type ipv4_addr . ipv4_addr . inet_proto . inet_service;
flags interval
counter
elements = {
172.25.3.2 . 172.25.1.2 . tcp . 3306, # Postfix zu Mariadb
172.25.2.10 . 172.25.1.2 . tcp . 3306, # Nextcloud zu Mariadb
172.25.2.10 . 172.25.3.3 . tcp . 143, # Nextcloud Dovecot
172.25.2.10 . 172.25.3.3 . tcp . 993, # Nextcloud Dovecot
}
}
chain forward {
type filter hook forward priority 0;
# hergestellte Verbindungen werden direkt durchgelassen
ct state { established, related } accept
# erlaubt Kombinationen die im Set fwd_docker_accept enthalten sind
ip saddr . ip daddr . meta l4proto . th dport \
@fwd_docker_accept accept
# erlaubt Kombinationen die im Set fwd_docker_to_wan enthalten sind
oifname eth0 ip saddr . iifname . ip daddr . meta l4proto . th dport \
@fwd_docker_to_wan accept
# sonstiger Transfer wird gedroppt und geloggt
# einsehbar mittels dmesg
log drop
}
}
Wie bereits hingewiesen, sind dies nur Beispiele und die Menge der Datentypen erstreckt sich über eine größere Liste: https://wiki.nftables.org/wiki-nftables/index.php/Data_types.
Je nach Bedarf können diese frei kombiniert werden. Aber nochmal erwähnt werden soll, dass maximal 5 Datentypen kombiniert werden können. Nutzt man mehr, wird das Set auf 5 Typen reduziert und wird fehlerhaft konfiguriert.
Das eine größere Menge nicht vorgesehen ist, lässt sich im Quellcode der nftables Binary gut erkennen:
https://github.com/google/nftables/blob/0dda43a5f98c5bcb2a2a3f0de3c9adf458fa45f5/set.go#L193
Falls ein Container in das Internet kommunizieren soll, muss noch an ein eventuell notwendig Masquerading (Source NAT) gedacht werden. Beispiele finden sich im Internet zu genüge, aber soll hier nicht unerwähnt bleiben:
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname eth0 masquerade
}
}
Man kann die Postrouting Regel auch genauer einstellen und auch die Quell-IP berücksichtigen, allerdings filtert die Forwarding Chain bereits den Traffic, sodass es an dieser Stelle doppelt wäre.
Testen der NFTables-Regeln
nft -c -f /etc/nftables.conf
Gibt es keine Fehler (wenn doch, Komma, Klammern prüfen!), kann nftables mittels restart die Regel neu einlesen:
systemctl restart nftables
# Autostart für nftables aktivieren
systemctl enable nftables