IPv6: Jetzt erst recht!

IPv4, IPv6, SSL und Docker – sehr viel verrückter wird es nicht!

Momentan sieht’s so aus: Die Ports 80 (http) und 443 (https) werden über den sogenannten „Userland Proxy“ von Docker auf allen Netzwerk-Interfaces des Servers veröffentlicht. Die SSL-Terminierung übernimmt Nginx. Für IPv4-Clients ist alles super, für IPv6-Clients wird der jeweilige Container intern aber per IPv4 kontaktiert. Das ist im Log des Reverse Proxy zu sehen:

Screenshot mit interner IPv4-Adresse im Log des Reverse Proxy

Im aktuellen Setup ist das unschön, aber nicht schlimm. Wirklich problematisch wird es allerdings, wenn ein Container basierend auf der Client-IP Entscheidungen treffen muss, wie es z.B. bei der Spam-Erkennung mit DNSBL für einen SMTP-Server der Fall ist. Da wäre es natürlich nicht richtig, statt der korrekten IPv6-Adresse des Clients eine interne IPv4-Adresse wie 172.20.0.1 zu prüfen. Und weil ich gerade dabei bin, einen Mailserver für die Customieze aufzusetzen, muss eine Lösung her!

Als erstes deaktiviere ich den Userland Proxy. Dazu trage ich folgendes in /etc/docker/daemon.json ein:

{
  "userland-proxy": false
}

Ein anschliendes systemctl restart docker startet den Docker-Daemon neu. Für IPv4 kümmert sich Docker selbst um die passende Netfilter-Konfiguration für die Container-Ports. Für IPv6: Keine Chance! Damit ist die Customieze über IPv6 erstmal offline. Weiter im Text…

Idee 1: Kein IPv6 anbieten

Niemals!

Idee 2: Docker-Containern öffentliche IPv6-Adressen zuweisen

Mal ausprobieren: Ein Docker-Netzwerk mit einem Teil der öffentlichen IPv6-Adressen ist ja schnell angelegt:

docker network create --ipv6 --subnet 2a01:4f8:160:30a8:c47e::/80 --gateway 2a01:4f8:160:30a8:c47e::1 ipv6public

Reverse Proxy und (noch nicht fertig konfigurierter) SMTP-Server sind testweise auch schnell in das neue Netzwerk eingehängt:

docker network connect ipv6public common_nginx-proxy_1 --ip6 2a01:4f8:160:30a8:c47e::2
docker network connect ipv6public mailcustomiezede_postfix_1 --ip6 2a01:4f8:160:30a8:c47e::10

Damit sind beide Container sofort über die angegeben IPv6-Adressen global erreichbar, weil Docker automatisch Routen und Forwarding-Regeln für das Netzwerk anlegt. Das klingt erstmal nicht schlecht, stellt mich aber vor zwei erhebliche Probleme:

  1. Der geplante Mail-Hostname mail.customieze.de müsste nach 2a01:4f8:160:30a8:c47e::10 aufgelöst werden. Dort lauscht aber jetzt kein Reverse Proxy, der sich um das automatische Ausstellen des SSL-Zertifikats kümmert. Ich müsste das also im SMTP-Container selbst implementieren.
  2. Automatische Container-Isolation: Fehlanzeige! Alle so konfigurierten Container stehen komplett offen im Internet. Selbst nicht mit Docker veröffentlichte Ports sind über die IPv6-Adressen zugänglich. Das ist für meine Spielereien mit einem halbgaren SMTP-Server eher schlecht, schließlich soll er ja nicht sofort zur Spamschleuder werden!

Beide Probleme lassen sich zwar lösen, aber zum „mal eben Ausprobieren“ ist mir das zu viel Aufwand. Zurück auf Anfang:

docker network disconnect ipv6public common_nginx-proxy_1
docker network disconnect ipv6public mailcustomiezede_postfix_1
docker network rm ipv6public

Idee 3: Alles durch Nginx

Eine weitere Möglichkeit wäre, nur den Reverse Proxy über eine öffentliche IPv6-Adresse anzubieten, und auch SMTP darüber laufen zu lassen. Das geht wohl mit Nginx auch irgendwie, allerdings braucht der dafür einen Authentifizierungs-Server.

Da der SMTP-Server aber auch Mails von anderen SMTP-Servern empfangen soll, muss er anonym zugänglich sein. Das wird so also nichts.

Idee 4: Alles durch HAProxy

Die vorherige Idee scheitert an einer Eigenart von Nginx. Warum also nicht stattdessen HAProxy verwenden? SSL-Terminierung und Reverse Proxy gehört zum Standardrepertoire, und Proxy Protocol geht damit auch.

