featured image Kubernetes AppOps Security: Network Policies Teil 1 - Good Practices

29.10.2019 von Johannes Schnatterer in Software Craftsmanship

Kubernetes AppOps Security: Network Policies Teil 1 - Good Practices

+++Der Originalartikel kann hier heruntergeladen werden: Zeitschriftenartikel, veröffentlicht in JavaSPEKTRUM 05/2019.+++

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.

Unerlaubter Netzwerkzugriff, der mittels Network Policies verhindert werden kann

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”).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

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.

Whitelisting von eingehendem Datenverkehr

Ein sinnvoller erster Schritt ist das Whitelisting des eingehenden Datenverkehrs.

1
2
3
4
5
6
7
8
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 „kube-system” Namespace mehr zu beachten als bei anderen Namespaces.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.

1
2
3
4
5
6
7
8
9
10
11
12
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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. Da der API-Server nicht im Pod-Netzwerk liegt, wird er vom „namespaceSelector” nicht getroffen. Für Pods, die dies benötigen, bietet sich auf den ersten Blick an, den gesamten internen Adressbereich des Clusters per Whitelisting freizugeben, beispielsweise mittels CIDR „10.0.0.0/8”. In der Umsetzung zeigt sich jedoch, dass „egress” erst nachträglich zu den Network Policies hinzugefügt wurde und offensichtlich noch nicht den gleichen Reifegrad hat: Im CNI-Plugin Calico funktioniert dies beispielsweise nicht. Um das Problem zu umgehen, bleibt nur den gesamten Datenverkehr für diese Pods freizugeben, wie in Listing 8 gezeigt.

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.


Johannes Schnatterer
Johannes Schnatterer

- Solution Architect -

Johannes ist Continuous Delivery Enthusiast, fokussiert auf Software Qualität, hat einen ausgeprägten Open Source Enthusiasmus und ist überzeugt, dass prägnante Dokumentation entscheidend sein kann.