ADMIN

2021

11

2022-11-01T12:00:00

Collaboration

PRAXIS

048

Virtualisierung

Container

Kubernetes

FortiGate als Loadbalancer für Kubernetes

Durchstellen bitte!

von Konstantin Agouros

Veröffentlicht in Ausgabe 11/2021 - PRAXIS

Kubernetes hat sich als Standardplattform zum containerbasierten Applikationsbetrieb etabliert. Die Container laufen in der Regel in einem privaten Adressraum und werden bei jedem Neustart auf einen freien Host verteilt. Diese Dynamik macht es für Drittanwendungen allerdings mitunter schwer, eine Netzwerkverbindung herzustellen. Wir zeigen, wie Sie eine FortiGate-Firewall als Loadbalancer einsetzen und so gleichzeitig für Erreichbarkeit und Sicherheit sorgen.

Firewalls von Fortinet verfügen über SDN-Konnektoren und lassen sich so mit diversen externen Komponenten wie OpenStack, Public-Cloud-Providern wie AWS oder Google oder eben auch Kubernetes koppeln. Außerdem bieten die FortiGate-Modelle einen Loadbalancer, der über die genannten Konnektoren an die privaten IP-Adressen der Container kommen kann, um diese ins Routing zu integrieren. Damit die Firewall als Loadbalancer jedoch weiß, an welchen physischen Host sie die Pakete für den entsprechenden Dienst schicken soll, fehlt eine letzte Komponente, die die Zuordnung der laufenden Kubernetes-Pods zu den physischen Hosts schafft. Hier kommt das Netzwerk-Plug-in "Calico" von Tigera zum Einsatz. Es ermöglicht, die gewünschte Information über das Border Gateway Protocol (BGP) zu exportieren – damit ist das Routing zu den einzelnen Kubernetes-Pods gesichert. Der Vorteil dieses Konstrukts ist, dass ein Netzwerkgerät die Pakete verteilt. Zum anderen lassen sich sämtliche Sicherheitsfunktionen vorschalten, die die Firewall bietet, um den Dienst so vor Angriffen zu schützen.
Calico und Kubernetes verzahnen
Der erste Schritt in unserem Workshop ist also das Ausrollen von Calico als Netzwerk-Plug-in. Das Kubernetes-Deployment-File im YAML-Format laden Sie unter [1] herunter. Das Kommando kubectl apply -f calico.yaml rollt dann das Plug-in innerhalb des Clusters aus und verteilt es auf die Knoten. Sie müssen das Plug-in aber noch konfigurieren, zum Beispiel ist die Firewall als BGP-Peer einzutragen. Hierfür stehen zwei Möglichkeiten zur Auswahl: Mit dem Programm "calicoctl" können Sie ein eigenes Binary herunterladen. Die aktuellen Releases finden Sie unter [2]. Alternativ gibt es von Tigera den Calico-API-Server. Einmal installiert, lässt sich dann kubectl nutzen, um die zum Calico-Bereich gehörenden Ressourcen zu verwalten. Nutzen Sie das calicoctl-Binary, sollten Sie es in einen eigenen Pfad legen, zum Beispiel "/usr/local/bin". Um zu überprüfen, ob das Deployment funktioniert hat, dient das Kommando calicoctl node status, das einen Output wie in Listing 1 liefern sollte. Zum Anlegen des BGP-Peers erstellen Sie schließlich eine YAML-Datei nach folgendem Beispiel:
---
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
      name: bgppeer-forti
spec:
      peerIP: 10.10.1.134
     asNumber: 64512
