Microservices: Ende-zu-Ende, IPv6, Docker und HTTPS / TLS / SSL

IPV6, das neue IP-Protokoll kommt, langsam aber sicher. Amazon ist mit seiner AWS-Cloud voran geprescht und verlangt mittlerweile Aufpreis für externe IPv4-Adressen. Diese werden zunehmend knapper und somit sind auch die Kosten verständlich. Auch bei diversen Hosting-Anbietern hat man seit einiger Zeit zumindest einmal Vorbereitungen getroffen entsprechende Kosten an den Kunden dezidiert weitergeben zu können. Unter anderem wird ein IPv6-only-Betrieb vergünstigt angeboten. Leider ist er auch für mich noch keine wirkliche Option, bereits der Blog benötigt um gut erreichbar zu sein immer noch eine externe IPv4-Adresse, ich möchte ja eine möglichst große Reichweite haben und potentielle Leser nicht aufgrund ihrer IP-Version diskriminieren.

Nun habe ich ja mittlerweile auch einige Dinge nach aktuellem Stand der Technik per Docker virtualisiert (genauer gesagt ist es ja nur eine Paravirtualisierung). IPv6 ist in Docker immerhin angekommen, der Support lässt allerdings immer noch deutlich zu wünschen übrig. Halbwegs tragbar ist der Aufwand wenn man eine feste, öffentliche IPv6-Range hat und mit docker compose bzw. eigenen Docker-Netzwerken arbeitet. Man muss sich dann nur mit der Problematik der Netzwerkaufteilung Gedanken machen. Entgegen der häufig gelesenen Aussage ist aber bei /64-Netzwerken nicht Schluss mit dem Subnetting in IPv6. Derartige Ranges bekommt man in der Regel bei den Hosting-Anbietern im Standard-Paket. Für mich selbst haben sich /80er Aufteilungen für Docker-Netzwerke bisher ganz gut bewährt, das kann man aber je nach Bedarf noch größer oder kleiner schneiden.

Ende-zu-Ende sollte der Standard sein

Durch die Nutzung von IPv6 wird auch an vielen Stellen wieder eine Internet-Gegebenheit wieder stärker in den Vordergrund gerückt, die in den letzten Jahren immer weniger beachtet wurde: Die Kommunikation von Ende-zu-Ende, das Netzwerk an und für sich sowie die Knotenpunkte sind nur noch für das Routing bzw. die Weiterleitung verantwortlich. Was genau im IP-Paket steht ist für derartige Entscheidungen nicht relevant, sondern ausschließlich die Kopfdaten die Angaben wie Quelle und Ziel enthalten. Im besten Fall ist der Inhalt sogar verschlüsselt und somit gar nicht ohne weiteres einsehbar.

Bereits vor dem Hype und Docker als Virtualisierung gab und gibt es Methoden um mit nur einer externen IPv4-Adresse mehrere Websites betreiben zu können. Häufig wird das Verfahren VirtualHosts genannt. Es basiert im Wesentlichen darauf, dass der Endpunkt das Paket bis zum HTTP-Header aufdröselt und dort nach dem Host-Header schaut. Im SSL-Fall (also mit Verschlüsselung) geht es zusätzlich per SNI-Erweiterung um das korrekte Zertifikat für eine Verbindung zu verwenden. So lange der Server dann auch der tatsächliche Endpunkt ist, ist das auch völlig unproblematisch. Spannend wird es wenn man den annehmenden Webserver zu einem Load-Balancer oder Reverse-Proxy umbaut. Dieser schaut auch auf die Header und leitet dann das Paket an einen passenden Endpunkt weiter, allerdings wieder als Netzwerk-Paket. Was dabei oft auf der Strecke bleibt ist die Verschlüsselung – die Verbindung hinter dem Loadbalancer ist ganz häufig nicht mehr verschlüsselt.

Man mag nun einwenden, dass sich der Loadbalancer ja bei Docker auf dem gleichen Host befindet und somit die Verschlüsselung nur wenig Mehrwert bringt. Das ist aber eigentlich nur ein Spezialfall und es ist gar nicht so selten, dass der Loadbalancer das Paket an dedizierte Server weiterreicht, also doch wieder per Netzwerk und TCP/IP (egal ob nun IPv4 oder IPv6).