Das automatische Konfigurieren der Backend-Server und das anfordern/erneuern der SSL-Zertifikate müsste dann natürlich für HAProxy implementiert werden. Wenn es nichts einfacheres gibt, ist das eine brauchbare Lösung.

Die Suche geht weiter…

Idee 5: NAT für IPv6

Die Kombination aus IPv6 und NAT gehört zu den Dingen, die man eigentlich lassen sollte. Schließlich gibt es genug IPv6-Adressen, so dass man diese Krücke aus IPv4-Zeiten eigentlich nicht mehr brauchen sollte. Solange Docker aber keine automatische Isolation der Container über IPv6 mitbringt, ist NAT allerdings eine ernstzunehmende Option!

Das Docker-Image robbertkl/ipv6nat sieht recht vielversprechend aus. Dafür verlangt es allerdings auch relativ viel: Es soll als privilegierter Docker-Container laufen. Na das wollen wir doch erst mal sehen!

Ein etwas längerer Blick in den Code verrät: Der Container aktualisiert mit ip6tables diverse Netfilter-Regeln, sobald ein Docker-Container mit einem Docker-Netzwerk verbunden oder von ihm getrennt wird. Bei Bedarf muss er dafür noch Kernel-Module nachladen. Prinzipiell sollte der Container also mit den Linux-Capabilitys NET_ADMIN und SYS_MODULE auskommen. Das ist auch noch ziemlich heftig, aber immerhin weniger als ein generelles „privilegiert“.

Da der NAT-Container nur IPv6-Adressen im ULA-Bereich berücksichtigt, lege ich erstmal ein Netzwerk mit passendem IPv6-Addressraum an:

docker network create --ipv6 --subnet fd00:ca7e:d09e::/48 --gateway fd00:ca7e:d09e::1 ipv6ula

Anschließend verbinde ich testweise den Reverse Proxy mit dem neuen Netzwerk:

docker network connect ipv6ula common_nginx-proxy_1

Und siehe da: Es gibt wieder IPv6! Der NAT-Container hat automatisch die benötigten Netfilter-Regeln angelegt, die zwischen den Container-Ports im ULA-Netz und den konfigurierten „öffentlichen“ Ports vermitteln:

Screenshot mit NAT-Regeln für IPv6

Ein paar Filter-Regeln kommen auch noch dazu:

Screenshot mit Filter-Regeln für IPv6

Das Netzwerkinterface br-fe04d54f08a8 in den Screenshots entspricht dabei dem Docker-Netzwerk ipv6ula.

Soweit funktioniert das alles. Beim Starten und Stoppen von Containern mit Port-Mappings werden die Netfilter-Regeln entsprechend aktualisiert. Auch mit dem SMTP-Server klappt das. Die separate IPv6-Adresse für den SMTP-Server kann ich mir damit aber auch wieder sparen.

Ich konfiguriere den NAT-Container noch im docker-compose.yml, wo ich auch das ULA-Netzwerk deklariere und den Reverse Proxy damit verbinde:

version: '3.3'

services:
  nginx-proxy:
    build: nginx-proxy # jwilder/nginx-proxy:0.6.0 + selbstsigniertes Zertifikat für unbekannte Domains + Konfiguration für große Uploads
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - certificates:/etc/nginx/certs/
      - nginx-vhost.d:/etc/nginx/vhost.d/:ro
      - nginx-html:/usr/share/nginx/html/:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
    networks:
      - autoproxy
      - ipv6ula
    environment:
      - ENABLE_IPV6=true
    labels:
      com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy: "true"

  letsencrypt-nginx-proxy-companion:
    image: jrcs/letsencrypt-nginx-proxy-companion:v1.7
    restart: unless-stopped
    depends_on:
      - nginx-proxy
    volumes:
      - certificates:/etc/nginx/certs/
      - nginx-vhost.d:/etc/nginx/vhost.d/
      - nginx-html:/usr/share/nginx/html/
      - /var/run/docker.sock:/var/run/docker.sock:ro

  ipv6nat:
    image: robbertkl/ipv6nat:v0.3.2
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    network_mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /lib/modules:/lib/modules:ro

volumes:
  nginx-vhost.d:
  nginx-html:
  certificates:
    external: true

networks:
  autoproxy:
    external: true
  ipv6ula:
    external: true

Insgesamt ist dieses Konstrukt gar nicht so schlimm, wie ich es mir vorgestellt hatte. Mal abwarten, wie es sich in den nächsten Tagen so entwickelt!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert