Subsections of Docker
Docker Netzwerk isolieren mit Firewall
Idee
Die Idee 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, oder auch HTTP/S Requests ins Internet sein.
Alle Zugriffe zwischen den Docker-Netzen, oder an andere Netze, können somit gezielt freigegeben oder unterbunden werden. Dieses Verfahren ist zwar anfänglich aufwendiger und es werden auch eventuell Probleme bei der Einrichtung von Containern auftreten, allerdings kann man die Kommunikation besser prüfen (loggen).
Zusätzlich ist das Unterbinden von Zugriffen in das Internet, oder das “nach Hause Telefonieren” einfacher …
Die hier verwendeten Informationen sind Beispiele und können bei falscher Anwendung zu Netzwerkunterbrechung führen!
Netzwerk
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 Container neu erstellt werden. Ein /24 ist verhältnismäßig groß, dient aber der einfacheren Handhabung und kann bei Bedarf auch verkleinert werden.
Grafik des Netzwerks- / der Container-Struktur
Docker-Compose
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!
MariaDB
# File: /srv/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
# File: /srv/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
# File: /srv/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 (Layer3) möglich ist.
Sollen Container außerhalb des eigenen Netzwerks (Subnets) miteinander sprechen, müssen Pakete geroutet werden. Dies übernimmt der Host auf dem die Container laufen. Dies ist möglich weil auf den Bridges selbst eine IP-Adresse (.1) gebunden ist und diese als Standardgateway in den Containern hinterlegt ist.
Beispiel
Nextcloud_web (172.25.2.10) muss mit der Mariadb (172.25.1.2) sprechen und weil diese nicht im selben Subnetz sind, routet der Container die Anfrage über sein Standardgateway (172.25.2.1).
Das Host-Netzwerk (Host Namespace) kennt wiederum den Weg zum MariaDB-Container, weil er selbst Mitglied im Netzwerk ist (172.25.1.1).
Erklärung was Linux Namespaces sind: https://en.wikipedia.org/wiki/Linux_namespaces (siehe Network Namespace).
Konfiguration
Routing
Routing muss aktiviert sein, was im Normalfall durch Docker passiert sein sollte.
Es kann aber im Zweifel folgenderweise aktiviert werden:
net.ipv4.ip_forward=1
sysctl -p /etc/sysctl.d/99-routing-enable.conf
Docker Daemon
Danach ist die Kommunikation grundlegend erst mal erlaubt, sofern Docker mit eigenen Firewall-Regeln dies nicht unterbindet. Nun kann der Docker Daemon konfiguriert werden, damit dieser die Firewall-Regeln mittels iptables nicht mehr anlegt ():
{
"iptables": false
}
Falls bereits Inhalte in der Datei hinterlegt sind, muss die JSON Datei ergänzt 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
Installation
pacman -Syu nftables
apt-get update; apt-get install nftables
Firewall Regeln
Die hier gezeigten Regeln sind Beispiele und sollte nicht komplett kopiert werden!
Falsche handhabung kann zu Störungen im Netzwerk führen!
#!/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
}
}
Sets / Mappings
Eine weitere Möglichkeit um weniger Regeln zu definieren, ist die Verwendung von Mappings und Sets, denn jede Regel kostet Rechenzeit.
Sets können sowohl anonym (mittels “{}”) , als auch mit festem Namen angelegt werden.
Der Vorteil von Sets mit Namen ist, dass diese zur Laufzeit ergänzt, oder Einträge gelöscht werden können.
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
}
}
Eine Auflistung möglicher Datentypen kann hier eingesehen werden: https://wiki.nftables.org/wiki-nftables/index.php/Data_types. Diese können miteinander kombiniert werden, allerdings ist dies auf maximal 5 Datentypen beschränkt. 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
Masquerading (Source NAT)
Falls ein Container in das Internet kommunizieren soll, muss noch an ein eventuell notwendig sMasquerading (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.
Es bietet sich hier auch die Nutzung von sogenannten Markierungen an, diese können beim Forwarding angeheftet werden und im späteren Verlauf von Regeln berücksichtigt werden: https://wiki.nftables.org/wiki-nftables/index.php/Matching_packet_metainformation
Testen der Regeln
nft -c -f /etc/nftables.conf
Treten Fehler auf, sollten Kommata, Klammern und Referenzen wie Variablen, Setnamen geprüft werden. NFTables gibt in der Regel konkrete Fehlermeldungen zurück.
Regeln laden & Autostart aktivieren
systemctl restart nftables
# Autostart für nftables aktivieren
systemctl enable nftables
Zugriff auf Docker Network Namespace vom Host-System aus
Die Befehle sind mit Root-Rechten durchzuführen und können bei falscher Handhabung zu Netzwerkproblemen führen!
Vorwort
Erklärungen zum Thena Linux Network Namespaces finden sich im Internet, beispielsweise:
- https://www.man7.org/linux/man-pages/man7/network_namespaces.7.html
- https://www.sobyte.net/post/2021-10/learn-linux-net-namespace/
Docker Netzwerke werden durch Namespaces repräsentiert und ermöglichen damit die Isolation der Container.
Im Gegensatz zu händisch angelegten Namespaces können diese aber nicht per ip netns Befehl aufgelistet, oder betreten werden.
Hierzu muss erst eine Verlinkung erstellt werden, damit man den Weg in den Namespace definiert…
Wofür wird das ganze benötigt?
Zum Debuggen kann es sehr hilfreich sein, wenn man statt den Container zu betreten (docker exec
), den Network Namespace betritt.
Denn im Container selbst ist auch das Dateisystem isoliert.
Durch das Betreten des Network Namespaces stehen alle Tools aus dem Hostsystem zur Verfügung.
Das ist immer dann nützlich, wenn Anwendungen im Container nicht zur Verfügung stehen (z.B. tcpdump, ip, ssh, ss, netstat, …).
Wichtig zu wissen:
Durch den Wechsel des Network Namespaces, sind anderen Namespaces davon unberührt geblieben und z.B. Prozess-IDs bei Tools wie netstat können nicht korrekt angezeigt werden, oder Fehlermeldungen auftreten… Man hat eben nur die Netzwerkumgebung gewechselt …
Zugriff auf Namespace
- Container ID ermitteln (ID steht ganz vorne)
docker ps | grep <NAME>
container_id="xxxxxxxxxxx"
- Process ID des Containers ermitteln
pid=$(docker inspect -f '{{.State.Pid}}' ${container_id})
- Namespace-Verknüpfung anlegen
mkdir -p /var/run/netns/
ln -sfT /proc/$pid/ns/net /var/run/netns/${container_id}
- Verfügbarkeit prüfen
ip netns | grep ${container_id}
- Zugriff auf den Namespace
ip netns exec ${container_id} ip a
Möchte man eine Shell im Namespace starten geht das auch:
ip netns exec ${container_id} bash
Man kann durch beenden des Befehls (Schließen der Shell) wieder in den Host Namespace wechseln, entweder mit “STRG + D” oder dem Befehl exit
!