Docker compose – Infrastructure as Code für Netzwerke mit IPv4 und IPv6

Ganz häufig wird heute komplexere Software schon als Docker-Compose-Datei zur Installation zur Verfügung gestellt (in der Regel docker-compose.yml, aber die Datei kann heißen wie sie mag). Docker compose ist eine Art Beschreibung für ein Set von Microservices welches dann eine Software-Lösung als gesamtes bilden. Der Klassiker aus vergangener Zeit ist der LAMP-Stack (Linux, Apache, PHP, MySQL), im Docker Compose würde man in diesem Falle den Stack in zwei oder drei Container zerlegen: ein Container macht Apache, also das HTTP(S)-Handling, und MySQL (moderner MariaDB) kümmert sich exklusiv um die Datenhaltung per Datenbankserver. Wenn man es mag, kann man den PHP-Anteil noch als FCGI-Server auslagern (PHP-FPM). Alle Microservices steckt man in ein eigenes Subnetz in dem sie untereinander kommunizieren können. Von Haus aus klappt das mit privaten (oder definierten) IPv4 Netzwerken, mit ein wenig Nachhilfe geht es auch mit IPv6. Was man ggf. noch beachten muss sind die Portfreigaben (primär für IPv4) und das Routing (vor allem für IPv6).

Security – Firewall, Routing etc. – das lasse ich machen

In letzter Zeit habe ich mich wieder verstärkt mit unterschiedlicher Software-Architektur im Sinne von Microservices und Docker auseinander setzen dürfen. Dabei ist mir ein gängiges Konstrukt aufgefallen, das ich nicht ganz so tauglich finde: Bereits für einfache Installationen wird immer wieder auf einen Reverse-Proxy bzw. Load-Balancer verwiesen, vor allem wenn es darum geht Services per HTTPs bzw. SSL abzusichern. Gängiger Weise lautet die Empfehlung dann: Installiere zusätzlich zum Software-Paket einen Loadbalancer mit HTTPs der sich um die Terminierung von HTTPs und die Zertifikate kümmert. Angesprochen werden die Services hinter dem Loadbalancer dann in der Regel per unverschlüsseltem HTTP.

Damit macht man es sich als Entwickler leider recht einfach: „Absichern, das muss ich nicht machen, das können andere besser!“ Ich halte dieses Vorgehen für nicht wirklich gut, insbesondere in der Art und Weise wie es bei vielen Projekten gehandhabt wird: Dort wird teilweise sogar davon abgeraten die ggf. direkt im Service vorhandene HTTPS-Implementierung zu verwenden. Die allermeisten Frameworks bieten eine HTTPS-Implementierung mittlerweile nämlich nativ an – man müsste sich nur um das ungeliebte Kind „Security“ kümmern und das ist natürlich nicht so attraktiv wie ein neues Feature (und ich kann das teilweise auch nachvollziehen).

Mit IPv6 gewinnt man neue Freiheiten

Das Problem wird in Zukunft noch massiv größer werden, insbesondere mit der verstärkten Verbreitung von IPv6 und der Verwendung innerhalb von Docker (oder anderen Paravirtualisierungen). Mit der schieren Größe der IPv6-Netzwerke die man in der Regel ohne Kosten zur Verfügung stellt, ist es selbstverständlich attraktiv und legitim das zugewiesene Netzwerk-Segment dem eigenen Bedarf folgend weiter aufzuteilen. Es bietet sich ggf. sogar an die Netze entsprechend der angebotenen Services zu schneiden: Alles was e-mail behandelt, landet in einem getrennten Subnetz, alles was die Website betrifft landet in einem weiteren Subnetz, der Blog wiederrum erhält auch ein eigenes Netz. In den setzt man dann die einzelnen Services auf, wie man sie gerade braucht. Dank IPv6 kann man sie ggf. sogar direkt erreichbar machen, ohne einen Loadbalancer oder Reverse-Proxy aufsetzen zu müssen. Absichern muss man die Services ggf. natürlich auch: man möchte ggf. ja nicht, dass die Datenbank im Internet direkt erreichbar ist.