Listing 1: Output von calicoctl node status
Calico process is running. IPv4 BGP status +----------------------+-------------------------+----------+---------------+---------------+ | PEER ADDRESS | PEER TYPE            | STATE | SINCE         | INFO          | +----------------------+-------------------------+----------+---------------+---------------+ | 10.10.1.173         | node-to-node mesh  | up         | 2021-06-28 | Established | | 10.10.1.243         | node-to-node mesh  | up         | 2021-06-28 | Established | | 10.10.1.134         | global                      | up         | 2021-06-28  | Established | +----------------------+-------------------------+----------+---------------+---------------+ IPv6 BGP status No IPv6 peers found.
Der Parameter "asNumber" meint das "Autonomous System", das die Gegenstelle beschreibt. Calico selbst läuft im AS 64512, dieser Wert lässt sich aber ändern. Soll das Calico-AS etwa 64555 sein, so erreichen Sie dies mit dem folgenden Befehl:
Firewalls von Fortinet verfügen über SDN-Konnektoren und lassen sich so mit diversen externen Komponenten wie OpenStack, Public-Cloud-Providern wie AWS oder Google oder eben auch Kubernetes koppeln. Außerdem bieten die FortiGate-Modelle einen Loadbalancer, der über die genannten Konnektoren an die privaten IP-Adressen der Container kommen kann, um diese ins Routing zu integrieren. Damit die Firewall als Loadbalancer jedoch weiß, an welchen physischen Host sie die Pakete für den entsprechenden Dienst schicken soll, fehlt eine letzte Komponente, die die Zuordnung der laufenden Kubernetes-Pods zu den physischen Hosts schafft. Hier kommt das Netzwerk-Plug-in "Calico" von Tigera zum Einsatz. Es ermöglicht, die gewünschte Information über das Border Gateway Protocol (BGP) zu exportieren – damit ist das Routing zu den einzelnen Kubernetes-Pods gesichert. Der Vorteil dieses Konstrukts ist, dass ein Netzwerkgerät die Pakete verteilt. Zum anderen lassen sich sämtliche Sicherheitsfunktionen vorschalten, die die Firewall bietet, um den Dienst so vor Angriffen zu schützen.
Calico und Kubernetes verzahnen
Der erste Schritt in unserem Workshop ist also das Ausrollen von Calico als Netzwerk-Plug-in. Das Kubernetes-Deployment-File im YAML-Format laden Sie unter [1] herunter. Das Kommando kubectl apply -f calico.yaml rollt dann das Plug-in innerhalb des Clusters aus und verteilt es auf die Knoten. Sie müssen das Plug-in aber noch konfigurieren, zum Beispiel ist die Firewall als BGP-Peer einzutragen. Hierfür stehen zwei Möglichkeiten zur Auswahl: Mit dem Programm "calicoctl" können Sie ein eigenes Binary herunterladen. Die aktuellen Releases finden Sie unter [2]. Alternativ gibt es von Tigera den Calico-API-Server. Einmal installiert, lässt sich dann kubectl nutzen, um die zum Calico-Bereich gehörenden Ressourcen zu verwalten. Nutzen Sie das calicoctl-Binary, sollten Sie es in einen eigenen Pfad legen, zum Beispiel "/usr/local/bin". Um zu überprüfen, ob das Deployment funktioniert hat, dient das Kommando calicoctl node status, das einen Output wie in Listing 1 liefern sollte. Zum Anlegen des BGP-Peers erstellen Sie schließlich eine YAML-Datei nach folgendem Beispiel:
---
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
      name: bgppeer-forti
spec:
      peerIP: 10.10.1.134
     asNumber: 64512
