Die AWS re: Invent 2019 Konferenz war wie ihre vorherigen Ausgaben voller interessanter Breakout Sessions, die darauf abzielten, die Teilnehmer mit dem ausgewählten technischen Problem im Zusammenhang mit der Amazon Web Services Cloud vertraut zu machen. Eine dieser Reden hat mich dazu inspiriert, ein paar Worte über die Sicherheit von Apps zu schreiben, die im Serverless Modell erstellt wurden. Das Vertrauen der Unternehmen in diese Architektur wächst nicht nur bei Startups und kleinen Unternehmen, sondern auch bei großen Unternehmen stetig. Es gibt viele Anzeichen dafür, dass sich dieser Trend in den kommenden Jahren fortsetzen wird und die Anzahl der Serverless Produktion Implementierungen zunehmen wird. Aus diesem Grund lohnt es sich auf jeden Fall, sich die von AWS bereitgestellten Tools anzusehen, mit denen Programmierer, Administratoren und Architekten das Risiko von Datenlecks oder die Kontrolle über ihre Software minimieren können.
Bei der Entscheidung, die App im Serverless Modell zu implementieren, teilen wir die Verantwortung für ihre Sicherheit mit dem Cloud Dienstanbieter. Dies beschreibt das sogenannte Modell der geteilten Verantwortung (Eng. Shared Responsibility Model) [1]. Amazon Web Services muss die Sicherheit aller Computer- und Netzwerkinfrastrukturen, die richtige Konfiguration von Betriebssystemen und Softwarekomponenten (Hypervisor, Firewall usw.), die fortlaufende Installation von Sicherheitsupdates und Patches sowie die Datenverschlüsselung und den Netzwerkverkehr gewährleisten. Der Kunde, der direkte Benutzer von AWS Diensten ist, ist dafür verantwortlich, sein Programm so zu implementieren, dass potenzielle Angreifer nicht auf vertrauliche Ressourcen zugreifen oder unerwünschte Aktionen ausführen können. Mit anderen Worten – der Kunde ist verantwortlich für Errors im Code und in der Businesslogik der Apps sowie für die richtige Überwachung der relevanten Parameter und die Ereignisprotokollierung.
Benutzerauthentifizierung
Derzeit verfügt fast jede Business App über einen Zugriffskontrollmechanismus. Wir möchten sicherstellen, dass der Benutzer wirklich der ist, für den er sich ausgibt, und nur Zugriff auf die Ressourcen hat, über die er verfügen sollte. Die häufigste Authentifizierungsmethode ist die Verwendung eines statischen Kennworts, das normalerweise in der Datenbank gespeichert wird. Das Speichern von Passwörtern als einfacher, unverschlüsselter Text (Eng. plaintext) ist natürlich eine absolut inakzeptable Vorgehensweise. Das Hashing mit schlechten kryptografischen Hash Funktionen wie MD5 oder SHA1, die seit langem nicht mehr sicher sind, war lange keine bessere Option. Wenn man die Sicherheit ernst nimmt, sollte man sich auf einen der modernen Hashing Algorithmen konzentrieren, z. B. Bcrypt oder PBKDF2. Sie verfügen über integrierte Mechanismen zum Dehnen und Hinzufügen von Salz, wodurch sie äußerst widerstandsfähig gegen Brute Force Angriffe (Eng . brute force), Wörterbuchangriffe oder Regenbogentabellen (Eng. rainbow tables) sind.
Benutzer können dank des SRP (Eng. Secure Remote Password) Protokolls auch authentifiziert werden, ohne Kennwörter in einer Datenbank zu speichern oder sogar zwischen Client und Server zu übertragen. Einfach ausgedrückt: Anstelle eines Passworts muss ein Verifizierer gespeichert werden, also der nach der Formel v = gx mod N berechnete Wert, wobei g das sogenannte Gruppengenerator ist, N – eine ausreichend große Primzahl und x – ein Wert, der auf der Grundlage von Benutzername, Passwort und Salt unter Verwendung einer Einweg Hash-Funktion berechnet wird. Die Aufgabe eines potenziellen Angreifers, der das Protokoll brechen möchte, besteht darin, x zu berechnen, also einen diskreten Logarithmus zu finden. Selbst wenn man die Werte von N und g kennt, ist es äußerst schwierig zu berechnen (vorausgesetzt, die Zahl N erfüllt bestimmte Kriterien). Das SRP Protokoll kann daher eine interessante, aber auch anspruchsvolle Implementierungsalternative zur gängigen Speicherung von Passwort-Hashes in einer Datenbank sein.
Müssen wir in diesem Fall einen Programmierer mit Kenntnissen der modularen Arithmetik einstellen, um den sicheren Authentifizierungsprozess in unserer App nutzen zu können? In einer Serverless Welt – nein. Amazon hat solche Programmierer bereits damit beauftragt, die ganze mühsame Arbeit für uns zu erledigen. Aus diesem Grund können (und sollten) wir den Amazon Cognito Dienst nutzen, der uns von der Verantwortung für die sichere Speicherung von Benutzerzugriffsdaten befreit. Dank Bibliotheken wird es auch die Implementierung der Authentifizierungslogik mit und ohne SRP Protokoll für viele gängige Programmiersprachen erleichtern. Mit Cognito kann man einen eigenen Benutzerpool erstellen und in externe Identitätsanbieter wie Google, Facebook, Amazon oder andere Anbieter integrieren, die das OpenID Connect oder SAML Protokoll unterstützen. Dies erleichtert die Implementierung einer der guten Sicherheitspraktiken – der Zentralisierung des Identitätsmanagements, was zu einer Begrenzung der Anzahl möglicher Angriffsvektoren führt. Für die App gibt es dann nur einen Identitätsspeicher, obwohl Benutzerdaten tatsächlich an vielen verschiedenen Orten gespeichert werden können. Die Verwendung von Amazon Cognito in unserem Serverless Puzzle ist definitiv ein Schritt in die richtige Richtung, wenn wir ein hohes Maß an Sicherheit wünschen.
REST API Zugriffskontrolle
Es ist sehr wahrscheinlich, dass Ihre Serverless Anwendung die REST API verwendet (oder verwenden wird), um Daten zwischen verschiedenen Komponenten auszutauschen.
In der AWS Cloud zum Erstellen und Verwalten von API Endpoints gibt es einen dedizierten Dienst namens Amazon API Gateway. Trotz der Tatsache, dass wir in diesem Fall auch die Verantwortung für die Sicherheit mit dem Lieferanten teilen, sollte beachtet werden, dass es in unserer Verantwortung liegt, die ordnungsgemäße Konfiguration sicherzustellen. Eines seiner Schlüsselelemente ist das sogenannte Autorisierer, also ein in die Gateway API integrierter Mechanismus, dessen Aufgabe es ist, den Zugriff auf unsere API zu steuern. Betrachten wir ein Fragment der Architektur der einfachen Anwendung, das in der folgenden Abbildung dargestellt ist.
Die ClientApp sendet Anforderungen an einen oder mehrere REST API Endpoints, die über den Amazon API Gateway Dienst gemeinsam genutzt werden, und ruft dann die entsprechenden Lambda Funktionen auf. Wenn die App über einen Authentifizierungsmechanismus verfügt, möchten wir wahrscheinlich, dass nur angemeldete Benutzer API Anforderungen senden und somit Zugriff auf geschützte Ressourcen erhalten. Bei Verwendung des zuvor beschriebenen Amazon Cognito Dienstes, ist die Sache einfach.
Um den Mechanismus zur API Zugriffsbeschränkung mithilfe der Cognito Integration besser zu verstehen, beginnen wir mit einer kurzen Erläuterung von JWT (Eng. JSON Web Token) [2]. Es ist ein Standard, der eine sichere Art des Informationsaustauschs in Form eines JSON Objekts beschreibt, das kryptografisch codiert und signiert ist, sodass wir (fast) sicher sein können, dass die Daten tatsächlich von der erwarteten Quelle stammen. Ein typisches JWT Token besteht aus drei Teilen – dem Header, den korrekten Daten (der sogenannten Payload) und der Signatur, die durch einen Punkt getrennt sind. Der Amazon Cognito Dienst gibt bei korrekter Benutzerauthentifizierung drei JWT Token an unsere App zurück:
- ID Token
- Access Token
- Refresh Token
Die ersten beiden enthalten Anmeldeinformationen für die Benutzeridentität und sind ab dem Zeitpunkt ihrer Erstellung eine Stunde lang gültig. Mit Refresh Token kann man ID und Access Token nach ihrem Ablaufdatum neu generieren, ohne den Benutzer erneut authentifizieren zu müssen. Für jeden Cognito Pool werden zwei Paare von RSA Kryptografieschlüsseln generiert. Einer der privaten Schlüssel wird zum Erstellen der digitalen Tokensignatur verwendet. Wenn man den öffentlichen Schlüssel kennt, können sowohl unsere ClientApp als auch die Gateway API leicht überprüfen, ob der Benutzer, der die JWT Daten verwendet, tatsächlich aus unserem Pool stammt und ob er nicht versucht, sich als ein anderer Benutzer auszugeben. Jeder Versuch, die Token Zeichenfolge zu manipulieren, macht die Signatur ungültig.
Mit diesem Wissen nähern wir uns langsam der Erklärung des Geheimnisses des in den Cognito Pool integrierten Authorizer. Schauen wir uns eine modifizierte Version des zuvor vorgestellten Architekturdiagramms an.
Die von der ClientApp gesendete REST API Anforderung enthält diesmal einen zusätzlichen HTTP Header – Authorization. Als Wert für diesen Header legen wir das ID Token oder Access Token fest, das zuvor als Antwort auf eine erfolgreiche Benutzerauthentifizierung an die App gesendet wurde. Der API Gateway Dienst stellt sicher, dass das Token tatsächlich mit dem Schlüssel signiert wurde, der dem richtigen Cognito Pool zugeordnet ist, und dass das Token abgelaufen ist, bevor die Anforderung an die Lambda Funktion gesendet wird. Wenn die Überprüfung fehlschlägt, wird die Anforderung abgelehnt. Selbst wenn ein potenzieller Angreifer die Endpoint URL errät, kann er daher keine Anfrage erfolgreich an die API senden, ohne ein registrierter Benutzer unserer App zu sein. Die Lösung ist einfach, elegant und sicher und übrigens ein weiteres Argument für die Nutzung des Amazon Cognito Dienstes.
Ein anderer verfügbarer Autorisierungstyp ist Lambda Authorizer, der in Situationen nützlich ist, in denen wir Cognito aus irgendeinem Grund nicht verwenden können. Wie der Name schon sagt, verwendet diese Lösung die Lambda Funktion, die aktiviert wird, wenn die Anforderung am Endpoint eintrifft. Die Funktion enthält unsere eigene Logik zur Anforderungsüberprüfung basierend auf dem bereitgestellten Token oder basierend auf den mit der Anforderung gesendeten Headern und Parametern. Als Ergebnis der Lambda Operation muss ein Objekt zurückgegeben werden, das eine IAM Richtlinie enthält, die angibt, ob die Gateway API die Anforderung akzeptieren oder ablehnen soll.
Sichere Lambda Funktionen
Wenn man über die Sicherheit der Serverless App schreibt, kann man Probleme mit der Lambda Funktion nicht ignorieren. Wie bereits erwähnt, ist der Cloud Dienstanbieter für die richtige Konfiguration der Laufzeitumgebung und der gesamten zugehörigen Infrastruktur sowie der Programmierer verantwortlich – für das Schreiben von Code, der frei von Schwachstellen ist, die einen Angriffsvektor darstellen können. Eine solche Sicherheitsanfälligkeit, die im OWASP Serverless Top 10 [3] Ranking an erster Stelle steht, ist die sogenannte Injektion (Eng. injection), die viele Menschen mit dem in verschiedenen Quellen gut beschriebenen beliebten SQL Injection Angriff in Verbindung bringen können. Zur Erinnerung: es besteht in der unbeabsichtigten Ausführung einer SQL Abfrage (oder eines Teils davon), die der Angreifer in den Eingabedaten platziert, die unsere App ohne vorherige Filterung verarbeitet. Im Serverless Modell muss dieser Datentyp nicht unbedingt direkt von der Benutzeroberfläche stammen. Lambda Funktionen werden häufig als Reaktion auf das Eintreffen eines bestimmten Ereignisses von einem anderen AWS Dienst gestartet, z. B.: Erstellen einer neuen Datei im S3 Bucket, Ändern des Datensatzes in der DynamoDB Tabelle oder Erscheinen einer Benachrichtigung im SNS Thema. Das Event Objekt, das eine Reihe von Informationen zu einem solchen Ereignis speichert und in der Lambda Hauptmethode verfügbar ist, kann in einigen Fällen auch vom Angreifer “injizierten” Code enthalten. Aus diesem Grund ist es äußerst wichtig, dass jedes Byte der Eingabedaten, die von einer beliebigen Quelle an unsere Funktion gesendet werden, richtig gefiltert wird, bevor es in einer SQL Abfrage oder einem System Shell Befehl verwendet wird. Was ist die Gefahr im letzteren Fall? Alle gängigen Programmiersprachen verfügen über Funktionen oder Bibliotheken zum Ausführen von Shell Befehlen aus dem Code heraus. Aus technischer Sicht hindert nichts daran, dies in Lambda Funktionen zu tun, aber…
Um den möglichen Angriffsvektor besser zu verstehen, erinnern wir uns kurz an die Grundlagen des Lambda Dienstes.
Quelle: Security Overview of AWS Lambda [1]
Zur Ausführung unserer Funktionen verwendet AWS spezielle Arten von virtuellen Maschinen, die sogenannten MicroVMs [4]. Jede Instanz von MicroVM kann für die Anforderungen verschiedener Funktionen innerhalb eines bestimmten Kontos wiederverwendet werden. Außerdem kann jede solche Instanz viele Laufzeitumgebungen enthalten (dies sind Container irgendeiner Art), in denen die vom Benutzer ausgewählte Laufzeitumgebung ausgeführt wird, z. B. Node.js, JVM oder Python. Laufzeitumgebungen werden nicht von verschiedenen Funktionen gemeinsam genutzt, können jedoch – was wichtig ist – erneut verwendet werden, um nachfolgende Aufrufe desselben Lambda auszuführen. Wenn wir also einen System Shell Befehl in dem dynamisch erstellten Code aufrufen, der ungefilterte Eingaben enthält, kann der Angreifer diese Tatsache nutzen, um die Kontrolle über die Laufzeitumgebung und damit über andere Funktionsaufrufe zu übernehmen. Auf diese Weise kann er häufig auf vertrauliche Informationen zugreifen, die beispielsweise in einer Datenbank oder in Dateien im Verzeichnis /tmp gespeichert sind, und einige Ressourcen der App zerstören. Die Sicherheitsanfälligkeit wurde als RCE (ang. Remote Code Execution) [5] beschrieben.
Neben dem Filtern und Validieren von Eingabedaten ist die Anwendung des Mindestberechtigungsprinzips eine der wichtigsten Sicherheitspraktiken für den AWS Lambda Service (Eng. principle of least privilege). Um es richtig umzusetzen, müssen wir sicherstellen, dass zwei Annahmen erfüllt sind:
- Jedem Lambda in unserer Anwendung sollte eine separate IAM Rolle zugewiesen sein. Das Erstellen einer gemeinsamen Rolle für alle Funktionen ist nicht akzeptabel.
- Die Rolle jedes Lambda sollte nur die Operationen zulassen, die tatsächlich ausgeführt werden. Vermeiden Sie die Verwendung eines Platzhalters (*, sog. wildcard)
in die Richtlinie.
Ein einfaches Beispiel für die praktische Anwendung der Regel: wenn die Aufgabe einer bestimmten Funktion darin besteht, Datensätze aus der DynamoDB Datenbank zu lesen, darf die zugewiesene IAM Rolle nur die in diesem Fall erforderliche Leseoperation aus der spezifischen Tabelle zulassen. In einigen Situationen können wir noch einen Schritt weiter gehen und den Zugriff nur auf ausgewählte Datensätze in der Tabelle und ausgewählte Attribute dieser Datensätze beschränken [6]. Selbst wenn der Angreifer in der Lage ist, uns zu überlisten und Zugriff auf die Lambda Laufzeitumgebung zu erhalten, wird durch die Anwendung des Prinzips der Mindestberechtigungen der verursachte Schaden erheblich reduziert.
Speicherung von Zugangsdaten
Es ist leicht zu bemerken, dass wir uns bei Verwendung der Amazon DynamoDB Datenbank nicht um den Aufbau einer Verbindung und Authentifizierung mit einem Benutzernamen und einem Kennwort kümmern müssen, was normalerweise bei Datenbankservern der Fall ist. Die Kommunikation erfolgt über das HTTP (S) Protokoll, und jede gesendete Anforderung enthält eine kryptografische Signatur. Der Entwickler muss normalerweise die Details dieser Schnittstelle auf niedriger Ebene nicht kennen, da er die praktische API auf hoher Ebene mithilfe von AWS CLI oder AWS SDKs verwenden kann. In einigen Anwendungen muss jedoch eine andere Datenbank verwendet werden. Dies bedeutet normalerweise, dass Zugriffsdaten irgendwo im “Abgrund” unserer App gespeichert werden müssen. Es ist eine schlechte Idee, sie direkt in den Lambda Funktionscode zu schreiben, was häufig bedeutet, dass sie später in das Git Repository verschoben werden. Eine bessere Lösung könnte darin bestehen, die verschlüsselten Lambda Umgebungsvariablen zu verwenden, und noch besser, den AWS Secrets Manager Dienst zu verwenden. Man kann Passwords, API Zugriffsschlüssel und andere vertrauliche Informationen sicher speichern. Der Programmierer kann solche Daten einfach im Lambda Funktionscode herunterladen, indem er die entsprechende Methode aus dem AWS SDK aufruft.
Falls unsere App einen Server oder Datenbankcluster verwendet, der mit Amazon RDS erstellt wurde, können wir die IAM Authentifizierung verwenden [7]. Nach der richtigen Konfiguration der Datenbank und der der Lambda Funktion zugewiesenen IAM Rolle laden wir mit einem Aufruf der AWS SDK Methode ein temporäres Zugriffstoken herunter, das 15 Minuten gültig ist und das wir dann anstelle des Kennworts in der Standardprozedur für die Verbindung mit der Datenbank verwenden. Es ist erforderlich, dass es sich um eine verschlüsselte SSL Verbindung handelt, die das Sicherheitsniveau der Lösung weiter erhöht. Es sind jedoch einige Einschränkungen zu beachten – mit der MySQL Engine können auf diese Weise bis zu 200 neue Verbindungen pro Sekunde hergestellt werden.
Zusammenfassung
Es ist zu beachten, dass die Auswahl des Serverless Modells uns nicht vollständig von der Notwendigkeit befreit, sich mit sicherheitsrelevanten Fragen zu befassen. Durch die Verwendung der in der AWS Cloud verfügbaren Tools und Dienste können wir diese Aufgabe jedoch definitiv vereinfachen. Eine detaillierte Beschreibung aller möglichen Bedrohungen, die in dieser Architektur auftreten, und wie man ihnen entgegenwirkt, reicht aus, um mindestens ein aufgeblähtes Buch zu schreiben. Dieser Artikel befasst sich nur mit ausgewählten Themen und ist ein guter Ausgangspunkt, um das Wissen zu diesem Thema weiter zu vertiefen. Ich empfehle Ihnen, die ergänzenden Materialien zu lesen und eine Vorschau der Aufzeichnung aus der Vorlesung „Securing enterprise-grade Serverless apps” anzuzeigen, auf deren Grundlage dieser Artikel erstellt wurde.
Ergänzende Materialien
- Amazon Web Services, Inc., Security Overview of AWS Lambda. An In-Depth Look at Lambda Security.
https://d1.awsstatic.com/whitepapers/Overview-AWS-Lambda-Security.pdf - Auth0, Inc., JSON Web Token Introduction
https://jwt.io/introduction/ - The OWASP Foundation, OWASP Serverless Top 10
https://github.com/OWASP/Serverless-Top-10-Project - Amazon Web Services, Inc., Firecracker
https://firecracker-microvm.github.io - Yuval Avrahami, Gaining Persistency on Vulnerable Lambdas
https://www.twistlock.com/labs-blog/gaining-persistency-vulnerable-lambdas/ - Amazon Web Services, Inc., Using IAM Policy Conditions for Fine-Grained Access Control
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/specifying-conditions.html - Amazon Web Services, Inc., IAM Database Authentication for MySQL and PostgreSQL
https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html