Im ersten Moment ist das Konstrukt ggf. etwas gewöhnungsbedürftig, das liegt vor allem daran dass es mit IPv4 einfach so nicht denkbar war. Auch wird häufig eine Art „zusätzliche Sicherheit“ ins Spiel gebracht: Verwendet man intern private IPv4 Netzwerke so werden diese Pakete nicht geroutet und der Loadbalancer oder Port-Freigaben sorgen dafür dass nur die Services erreichbar sind die man definiert hat. Gerade bei letzterem hilft es enorm sich einmal klar zu machen, was Docker im Hintergrund für einen automatisiert erledigt: Im Wesentlichen ist es nicht mehr als automatisiert Port-Forwarding-Regeln in den Firewall-Regeln zu hinterlegen, bzw. für den Verkehr nach draußen NAT.

Richtig ekelhaft sind dann für den IPv6-Betrieb Vorschläge doch intern private IPv6-Adressen zu verwenden und dann IPv6-NAT (Network Adress Translation – früher auch Masquerading)zu machen. Häufigste Begründung ist dann: Unter IPv4 macht man es ja auch so. Dabei ist gerade einer der Vorzüge von IPv6 ja gerade dass man auf diese Hilfsmittel unter IPv6 vollständig verzichten kann und sollte: Routing ist wieder Routing und bedarf keiner aufwändigen Extralogik. Die Zwischenpunkte auf einer Route und das betrifft auch den letzten Knoten in Form des Host-Systems bei einer Paravirtualisierung geht der Inhalt der Pakete gar nichts an, es geht nur um die Zieladresse. Man bekommt von außen also wie früher einen direkten Zugriff auf den Endpunkt, ohne dass jemand zwischendrin noch die Pakete aufschnüren muss (oder gar kann).

Microservices nativ mit IPv6

Wenn man nun modern sein möchte und IPv6 mit Docker und Microservices einsetzen möchte, dann gibt es ein paar Dinge die man beachten sollte damit es sicher funktioniert.

Als erstes gilt es das eigene IPv6-Netzwerk (zugewiesenes Präfix) zu segmentieren, ich selbst habe mich bei einem zugewiesenen /64 Block für einen Block /68 entschieden. Docker selbst habe ich angewiesen per Default Netzwerke der Größe /80 zu erzeugen wenn man ein internes Netz anlegt. Das ergibt 12 Bit also 4096 mögliche Netzwerke, ich denke das reicht für eine einzelne Maschine mehr als aus, aber man könnte es auch größer oder kleiner schneiden, ganz nach Bedarf.

In den Docker-Compose Files muss man derzeit die IPv6-Adressen noch statisch angeben, das ist aber nicht weiter schlimm, im einfachsten Fall nummeriert man die Container einfach beginnend bei [Präfix]::2 durch, die [Präfix]::1 ist das Gateway / der Host. Damit man sich die Adressen nicht merken muss, kann man sie auch im DNS mit Namen versehen, insbesondere für Container welche SSL / HTTPS sprechen ist das zur Prüfung der Identität sogar zwingend erforderlich.

Die Web-Services die man aufsetzt konfiguriert man so, dass diese nativ HTTPS sprechen und versorgt diese ggf. mit entsprechenden Zertifikaten. Einige bringen sogar bereits Hilfs-Container mit, welche sich um die Registrierung bei LetsEncrypt kümmern und Zertifikate für den Service somit automatisch organisieren bzw. später erneuern.

In den IPv6-Firewall-Regeln trägt man entsprechende Regeln ein: Generell sollte man alles was von extern in die Docker-Netzwerke möchte blocken (Policy: drop) und dann explizite Ziel-Adressen und Ports freigeben.

