Kubernetes AppOps Security Teil 1: Network Policies einsetzen (1/2) – Grundlagen und Good Practices
Das Netzwerkmodell von Kubernetes mag für viele ungewöhnlich erscheinen: Es fordert ein flaches, nicht hierarchisches Netzwerk, in dem alle (Nodes, Pods, Kubelet, etc.) mit allen kommunizieren können. Der Datenverkehr im Cluster ist also standardmäßig nicht eingeschränkt, auch nicht zwischen Namespaces. Traditionelle SysAdmins fragen an dieser Stelle vergebens nach Netzwerksegmenten und Firewalls.
Dafür bietet Kubernetes die Network Policy
Ressource, um Netzwerkverbindungen einzuschränken. In einer Network Policy werden Regeln für ein- und ausgehenden Datenverkehr auf den ISO/OSI-Schichten 3 und 4 (IP-Adressen und Ports) deklariert und diese Regeln mittels Labels einer Gruppe von Pods zugeordnet. Die Regeln werden anschließend durch das eingesetzte Container Networking Interface (CNI) Plugin (beispielsweise Calico, Cilium) von der Kubernetes API abgefragt und durchgesetzt.
Der Datenverkehr eines Pods ist also nicht eingeschränkt, wenn
- keine Network Policies vorliegen,
- der Pod kein Label hat, das auf eine Network Policy passt oder
- das verwendete CNI-Plugin Network Policies nicht implementiert.
Was die Sicherheit angeht, ist dieses Standardverhalten nicht ideal. Insbesondere dann, wenn mehrere Anwendungen, Teams oder Mandanten auf einem Cluster betrieben werden, kann eine einzige verwundbare Anwendung ein Einfallstor bieten. Abbildung 1 zeigt ein Beispiel: Ein Angreifer verschafft sich über die verwundbare Anwendung von „Team B” die Kundendaten der Datenbank von „Team A”. Diese ist zwar nicht direkt übers Internet erreichbar aber fehlkonfiguriert und erfordert keine Authentifizierung. Fälle wie diese sind nicht akademisch! Beispielsweise wurden MongoDB-Instanzen tausendfach ohne Authentifizierung und sogar direkt übers Internet erreichbar betrieben.
Auch der ausgehende Datenverkehr kann eine Gefahr darstellen. Beispielsweise könnte ein kompromittierter Host versuchen, eine Verbindung zu einem Command-Server aufzubauen. Sind keine ausgehende Verbindungen möglich, wird dies unterbunden.
Generelle Möglichkeiten und Syntax
Network Policies bieten die Möglichkeit Datenverkehr einzuschränken und damit die Auswirkungen eines Angriffs zu begrenzen. Dabei kann der eingehende (ingress
) und/oder ausgehende (egress
) Datenverkehr für bestimmte Pods, ganze Namespaces oder IP-Adressbereiche (nur egress) freigegeben werden. Optional kann dies auf Ports eingeschränkt werden.
Wie bei Kubernetes üblich, wird der gewünschte Zustand des Clusters deklarativ in YAML festgelegt. Ein einfaches Beispiel für eine Network Policy zeigt Listing 1. Die Zuordnung der Pods wird lose gekoppelt per Label vorgenommen. Die gilt sowohl für die Pods auf welche die Network Policy angewendet wird (podSelector
), als auch bei der Spezifikation der Regeln (unterhalb von podSelector
, im Beispiel ab ingress
).
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: db-allow
namespace: team-a
spec:
podSelector:
matchLabels:
db: a
ingress:
- from:
- podSelector:
matchLabels:
app: a
Listing 1: Einfache Network Policy, die den ungewollten Zugriff aus Abbildung 1 unterbindet
Da der Datenverkehr innerhalb eines Clusters ohne Network Policies komplett uneingeschränkt ist, muss zunächst eine Network Policy angewendet werden, die alle Verbindungen unterbindet (deny-all Policy, siehe Listing 2). Anschließend können dann die gewünschten Verbindungen über weitere Network Policies erlaubt werden (whitelisting). Die Network Policy in Listing 1 wird auf alle Pods mit dem Label „db: a” angewendet und erlaubt dort nur eingehenden Datenverkehr von Pods die das Label „app: a” haben. Generell gelten Network Policies für Pods innerhalb des Namespaces in dem sie angewendet werden (in Listing 1 „team-a”). Trotzdem ist es möglich, ingress
- oder egress
-Regeln zu oder von anderen Namespaces zu spezifizieren (dazu mehr im nächsten Abschnitt).
Das Lesen der Syntax ist zu Beginn etwas gewöhnungsbedürftig. Den Start in das Thema erleichtern die „Network Policy Recipes”: Hier werden Network Policy-Beispiele von einfachen bis hin zu komplexen Anwendungsfällen mit Hilfe von animierten Gifs anschaulich erklärt. Einzelne Pods zu schützen, wie in Listing 1, ist ein Anfang. Wie aber kann bei größeren Clustern und komplexen Anwendungslandschaften sichergestellt werden, dass keine unerwünschten Netzwerkzugriffe stattfinden? Ein Lösungsansatz dazu wird im Folgenden anhand von Beispielen gezeigt. Wer diese selbst in einer definierten Umgebung ausprobieren will, findet vollständige Beispiele mit Anleitung im Repository „cloudogu/k8s-security-demos” bei GitHub.
Mit Networkpolicies kann man generell eingehenden und ausgehenden Netzwerkverkehr (Ingress, Egress) erlauben oder verbieten. Hier stellt sich die Frage womit man beginnt. Ein sinnvoller erster Schritt ist das Whitelisting des eingehenden Datenverkehrs von Anwendungen. In späteren Schritten beschreibt das Buch die Einschränkung von ausgehendem Datenverkehr und speziellen Namespaces wie kube-system
und Operator-Namespaces.
Whitelisting von eingehendem Datenverkehr
Ein sinnvoller erster Schritt ist das Whitelisting des eingehenden Datenverkehrs.
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: ingress-default-deny-all
namespace: team-a
spec:
podSelector: {}
ingress: []
Listing 2: Network Policy, die jegliche Kommunikation zu Pods eines Namespaces verbietet
Dies lässt sich durch eine Network Policy pro Namespace abbilden, die alle Pods eines Namespaces selektiert und jeglichen eingehenden Datenverkehr verbietet. Listing 2 zeigt wie es geht: Alle Pods werden selektiert, aber keine erlaubten ingress
-Regeln beschrieben. Danach sind keine Zugriffe auf Pods innerhalb des Namespaces möglich, außer sie wurden explizit erlaubt, wie Listing 1 zeigt.
Wird dies auf jeden Namespace angewendet (zunächst mit Ausnahme von kube-system
, dazu mehr im nächsten Abschnitt), können nur noch explizit freigegebene Verbindungen zwischen Pods im Cluster aufgebaut werden. Dieses Vorgehen zeigt große Wirkung bei überschaubarem Aufwand.
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: ingress-allow-traefik-to-access-app-a
namespace: team-a
spec:
podSelector:
matchLabels:
app: a
ingress:
- ports:
- port: 8080
from:
- namespaceSelector:
matchLabels:
namespace: kube-system
podSelector:
matchLabels:
app: traefik
Listing 3: Network Policy, die eingehenden Datenverkehr vom Ingress Controller erlaubt
Nicht vergessen werden sollte allerdings das Whitelisting des Datenverkehrs von
- Ingress Controller (beispielsweise NGINX oder Traefik), da sonst keine eingehenden Anfragen mehr möglich sind (Listing 3 zeigt Whitelisting für Traefik), sowie
- der Observability-Infrastruktur, da beispielsweise Anwendungsmetriken durch Prometheus (Listing 4) sonst nicht mehr abgeholt werden.
Beim Selektieren von Namespaces ist zu beachten, dass diese standardmäßig keine Label haben. Damit beispielsweise Listings 3 greift, muss dem Namespace kube-system
ein Label namespace: kube-system
hinzugefügt werden. Der Vorteil des Zuordnens auf Basis von Labels statt Namen ist, dass ein Label auch an mehrere Namespaces vergeben werden kann. Damit ist es beispielsweise möglich eine Network Policy auf alle produktiven Namespaces anzuwenden.
Die gleichzeitige Anwendung von namespaceSelector
und podSelector
, wie in Listing 3 und 4 verwendet, ist erst ab Kubernetes Version 1.11 möglich.
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: ingress-allow-prometheus-to-access-db-a
namespace: production
spec:
podSelector:
matchLabels:
db: a
ingress:
- ports:
- port: 9080
from:
- namespaceSelector:
matchLabels:
namespace: monitoring
podSelector:
matchLabels:
app: prometheus
component: server
Listing 4: Network Policy, die Prometheus das Monitoring einer Anwendung erlaubt
Für Fortgeschrittene: kube-system
Namespace
Wie erwähnt, ist beim Whitelisting des Kubernetes eigenen kube-system-Namespace und der möglicherweise vorhandenen anderen Operator-Namespaces (Ingress-Controller, Prometheus, Velero u.a.) mehr zu beachten als bei anderen Namespaces in denen gewöhnliche Anwendungen laufen.
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: allow-kube-dns-all-namespaces
namespace: kube-system
spec:
podSelector:
matchLabels:
k8s-app: kube-dns
ingress:
- ports:
- protocol: UDP
port: 53
from:
- namespaceSelector: {}
Listing 5: Network Policy, die Zugriffe auf den DNS-Service von kube-dns erlaubt
Wenn jeglicher eingehender Datenverkehr im kube-system
Namespace unterbunden wird, betrifft dies unter anderem
- die Namensauflösung mit dem DNS-Addon (beispielsweise kube-dns)
- und den eingehenden Datenverkehr von außen auf den Ingress Controller, der häufig in diesem Namespace betrieben werden.
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: ingress-allow-traefik-external
namespace: kube-system
spec:
podSelector:
matchLabels:
app: traefik
ingress:
- ports:
- port: 80
- port: 443
from: []
Listing 6: Network Policy, die HTTP-Zugriffe auf den Traefik Ingress Controler erlaubt
Wie DNS Zugriff auf die kube-dns Pods generell freigegeben werden kann, zeigt Listing 5: UDP Datenverkehr auf Port 53 darf von allen Namespaces zu den Pods stattfinden. Listing 6 zeigt ein Beispiel für den HTTP reverse Proxy Traefik, auf dessen Ports 80 (hier findet in der Regel ein Redirect auf 443 statt) und 443 Zugriffe ohne Angabe eines konkreten from
erlaubt werden.
Für Fortgeschrittene: Ausgehenden Datenverkehr einschränken
Wenn in allen Namespaces Whitelisting des eingehenden Datenverkehrs realisiert ist, können innerhalb des Clusters keine unerlaubten Zugriffe mehr erfolgen: Der ausgehende Datenverkehr eines Pods ist gleichzeitig der eingehende Datenverkehr eines anderen Pods, der dem Whitelisting unterliegt.
Es gibt allerding noch weiteren ausgehenden Datenverkehr, der einen genaueren Blick erfordert: Datenverkehr aus dem Cluster heraus, in umliegende Netze wie das Internet oder Firmennetz. Viele Anwendungen müssen nicht mit der Welt außerhalb des Clusters kommunizieren oder nur mit ausgewählten Hosts. Beispiele für erlaubte Zugriffe sind das Laden von Plugins (Jenkins, SonarQube, etc.) oder Zugriff auf Authentifizierungsinfrastruktur (OAuth Provider wie Facebook, GitHub, etc. oder LDAP/Active Directory in Firmen). Wie oben beschrieben, kann eine Verbindung ins Internet von Angreifern ausgenutzt oder ein Zugriff aufs Firmennetz als Einfallstor auf weitere Ziele verwendet werden. Dies kann seit Kubernetes Version 1.8 durch Einschränkung des egress
-Datenverkehrs verhindert werden.
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: egress-allow-internal-only
namespace: team-a
spec:
policyTypes:
- Egress
podSelector: {}
egress:
- to:
- namespaceSelector: {}
Listing 7: Network Policy, die ausgehenden Datenverkehr nur innerhalb des Clusters erlaubt
Auch hier ist es möglich das vom ingress
bekannte Whitelisting-Verfahren anzuwenden. Allerdings müssten dann alle ingress
-Regeln für den internen egress
-Datenverkehr wiederholt werden. Pragmatischer ist es daher, mit einer Network Policy pro Namespace nur ausgehenden Datenverkehr innerhalb des Clusters freizugeben und anschließend einzelnen Pods den Zugriff nach außen zu erlauben. Listing 7 zeigt eine Network Policy, die dies realisiert, indem sie auf alle Pods angewendet wird und egress
auf alle Namespaces erlaubt. Alles außerhalb des Pod-Netzwerks hat keinen Namespace und wird daher abgelehnt.
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: egress-allow-all-egress
namespace: team-a
spec:
policyTypes:
- Egress
podSelector:
matchLabels:
app: a
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
Listing 8: Network Policy, die jeglichen ausgehenden Datenverkehr für bestimmte Pods erlaubt
Das Whitelisting erfolgt dann, ähnlich wie beim ingress
, durch weitere Network Policies. Beim egress
werden jedoch IP-Adressblöcke mittels CIDR-Notation (Classless Inter-Domain Routing) freigegeben. Ein Beispiel, welches jeglichen Datenverkehr für bestimmte Pods erlaubt, zeigt Listing 8. Hier kann durch das Zulassen ausgewählter IP-Adressen oder -bereiche auch noch feiner justiert werden.
Beim Whitelisting des egress
gilt es Cloud-native Anwendungen die direkt mit der Kubernetes-API sprechen nicht zu vergessen. Das IP-Matching des API-Servers ist herausfordernd. Hier muss die IP-Adresse des Endpunkts und nicht die des Services angegeben werden. Diese IP-Adresse kann mit folgendem Befehl ermittelt werden: „kubectl get endpoints --namespace default kubernetes”. Die ermittelte IP-Adresse kann in eine Networkpolicy wie in Listing 8 (x.x.x.x/32 anstatt 0.0.0.0/0) eingesetzt werden um den Zugriff auf den API-Server zu erlauben.
Fazit und Empfehlung
Dieser Artikel beschreibt, wie mit überschaubarem Aufwand das Kubernetes-Bordmittel Network Policies eingesetzt werden kann, um eine solide weitere Verteidigungsschicht im Cluster einzuziehen.
Allerdings ist die Lernkurve nicht flach. Wer daher mit minimalem Aufwand die Sicherheit erhöhen möchte, dem sei mindestens das Whitelisting des ingress
-Datenverkehrs in allen Namespaces außer kube-system
geraten. Damit wird eine gewisse Isolierung der Namespaces erreicht. Außerdem entsteht zusätzlich ein guter Überblick darüber, wer im Cluster mit wem kommuniziert. Network Policies für kube-system
-Namespace und egress
-Datenverkehr sind zwar aufwändiger, erhöhen jedoch die Sicherheit noch deutlicher und sind daher auch empfehlenswert.
Einen Einstieg zum selbst ausprobieren bietet das Repository „cloudogu/k8s-security-demos”, in dem ein Cluster sukzessive mit dem in diesem Artikel beschriebenen Vorgehen abgesichert wird. Bei der Anwendung von Network Policies gibt es natürlich Fallstricke, die zusammen mit einigen praktischen Tipps zu Test und Debugging im nächsten Teil dieser Artikelserie thematisiert werden.
Tags