Erst durch beide Maßnahmen erreicht das Geflecht an Services einen Securitystandard, der bei einem monolithischen System sehr viel einfacher zu erreichen ist. Der administrative Umgang mit den Zertifikaten für die Transport Layer Security (TLS) kann bei einem verteilten System einen recht erheblichen Aufwand verursachen.
Allein schon durch die hohe Anzahl an eigenständigen Services erhöht sich die Wahrscheinlichkeit einer Sicherheitslücke. Sobald neue sogenannte Common Vulnerabilities and Exposures (CVEs) veröffentlicht werden, müssen diese unter Umständen in mehreren Services zeitnah ausgebessert werden. Erst wenn alle Microservices anschließend neu deployt wurden, gilt dieses Sicherheitsrisiko als behoben. Ein möglicher Angreifer hat im Grunde die Qual der Wahl, welchen der vielen Services er als Erstes zu kompromittieren versuchen soll. Sollte der Angriffsversuch auf den ersten Microservice nicht zum Erfolg führen, hat er noch genügend andere Opfer, über die er in das Geflecht der Microservices eindringen kann. Werden die Services über eine ungesicherte Verbindung aufgerufen, so ergeben sich für den Hacker noch mehr Möglichkeiten, in das Gesamtsystem einzubrechen.
Zero Trust
Werden die zuvor genannten Securityprobleme bei Microservices konsequent zu Ende gedacht, kommt man im Grunde zu dem Ergebnis, dass nur ein Zero-Trust-Ansatz einen ausreichenden Sicherheitsschutz bieten kann.
Zero Trust besagt, dass im Grunde keinem (Micro-)Service vertraut werden darf, sogar dann nicht, wenn er sich in einer sogenannten Trusted Zone befindet. Jeder Request zwischen den Services muss authentifiziert (AuthN) und autorisiert (AuthZ) werden und mittels TLS abgesichert sein. Als Authentifikationsmerkmal wird oft ein JSON Web Token (JWT) verwendet, das bei jedem Request mitgeschickt werden muss und somit den Aufrufer identifiziert. Dieses JWT wird entweder End-to-End verwendet, d. h., dass es während der gesamten Aufrufkette nicht ausgetauscht wird, oder man generiert sich mit einem TokenExchangeService für jeden einzelnen Request ein eigenes neues Token. Zu guter Letzt soll die Absicherung nicht nur auf der HTTP- oder gRPC-Ebene stattfinden (OSI Layer 7 Application), sondern auch in den darunter liegenden Netzwerkschichten Transport und Network (OSI Layer 4 und 3). Damit wird dem OWASP-Securityprinzip [1] Defense in Depth genüge getan.
Nicht jeder sicherheitsverantwortliche Mitarbeiter will diesen finalen Schritt gehen, da klar erkennbar ist, dass die Umsetzung von Zero Trust nicht gerade trivial ist. In der Gegenüberstellung Aufwand gegen Nutzen wird dabei oft der (falsche) Schluss gezogen, dass für Zero Trust der Aufwand viel zu hoch wäre, obwohl einem die innere Securitystimme sagt, dass Zero Trust der richtige Ansatz ist. Mittlerweile gibt es jedoch Systeme, die den Aufwand für Zero Trust sehr stark minimieren, womit der Nutzen die Oberhand gewinnt.
Zero Trust bei Microservices
Als Konsequenz der vorherigen Überlegungen müssen für Zero Trust bei Microservices mehrere Funktionalitäten umgesetzt werden. Jeder Request muss Informationen über den Aufrufer enthalten. Am einfachsten gelingt das mit einem JWT, das im HTTP-Header Authorization als Typ Bearer mitgeschickt wird. Der aufgerufene Service validiert dieses JWT und führt dann die notwendigen Berechtigungsprüfungen durch. Bei Verwendung eines Service-Mesh-Tools kann diese Prüfung auch von einem Sidecar übernommen oder ergänzt werden (dazu mehr in den folgenden Abschnitten). Zur Absicherung der Kommunikation mit TLS sind SSL-Zertifikate notwendig, die die Eigenschaft haben, dass sie nach einem gewissen Zeitintervall ungültig werden und somit ausgetauscht werden müssen. Das kann wegen der hohen Anzahl an Microservices nicht manuell erfolgen. Für diese administrative Aufgabe bieten Service-Mesh-Tools eine passende Automatisierung an, wodurch der Aufwand für die Ops-Kollegen gegen Null geht.
Um jetzt noch dem OWASP-Securityprinzip Defense in Depth gerecht zu werden, sollten die Kommunikationsverbindungen zwischen den Microservices mit Firewallregeln berechtigt oder unterbunden werden. Somit erfolgt eine weitere Absicherung der Kommunikation auf TCP/IP-Ebene. Kubernetes sieht dafür das Konzept der Network Policies vor. Auch hierzu mehr in den kommenden Abschnitten.
Typischer Ausgangspunkt
Die meisten Microservices-Projekte, die auf Kubernetes betrieben werden, starten mit der Ausgangssituation in Abbildung 1.
Abb. 1: Low Secure Deployment
Für die Ingress-Kommunikation stellen die Cloud-Betreiber passende Komponenten zur Verfügung oder beschreiben mit Tutorials, wie diese Komponenten einfach installiert werden können. Der Kommunikationspfad aus dem Internet geht meist über eine Firewall und wird mit einem LoadBalancer in Richtung Kubernetes-Cluster geroutet. Dafür stellen die Cloud-Provider oft fertige Lösungen zur Verfügung, die auch sehr einfach auf TLS umgestellt werden können.
Jetzt beginnt der Teil, bei dem man eigenverantwortlich weitere Maßnahmen zur Sicherheit umsetzen muss. Ein Ingress-Controller innerhalb Kubernetes kümmert sich dann um die Weiterleitung des Requests innerhalb des Clusters. Dieser wird typischerweise als SSL-Endpunkt mit einem firmeneigenen Zertifikat ausgestattet. Als Folge davon werden die Requests nach der SSL-Terminierung ohne TLS an die jeweiligen Microservices weitergeleitet, d. h., die Kommunikation innerhalb des Clusters erfolgt komplett ohne Absicherung. Manche der Microservices (oder doch schon alle?) besitzen eine Securityprogrammierung, die das empfangene JWT validiert und anschließend mit den enthaltenen Claim-Werten die Berechtigungsprüfung durchführt. Zur Validierung des JWT müssen (sporadisch) Requests zum Identity Provider (IDP), der das JWT ausgestellt hat, abgeschickt werden. Das erfolgt in der Regel mit TLS, da der IDP nur einen HTTPS-Zugang anbietet. Die interne Aufwand-Nutzen-Analyse hat ergeben, dass mit den vorhandenen Mitteln bei vertretbarem Aufwand ein gewisser Grad an Security erreicht worden ist. Das Projektteam ist sich bewusst, dass dieses Setting keinem Zero-Trust-Ansatz entspricht, glaubt aber, dass der Aufwand für Zero Trust viel zu hoch ist. Mangels besseren Wissens gibt man sich mit diesem (geringen) Level an Security zufrieden.
Service Mesh
Ein sehr viel höheres Securitylevel kann man mit Service-Mesh-Tools wie Istio oder Linkerd (u. v. m.) erreichen. Diese Tools bieten hierfür entsprechende Funktionalitäten an, die zum Beispiel das Zertifikatsmanagement automatisieren. Am Beispiel von Istio sollen diese Features genauer betrachtet werden.
Mutual TLS
Jeder Service, der Bestandteil eines Service Mesh wird, bekommt ein sogenanntes Sidecar, das die eingehende und ausgehende Kommunikation zum Service steuert und überwacht. Gesteuert wird das Verhalten des Sidecar über eine zentrale Steuerungskomponente, die dem Sidecar die passenden Informationen und Anweisungen übermittelt. Beim Start des Pods, der aus dem Service und dem Sidecar besteht, holt sich das Sidecar ein individuelles SSL-Zertifikat von der zentralen Steuereinheit ab. Damit ist das Sidecar in der Lage, eine Mutual-TLS-Verbindung (mTLS) zu etablieren. Nur der Request vom Sidecar zum Service, also die Kommunikation innerhalb des Pods, erfolgt dann ohne TLS. Nach einem vordefinierten Zeitintervall (bei Istio ist der Default 24 h), lässt sich das Sidecar automatisch ein neues Zertifikat vom Steuerungsservice ausstellen. Dieses neue Zertifikat wird dann für die nächsten 24 Stunden für die mTLS-Verbindung verwendet. Mit der in Listing 1 gezeigten Istio-Regel wird eine mTLS-Kommunikation für das gesamte Service Mesh verpflichtend.
apiVersion: security.istio.io/v1beta1 kind: PeerAuthentication metadata: name: default namespace: istio-system spec: mtls: mode: STRICT
Für eine Übergangslösung, in der noch nicht alle Services in den Service Mesh integriert wurden, gibt es einen speziellen Modus: PERMISSIVE. Damit werden Services innerhalb des Service Mesh mit mTLS angesprochen und Services, die außerhalb des Service Mesh laufen, werden weiterhin ohne TLS aufgerufen. Auch das erfolgt automatisch und wird vom jeweiligen Sidecar, das den Aufruf initiiert, entsprechend ausgeführt.
Der Vorteil für die Security liegt hier auf der Hand. Die Kommunikation innerhalb des Service Mesh erfolgt über mTLS, und das Zertifikatsmanagement läuft in kurzen Zeitintervallen vollautomatisch ab. Darüber hinaus erfolgt das gesamte mTLS-Handling ohne einen Eingriff in den Code des Service, läuft also aus Sicht des Service völlig transparent ab. Diese Absicherung auf dem Application Layer des Netzwerkstacks kann dann ohne Probleme mit Absicherungen auf Layer 3 und 4 kombiniert werden (siehe kommende Abschnitte).
Ingress Gateway
Istio bietet auch ein sogenanntes Ingress Gateway an, das den Eintrittspunkt in den Kubernetes-Cluster und somit in das Service Mesh regelt. Es kann damit den Ingress Controller aus der vorherigen Systemlandschaft (Abb. 1) vollständig ersetzen.
Man könnte Istio auch ohne Ingress Gateway betreiben, würde dann aber eine Menge an Funktionalitäten verlieren. Der Vorteil des Ingress Gateway liegt darin, dass dort schon die gesamten Istio-Regeln greifen. Somit sind alle Regeln für Trafficrouting, Security, Releasing usw. anwendbar. Auch die Kommunikation vom Ingress Gateway zum ersten Service ist bereits mit mTLS abgesichert. Im Falle eines alternativen Ingress Controllers wäre dieser Request noch ohne SSL-Absicherung. Die Bezeichnung Gateway (anstatt Controller) wurde von Istio ganz bewusst gewählt, da mit dem existierenden Funktionsumfang das Ingress Gateway auch als sogenanntes API Gateway betrieben werden kann.
Für den gesicherten Eintritt in das Cluster kann das Ingress Gateway mit dem entsprechenden Firmenzertifikat konfiguriert werden, ganz analog zum Vorgehen bei einem Ingress Controller. Die Istio-Regel in Listing 2 definiert die Funktionsweise des Ingress Gateways.
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: mygateway spec: selector: istio: ingressgateway servers: - port: number: 443 name: https protocol: HTTPS tls: mode: SIMPLE credentialName: mytls-credential hosts: - myapp.mycompany.de
Es wird ein SSL-Port (443) für alle Requests auf den Hostnamen myapp.mycompany.de geöffnet und das zugehörige SSL-Zertifikat wird aus dem Kubernetes Secret mytls-credential ausgelesen. Die Erstellung des SSL-Secrets erfolgt mit der Kubernetes-Regel in Listing 3.
apiVersion: v1 kind: Secret metadata: name: mytls-credential type: kubernetes.io/tls data: tls.crt: | XYZ... tls.key: | ABc...
Damit ist auch hier der Eingang in den Cluster mit TLS abgesichert und die weiterführende Kommunikation erfolgt mit dem mTLS-Setting von Istio.
Network Policy
Nachdem die HTTP-Ebene mit SSL abgesichert ist, sollten nun noch weitere Netzwerkschichten (OSI Layer 3 und 4) abgesichert werden. Um in Kubernetes so etwas wie Firewalls zu etablieren, gibt es das Konzept der Network Policy. Diese Policies definieren die Netzwerkverbindungen zwischen den Pods, wobei die Einhaltung der Regeln von einem zuvor installierten Netzwerk-Plug-in durchgesetzt werden. Network Policies ohne ein solches Netzwerk-Plug-in haben keinen Effekt. Kubernetes bietet eine große Auswahl an Plug-ins [2] an, die man in einem Kubernetes-Cluster installieren kann.
Als Best Practice gilt es, eine sogenannte Deny-All-Regel zu definieren. Damit wird im gesamten Cluster die Netzwerkkommunikation zwischen den Pods unterbunden. Die Deny-All-Ingress-Regel verbietet jede eingehende Kommunikation auf Pods im zugehörigen Namespace (Listing 4).
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-ingress namespace: my-namespace spec: podSelector: {} policyTypes: - Ingress
Nachdem diese Regel aktiviert wurde, kann man nun gezielt die einzelnen Ingress-Verbindungen freischalten. Die Regel in Listing 5 gibt beispielsweise den Ingress-Traffic auf den Pod mit dem Label app=myapp frei, aber nur wenn der Request vom Ingress Gateway kommt.
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: access-myapp namespace: my-namespace spec: podSelector: matchLabels: app: myapp ingress: - from: - podSelector: matchLabels: istio: ingressgateway
Für jede weitere zulässige Verbindung muss die Regel entsprechend erweitert oder es müssen zusätzliche Regeln definiert werden. Durch die Etablierung der zuvor genannten Regeln hat sich das Anfangs-Deployment (Abb. 1) verändert, wie in Abbildung 2 gezeigt wird.
Abb. 2: Medium Secure Deployment
Die SSL-Terminierung wird nun vom Istio Ingress Gateway ausgeführt. Jeder weitergeleitete Request wird mit mTLS abgesichert. Zusätzlich definiert eine Network Policy je Pod den gewollten Ingress Request. Alle ungewollten Requests werden vom Network-Plug-in unterbunden. Damit ist schon mal ein großer Schritt in Richtung Zero Trust umgesetzt. Was jetzt noch fehlt, sind Berechtigungsprüfungen, die die Zulässigkeit der Aufrufe noch weiter eingrenzen.
Authentication (AuthN) und Authorization (AuthZ)
Für AuthN und AuthZ bietet Istio eine Menge an Regeln, mit denen man sehr granular steuern kann, welche Aufrufe berechtigt sind und welche nicht. Diese Regeln werden von den Sidecars und vom Ingress Gateway beachtet, wodurch der gesamte definierte Regelsatz überall im Service Mesh angewandt wird. Auch hiervon merkt die jeweilige Applikation nichts, da dies transparent von den jeweiligen Sidecars übernommen wird.
Die Authentifizierung erfolgt auf Basis eines JSON Web Tokens, das von Istio überprüft wird. Dazu werden die notwendigen JWT-Validierungen ausgeführt, wobei auf den ausstellenden Identity Provider (IDP) zugegriffen wird. Nach erfolgreicher Prüfung gilt der Request innerhalb des gesamten Service Mesh als authentifiziert. Am besten geschieht das im Ingress Gateway, womit die Prüfung gleich beim Eintritt in das Service Mesh bzw. Cluster ausgeführt wird.
Mit der Regel in Listing 6 wird Istio angewiesen, das empfangene JWT gegen den IDP mit dem URL unter [3] zu validieren.
apiVersion: security.istio.io/v1beta1 kind: RequestAuthentication metadata: name: ingress-idp namespace: istio-system spec: selector: matchLabels: istio: ingressgateway jwtRules: - issuer: "my-issuer" jwksUri: https://idp.mycompany.de/.well-known/jwks.json
Wie aus der Regel ersichtlich wird, sind die Issuer als Array zu definieren, d. h. es können auch mehrere unterschiedliche IDPs angegeben werden.
Damit gilt der Request zwar als authentifiziert, es finden aber noch keine Berechtigungsprüfungen statt. Diese müssen separat mit einem anderen Regeltyp angegeben werden. Ebenso wie bei der Network Policy gibt es auch hier eine Best Practice, mit der alle Zugriffe innerhalb des Service Mesh als nicht berechtigt deklariert werden. Dies kann mit der Regel in Listing 7 festgelegt werden.
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: allow-nothing namespace: istio-system spec: {}
Jetzt kann wie zuvor bei der Network Policy jeder einzelne Zugriff auf einen genau spezifizierten Pod sehr feingranular geregelt werden. Innerhalb der Regel kann in den Abschnitten from, to und when definiert werden, woher der Request kommen muss, welche HTTP-Methoden und Endpunkte aufgerufen werden sollen und welche Authentifizierungsinhalte (Claims im JWT) enthalten sein müssen. Erst wenn alle diese Kriterien zutreffen, wird der Zugriff erlaubt (action: allow), wie in Listing 8 gezeigt wird.
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: my-app namespace: my-namespace spec: selector: matchLabels: app: my-app action: ALLOW rules: - from: - source: principals: ["cluster.local/ns/ns-xyz/sa/my-partner-app"] - source: namespaces: ["ns-abc", "ns-def"] to: - operation: methods: ["GET"] paths: ["/info*"] - operation: methods: ["POST"] paths: ["/data"] when: - key: request.auth.claims[iss] values: ["https://idp.my-company.de"]
Neben der Möglichkeit, einen Request mit der action: allow zu berechtigen, gibt es noch die Möglichkeit, gewisse Request zu verbieten (action: deny) oder mit action: custom eigene Berechtigungsprüfungen in den Service Mesh zu integrieren. Damit ist es möglich schon vorhandene Berechtigungssysteme, wie sie in vielen Unternehmen existieren, weiterhin zu nutzen.
Finales System
Nach Anwendung der gesamten Regeln ergibt sich die Zero-Trust-Infrastruktur in Abbildung 3.
Abb. 3: Secure Deployment
Neben der mit TLS abgesicherten Kommunikation sind jetzt noch Berechtigungsprüfungen hinzugekommen, die im Grunde in jedem der Sidecars ausgewertet werden. Als Basis für die Authentifizierung dient das im HTTP-Header mitgeschickte JWT. Um diesen finalen Zustand zu erreichen, sind die in Tabelle 1 gezeigten Regeln notwendig.
Funktion | Regeln |
---|---|
TLS Terminierung | Gateway-Regel und Kubernetes Secret |
mTLS | Network Policy (Deny Ingress) und jeweils eine Network Policy pro Pod |
Network Segmentation | Request Authentication |
Authentication | Authorization Policy (Allow Nothing) und jeweils eine Authorization Policy pro Pod |
Authorization | Regeln |
Tabelle 1: Übersicht Regeln
Insgesamt also nur sechs Basisregeln und pro Pod noch zwei weitere Regeln zur Zugriffssteuerung. Mit dieser geringen Anzahl an Regeln sollte nun die zuvor aufgestellte Aufwand- Nutzen-Analyse für eine Zero-Trust-Infrastruktur zu einem ganz anderen Ergebnis führen.
Fazit
Zugegeben, der Aufwand, ein Service-Mesh-Tool im Projekt zu etablieren, ist nicht gerade gering. Aber neben den ganzen Securityaspekten bieten diese Tools noch sehr viel mehr an Funktionalität, die einem beim Betrieb von Microservices einen sehr guten Dienst erweisen können. Rein aus dem Gesichtspunkt der Security wäre der Aufwand für ein Service-Mesh-Tool wohl relativ hoch, aber im Zusammenwirken mit den anderen Service-Mesh-Funktionalitäten wie Trafficrouting, Resilience und Releasing ergibt sich durchaus eine positive Bilanz in der Aufwand-Nutzen-Analyse. Außerdem wäre ein automatisiertes Zertifikatsmanagement auch nicht gerade trivial und bei einer selbst implementierten Lösung vielleicht auch nicht ganz fehlerfrei. Im Bereich der Security sollte man sich aber keine Fehler erlauben.
Die Kombination mit der Network Policy kann ohne Einflüsse auf Istio parallel etabliert werden, womit einem Defense-in-Depth-Ansatz entsprochen wird. Die Berechtigungsprüfungen können sehr fein gesteuert werden und lassen im Grunde keine Wünsche offen. Hier ist allerdings Vorsicht geboten, da durch die umfangreichen Möglichkeiten Situationen entstehen können, die sehr komplex und damit nur noch schwer verständlich sind. „Keep it simple, stupid“ (KISS) sollte hier als Handlungsoption immer wieder in Betracht gezogen werden.
Um auch bei der Berechtigungsprüfung einen Defense-in-Depth-Ansatz zu etablieren, sollte natürlich noch in den jeweiligen Applikationen eine Berechtigungsprüfung stattfinden. Im Bereich des Auditing wird von Istio derzeit nur Stackdriver unterstützt. Hier wäre eine größere Auswahl an Auditsystemen wünschenswert. Doch was noch nicht ist, kann ja noch werden.
Insgesamt lässt sich mit ein paar Kubernetes- bzw. Istio-Regeln eine Zero-Trust-Infrastruktur etablieren, die wohl bei jedem Securityaudit standhalten wird.
Links & Literatur
[2] https://kubernetes.io/docs/concepts/cluster-administration/addons/