Die Konfiguration eines Reverse-Proxies / Loadbalancers entfällt für den reinen IPv6-Betrieb, möchte man abwärtskompatibel bleiben, kommt man um diesen erst einmal nicht herum, allerdings fallen die Eitnräge relativ simpel aus: Die Weiterleitung erfolgt im Zweifel an den gleichen Hostnamen wie im HTTP-Header angefordert. Das sieht komisch und rekursiv aus, aber da der Loadbalancer das Paket ja ohnehin vollständig entpackt hat, muss er für ans Ziel ohnehin ein neues machen und da es sich um einen IPv6 fähigen Endpunkt handelt, wird dieser bevorzugt genutzt.

Was noch nicht klappt

Docker hat leider immer noch keine brauchbare Möglichkeit mit dynamischen IPv6-Präfixen zurecht zu kommen, bzw. dieser automatisch an die nachgelagerten Netzwerke weiter zu geben. Somit ist das Vorgehen für die internen Server innerhalb eines Heimnetzwerks nicht anwendbar (es sei denn man hat einen Vertrag mit einem statischen Präfix, aber diese sind eher die Ausnahme).

In den Docker-Compose-Files gibt es noch kein Handling für die Port-Freigaben unter IPv6, die Firewall-Regeln werden daher im Gegensatz zu den IPv4-Pendants noch nicht automatisch angelegt, hier muss man also immer selbst aufpassen was man exponiert.

Fazit

Ja mit IPv6 kommen einige Änderungen auf einen zu, es ist nochmal eine stärkere Orientierung in Richtung Micro-Services, diese zeigt sich nun nicht mehr nur in den fachlichen Abgrenzungen sondern auch auf der Infrastruktur-Ebene: man kann dort die fachlichen Aspekte jetzt ebenfalls viel klarer trennen.

In der Software-Entwicklung kommt gerade bei der Absicherung der Kommunikationskanäle noch einiges auf uns zu: Mit IPv6 kommt wieder mehr Ende-zu-Ende-Kommunikation ins Spiel. Das ist auf der einen Seite lobenswert, aber es bringt auch wieder mehr Verantwortung hin zu den Entwicklern der Applikation-Server: Dort muss man sich daran gewöhnen, dass ein Service prinzipiell immer exponiert und nicht in einem  abgeschotteten Teilnetz hinter einem NAT steht. „Security is not my problem“ hat an dieser Stelle definitiv ausgedient, einen Service nur per HTTP zu exponieren weil es Komplexität spart ist keine zukunftsfähige Lösung. Gleiches gilt natürlich auch für jeden anderen Service den man exponieren möchte (Mailserver, Datenbanken, usw), bei jedem dieser Services muss/sollte man SSL/TLS nunmehr von Anfang an mit berücksichtigen. Wünschenswert wäre hier dass es in den gängigen Frameworks auch zum Standard wird erst einmal nur per TLS lauffähig zu sein und nur per expliziter Konfiguration ohne Transportverschlüsselung.

Services die man nicht exponieren möchte, muss man im IPv6-Umfeld per Firewall explizit abschirmen, es gibt auch Konstellationen in denen es ggf. dazu kommen kann, dass im serviceeigenen Docker-Netzwerk eine Art Load-Balancer / Reverse-Proxy auf IPv6-Basis zum Einsatz kommt. Der Klassiker hier ist ein Apache-Webserver mit nachgelagertem PHPFPM. FastCGI ist so alt, es kennt schlichtweg keine Verschlüsselung. Ebenso kann man natürlich einen LoadBalancer / Reverse-Proxy einsetzen wenn man das Routing innerhalb einer Applikation beeinflussen möchte, also zum Beispiel bestimmte Anfragen an einen spezialisierten Container weiter zu reichen (z.B. statischen Inhalt durch einen nginx ausliefern lassen, dynamische Inhalte kommen vom Applikationsserver).

Die kommenden Jahre werden wir vermehrt mit IPv6 arbeiten müssen, einfach weil keine IPv4-Adressen mehr verfügbar sind oder Kundenanschlüsse mehr und mehr IPv6 nativ angeboten werden. IPv4 wird es irgendwann nur noch gegen Aufpreis geben. Stellen wir uns also jetzt bereits auf die neue Welt mit mehr Ende-zu-Ende Kommunikation und entsprechender Verschlüsselung ein. Es ist kein Hexenwerk TLS in einer eigenen Software zu nutzen.