Listing 1: Output von calicoctl node status
Calico process is running. IPv4 BGP status +----------------------+-------------------------+----------+---------------+---------------+ | PEER ADDRESS | PEER TYPE            | STATE | SINCE         | INFO          | +----------------------+-------------------------+----------+---------------+---------------+ | 10.10.1.173         | node-to-node mesh  | up         | 2021-06-28 | Established | | 10.10.1.243         | node-to-node mesh  | up         | 2021-06-28 | Established | | 10.10.1.134         | global                      | up         | 2021-06-28  | Established | +----------------------+-------------------------+----------+---------------+---------------+ IPv6 BGP status No IPv6 peers found.
Der Parameter "asNumber" meint das "Autonomous System", das die Gegenstelle beschreibt. Calico selbst läuft im AS 64512, dieser Wert lässt sich aber ändern. Soll das Calico-AS etwa 64555 sein, so erreichen Sie dies mit dem folgenden Befehl:
calicoctl patch bgpconfiguration default -p '{"spec": {"asNumber": "64555"}}'
Die Konfiguration der Firewall selbst erklären wir später.
Deployment eines Beispieldiensts
Wir wollen die Funktion des Loadbalancers an einem Beispieldienst demonstrieren. Zum Einsatz kommt ein einfacher Webserver-Container, dessen Image unter [3] zur Verfügung steht. Das Deployment in Listing 2 erzeugt eine Instanz des Containers. Die Menge der Replicas steht dabei anfangs auf 1.
Listing 2: Deployment eines Beispiel-Webservers
--- apiVersion: apps/v1 kind: Deployment metadata:       name: demoweb       namespace: default spec:       strategy:            type: Recreate       replicas: 1       selector:            matchLabels:                 app: httpbin       template:            metadata:                 labels:                      app: httpbin                 name: demoweb       spec:            containers:                 - image: docker.io/kennethreitz/httpbin                   imagePullPolicy: IfNotPresent                    name: httpbin                    ports:                      - containerPort: 80
Ein wichtiges Konfigurationselement ist dabei das Label, das in der Konfiguration der Firewall Verwendung findet, um die richtigen IP-Adressen aus den Datensätzen herauszufiltern. Ist die Datei als "webdemo.yaml" gespeichert, funktioniert der Rollout mit dem Kommando kubectl apply -f webdemo.yaml. Mit kubectl get pods überprüfen Sie für die einzelnen Pods und mit kubectl get deployments für das gesamte Deployment, ob der Rollout gelungen ist. Der Output dazu ist in Listing 3 abgebildet.
Listing 3: Output zur Prüfung des Deployments
# kubectl get pods NAME                                                       READY           STATUS           RESTARTS           AGE demoweb-7b88995777-ft5pw                   1/1                  Running            0                           4h15m # kubectl get deployments NAME                READY           UP-TO-DATE          AVAILABLE         AGE Demoweb           3/3                  3                               3                           26d
Wählen Sie wie in Listing 4 eine ausführlichere Ausgabe, so erscheint auch die Information, auf welchem Knoten des Clusters das Pod läuft und unter welcher IP-Adresse der Container auf Port 80 erreichbar ist. Am Output in Listing 4 ist auch ersichtlich, dass die IP-Adresse 192.168.13.66 zum Host Debkub3 (dieser hat im Demo-Aufbau die IP-Adresse 10.10.1.243) geroutet werden muss. Der Bereich, aus dem die IP-Adressen für die Container kommen, lässt sich selbstverständlich frei konfigurieren. In der Default-Installation kommt das Subnetz 192.168. 0.0/16 zum Einsatz und jeder Cluster-Host erhält daraus ein /26-Subnetz. Auch die Größe des Subnetzes ist einstellbar.
Listing 4: Erweiterter get-pods-Output
# kubectl get pods -o wide NAME                                          READY           STATUS           RESTARTS          AGE          IP                             NODE           NOMINATED NODE          READINESS GATES demoweb-7b88995777-ft5pw      1/1                  Running            0                          4h21m       192.168.13.66         debkub3         <none>
Loadbalancing mit Kubernetes-Bordmitteln
Kubernetes erlaubt es, Dienste in Eigenregie zu loadbalancen. Beim Anlegen eines Kubernetes-Services vom Typ "NodePort" geben Sie einen Port an, auf dem der Dienst von außen auf den physischen Nodes ansprechbar ist, der dann an einen der laufenden zum Dienst gehörenden Container weitergeleitet wird. Spricht ein Benutzer des Dienstes nun von außen einen der zum Cluster gehörenden Nodes auf diesem Port an, leitet Kubernetes über das Kubernetes-Overlay-Netzwerk die Pakete weiter. Dies geschieht allerdings rein in Software und wenn von außen nur ein Knoten verfügbar ist, kann dieser zum Flaschenhals werden.
Konfiguration der Firewall
Auf der Firewall – unser Demo-Aufbau lief mit der Softwareversion FortiOS 6.4.6. – sind nun die folgenden Arbeitsschritte durchzuführen:
- Konfiguration des SDN-Connectors, damit die Firewall Konfigurationsdaten aus dem Kubernetes-Cluster auslesen kann.
- Anlegen der BGP-Peers für alle Cluster-Knoten.
- Erzeugen einer "Dynamic IP", um die IP-Adressen der Pods des Deployments zu erfassen.
- Erstellen eines "Health Check", der überprüft, ob die Pods einsatzbereit sind.
- Erzeugen eines "Virtual Servers", der im Backend an die Pod-IP-Adressen weiterleitet und den im Schritt zuvor angelegten Health Check nutzt.
- Anlegen einer Regel in der Policy, die den Zugriff auf den virtuellen Server zulässt.
Die Konfiguration eines SDN-Connectors vom Typ "Kubernetes" benötigt zwei Parameter: Die IP-Adresse des Kubernetes-Masters sowie ein Secret-Token. Letzteres ist eine Möglichkeit des Logins zu einem Kubernetes-Cluster. Das Token gehört zu einem Service-Account, dem Sie die Rechte wiederum über ein Objekt vom Typ namens "ClusterRoleBinding" zuweisen. Die YAML-Datei in Listing 5 legt einen Admin mit vollen (auch schreibenden) Zugriffsrechten an. In produktiven Umgebungen reicht ein Read-Only-Nutzer, hierzu müssen Sie eine entsprechende Rolle erzeugen.
Listing 5: Service-Account mit ClusterRoleBinding
apiVersion:  v1 kind: ServiceAccount metadata:       name: myadmin-user       namespace: kube-system apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata:       name: myadmin-user roleRef:       apiGroup: rbac.authorization.k8s.io       kind: ClusterRole       name: cluster-admin subjects: - kind: ServiceAccount       name: name: myadmin-user       name: namespace: kube-system
Nach Anwendung dieser Datei mittels kubectl -f erhalten Sie den Namen des Tokens mit dem Kommando kubectl -n kube-system describe serviceaccount myadmin-user. Der Output enthält dann die Zeile:
Tokens: myadmin-user-token-<XXXX>
Den Inhalt des Tokens zeigt das Kommando kubectl -n kube-system describe secret myadmin-user-token-<XXXX>. Der Token produziert ein mehrere Zeilen langes Secret, das Sie am besten per Copy&Paste in die Konfiguration der Firewall eintragen. In der GUI der Appliance finden sich die "External Connectors" unter dem Hauptmenüpunkt "Security Fabric". Bild 1 zeigt den Dialog zum Anlegen des Kubernetes-Connectors. War die Konfiguration korrekt, erscheint in der Übersicht der Connectoren ein kleiner grüner Pfeil im Icon für das neu angelegte Objekt.
Bild 1: Der Dialog zum Anlegen des SDN-Connectors.
Als BGP-Peers müssen Sie nun die Cluster-Mitglieder anlegen. Auf der FortiGate-CLI sollten Sie dazu zunächst das eigene AS definieren, der Einfachheit halber kommt ebenfalls 64512 zum Einsatz. Als Router-ID geben wir die IP-Adresse an, die wir auch Calico bekanntgegeben haben und schließlich tragen wir die drei Nachbarn ein. Listing 6 zeigt die Konfiguration.
Listing 6: BGP-Konfiguration
config router bgp       set as 64512       set router-id 10.10.1.134 config neighbor             edit "10.10.1.213"                   set remote-as 64512             next             edit "10.10.1.243"                   set remote-as 64512             next             edit "10.10.1.173"                   set remote-as 64512             next       end end
Der nächste Schritt ist das Anlegen des Adressobjekts. Dieses befindet sich in der GUI unter dem Hauptpunkt "Policy & Objects". Unter "Addresses / Create New" erzeugen Sie eine neue Adresse. Nach Auswahl von "Dynamic" im Attribut "Type" erscheint der Subtyp ("Fabric Connector Address") und dort wählen Sie aus, welcher Fabric Connector die Adressdaten liefern soll. Nach Anklicken des vorher erzeugten Kubernetes-Connectors erscheint ein Default-Filter, wie in Bild 2 dargestellt.
Bild 2: Auch eine dynamische Adresse lässt sich über den entsprechenden Dialog anlegen.
Der Default-Filter, so wie er dort steht, würde jedoch alle Objekte im Namespace "Default" auflösen. Dies ist aber nicht unser Ziel. Hier kommt das Label "app=httpbin" zum Einsatz, da der SDN-Connector es erlaubt, auch Kubernetes-Labels als Filter zu verwenden. Hierzu klicken Sie auf den Filter und wählen die Zeile "K8S_Label. app=httpbin" aus. Die Interface-Zuordnung des Adressobjektes lässt sich jetzt noch auf das Interface einschränken, das aus Sicht der Firewall zu den Kubernetes-Knoten zeigt. Ist dies nicht eindeutig, belassen Sie es auf dem Wert "any".
Nach einem Klick auf den OK-Button dauert es einen kurzen Moment, bis die Firewall die Parameter abgerufen hat. So lange erscheint ein kleines rotes Ausrufungszeichen in der Übersicht der Adressobjekte. Nachdem die Auflösung erfolgt ist, verschwindet dieses. Bewegen Sie den Mauszeiger über das Objekt, taucht eine Infobox wie in Bild 3 auf. Sie zeigt, zu welchen Adressen sich das dynamische Objekt auflösen lässt.
Bild 3: Die Definition der Firewallregel – wichtig ist, den "Inspection Mode" auf "Proxy-based" zu setzen.
Im nächsten Gang erzeugen Sie einen "Health Check", was ebenfalls über den Bereich "Policy & Objects" funktioniert. Ein Objekt dieses Typs beschreibt, wie, aber nicht wer getestet wird. Da es sich um einen Webserver handelt, ist der Typ des Checks "HTTP". Anpassen müssen Sie den Port (80) und die URL ( / ). Das Prüfintervall setzen Sie entsprechend Ihrer Verfügbarkeitsanforderungen.
Der vorletzte Schritt ist der "Virtual Server". In der GUI müssen Sie dieses Feature erst aktivieren, da es sonst nicht zur Verfügung steht. Auf der CLI befindet sich die Konfiguration unter der Hierarchie "firewall vip" und unterscheidet sich von einer normalen Virtual-IP durch den Typ "server-load-balance". Bild 3 zeigt die Konfiguration.
Die Virtual-Server-IP ist die IP-Adresse, unter der der Dienst erreichbar ist. Als Port ist für HTTP 80 eingegeben. Für HTTP-Dienste können Sie Cookies für eine Session-Persistence einsetzen. Unter "Health Check" fügen Sie den zuvor eingetragenen Check ein. Als Nächstes folgt die Liste der realen Server. Hier würden normalerweise statische Server eingetragen. In unserer Konfiguration nutzen wir jedoch das dynamische Adressobjekt, das die Adressen aus der aktuellen Kubernetes-Konfiguration bezieht.
Der letzte Arbeitsschritt ist eine Firewallregel, die den Virtual Server als Zieladresse verwendet. Die Regel erlaubt das HTTP-Protokoll von beliebigen Quellen. Zusätzlich sind aus den Unified-Thread-Management-Funktionen der Firewall das Intrusion Prevention System und die Web Application Firewall aktiviert, um die Webcontainer zu schützen. Bild 3 zeigt die Definition der Regel. Dabei ist zu beachten, dass der "Inspection Mode" auf "Proxy" gestellt ist, da sonst der Virtual Server nicht als Ziel zum Einsatz kommen kann.
Link-Codes
[2] Aktuelles Release von calicoctl: https://github.com/projectcalico/calicoctl/releases/
Dynamik berücksichtigen
Bis hierhin zeigt der Loadbalancer auf einen Zielcontainer und die ganze Konfiguration ist zwar dynamisch, gestaltet sich aber aufwendiger, als das Ganze statisch zu konfigurieren und auf der Firewall eine Route einzutragen. Zum Tragen kommt dies erst, wenn Sie etwa die Menge der Container nach oben skalieren. Mit dem Kommando kubectl scale deployment. v1.apps/demoweb --replicas=3 erhöhen Sie die Anzahl der laufenden Container auf 3. Mit kubectl get pods -o wide lässt sich der Erfolg überprüfen, die Ausgabe sollte so aussehen wie in Listing 7.
Listing 7: Korrekte Ausgabe für drei laufende Container
# kubectl get pods -o wide NAME                                               READY           STATUS           RESTARTS          AGE          IP                             NODE            NOMINATED NODE          READINESS GATES demoweb-7b88995777-4chsr           1/1                  Running            0                          2m36s       192.168.212.66         debkub2         <none>                               <none> demoweb-7b88995777-n6nxn          1/1                  Running            0                          2m36s       192.168.13.67          debkub3         <none>                               <none> demoweb-7b88995777-wt8mw        1/1                  Running            0                          23h            192.168.212.65        debkub2         <none>                               <none>
Verharren Sie mit dem Mauszeiger über dem Adressobjekt der Container, bringen Sie die Adressauflösung in der GUI auf den Bildschirm.
Fazit
Unser Workshop nutzte eine FortiGate-Firewall, um Daten weiterzuleiten und per Mausklick fortgeschrittene Sicherheitsfunktionen zuzuschalten. Dies macht das sichere und performante Anbieten von Diensten auf Basis von Kubernetes mit geringem Aufwand möglich. Calico bietet zwar selbst bereits einige Optimierungen im Netzwerkverkehr, jedoch fehlt hier die Möglichkeit, UTM-Features in den Datenstrom zu schalten.
(ln)