Full Disclosure: Sicherung interner Kommunikation

Aktuelle E-Commerce-Systeme bestehen häufig aus mehreren Servern, die mit dem Browser der Benutzer über SSL-gesicherte Leitungen kommunizieren. Allerdings sind die Rechner der Kunden nicht die einzigen, mit denen ein Shop Daten austauschen muss: fast immer ist es notwendig, dass die einzelnen Server des Systems auch untereinander kommunizieren müssen. Dabei ist es unerheblich, ob es sich um eine Microservice– oder eine Vertikalen-Architektur handelt. Sogar die in die Tage gekommenen Monolithen nutzen bei einem Multi-Tier-Ansatz gerne mehrere Server.

Gemein ist diesen Backend-Requests (im Gegensatz zu dem Netzwerkverkehr zwischen Kunden-Browser und otto.de), dass hier im Allgemeinen keine vertraulichen Daten über die Leitung gehen, sondern beispielsweise die Aktualisierung einer Produktverfügbarkeit oder Konfigurations-Requests. Diese Inhalte müssen also nicht geheim gehalten werden, was uns den Aufwand und die Komplexität erspart, sie zu verschlüsseln.

Trotzdem darf natürlich nicht jeder die Texte im Shop ändern oder Produktinformationen anpassen, daher benötigen wir eine Methode zur Authentifizierung und Autorisierung der Requests.

Authentifizierung & Autorisierung

Diese Begriffe werden umgangssprachlich häufig synonym benutzt, aber für Techniker bezeichnen sie konzeptionell verschiedene Dinge:

Authentifizierung meint einen Vorgang, bei dem die Echtheit oder Identität von etwas festgestellt wird. In diesem Zusammenhang soll bei einem Request geprüft werden können, ob er von einem bekannten Benutzer bzw. Rechner initiiert wurde.

Bei der Autorisierung, die im Allgemeinen als darauffolgender Schritt vorgesehen ist, wird geprüft, ob der Benutzer bzw. Rechner die notwendigen Berechtigungen hat, die gewünschte Aktion auszuführen.

HMAC

Die HMAC-Methode wird in verschiedenen RFCs standardisiert. Wie alle MACs basiert sie darauf, dass eine Nachricht mit einem pre-shared key (PSK) signiert wird – in diesem Fall wird eine Hash-Funktion genutzt. In der bei OTTO genutzten Variante wird die Signatur dem ursprünglichen HTTP-Request als Signatur-Header beigefügt und daraufhin kann der Request vom Server entweder erlaubt oder verworfen werden.

Client

Um den Request zu signieren, muss der Client den Benutzernamen und den PSK mit dem aktuellen Zeitstempel und der eigentlichen Nachricht (also HTTP-Verb, URI, Body etc.) auf die richtige Weise kombinieren. Die Signatur wird dem Request ebenso als Header beigefügt wie der Zeitstempel. Da sich Client und Server den PSK teilen, wird er selbstverständlich nicht auch noch übertragen.
Das Diagramm zeigt, welche Informationen für die Signatur herangezogen werden. Als Beispiel wurde ein GET-Request auf die Ressource getLastOrder für den Benutzer example.user mit dem Geheimnis PSK am 22.06.2016 gegen halb zwölf mitteleuropäischer Zeit signiert.

HMAC

Server

Mit dem übertragenenen Benutzernamen kann der Server aus seiner Benutzerdatenbank den zugehörigen PSK ermitteln. Er wird genutzt, um mit den restlichen Informationen aus dem Request (Zeitstempel, HTTP-Verb etc.) dieselbe Signatur erneut zu generieren und sie mit der im Header übertragenen zu vergleichen. Unterscheiden sich die beiden Signaturen, dann weil die beim Signieren genutzten Information sich unterscheiden. Also entweder ist der PSK falsch oder es handelt sich um eine Replay-Attacke, bei der Teile des originalen Requests verändert wurden. Offensichtlich kann in beiden Fällen die Anfrage abgelehnt werden.

Andernfalls wird nun geprüft, ob der verifizierte Benutzer auch die Berechtigung hat, die gewünschte Aktion durchzuführen. Ist der Benutzer nicht dazu autorisiert, kann die Anfrage ebenfalls abgelehnt werden, ansonsten wird die Aktion durchgeführt.

Benutzung

Unter https://github.com/otto-de/hmac-auth findet sich eine Java-Implementierung dieser Methode für JVM-basierte Programme.

Um korrekte Header zu erzeugen, kann der HMACJerseyClient oder der HmacJersey2ClientRequestFilter benutzt werden, die bei der Instantiierung den Benutzernamen und den PSK bekommen. Als Zeitstempel wird die aktuelle Uhrzeit des Systems benutzt und die weiteren Informationen kommen aus dem Request, den der Client ja ohnehin generiert. Die Header werden dann ohne weiteres Zutun an jeden Request gehängt:

final WebTarget webTarget = jerseyRestClientFactory.getClient().target(url);
webTarget.register(new HmacJersey2ClientRequestFilter(userName, preSharedKey));

Um serverseitig die Header in Application-Containern zu prüfen, kann die andere Komponente der Bibliothek genutzt werden: der AuthenticationFilter prüft jeden ankommenden Request auf die HMAC-Header und führt eine Authentifizierung durch, falls sie vorhanden sind. Der Filter wird dem Application-Container einfach bekannt gemacht:

@Bean
public AuthenticationFilter authenticationFilter() {
    return new AuthenticationFilter(new AuthenticationService(new PropertyUserRepository()));
}

Ankommende Requests werden nun mit dem HTTP-Status 401 abgelehnt, wenn die HMAC-Header vorhanden sind, aber die Signatur falsch oder der Zeitstempel zu alt ist.

In Applikationen die Spring verwenden, steht für die Autorisierung die @AllowedForRoles-Annotation zur Verfügung, die beispielsweise an Service-Methoden gehängt werden kann. Ein Aspect unterbricht den Aufruf der Methode und prüft, ob der im Request angegebe Benutzer eine der Rollen hat, die in der Annotation stehen, bevor die Methode ausgeführt wird. Falls das nicht der Fall ist, wird eine AuthorizationException geworfen, die vom Benutzer der Bibliothek verarbeitet werden muss, die aber im Allgemeinen ebenfalls zu einem HTTP-Status 401 führt:

@RequestMapping(value = {"/priceOfIPhone"}, method = RequestMethod.PUT)
@AllowedForRoles(value = "iPhonePriceManager")
@ResponseBody
public void updateHealth(@RequestParam final Integer newIphonePrice) {
    final IPhoneData currentIPhoneData = allPhonesService.getIPhoneData();
    newIPhoneData = currentIPhoneData.withPrice(newIphonePrice);
    allPhonesService.setIPhoneData(newIPhoneData);
}

Eigenschaften der Implementierung

Sicher

Da die HMAC-Methode nicht vorschreibt, welche Hash-Algorithmen beim Signieren des Requests benutzt werden sollen, bleibt es der konkreten Implementierung vorbehalten, eine sichere Funktion zu wählen. Wir haben uns für HmacSHA256 entschieden, das genau für diese Zwecke entwickelt wurde und als State-of-the-Art Hash weit verbreitet und gut geprüft ist.

Andere denkbare Mechanismen wie z.B. HTTP-Basic sind so angreifbar, dass ihr Einsatz ohne zusätzliche SSL-Verschlüsselung, die wir uns ja ersparen wollen, grob fahrlässig ist.

Stateless

Die große Mehrzahl unserer Web-Server sind Docker-Container, die in ihrer Mesos-Umgebung ständig entstehen und vergehen können sollen, während die Maschinen je nach Last dynamisch skaliert werden. Bei zwei aufeinanderfolgenden Requests ist es also keinesfalls selbstverständlich, dass sie auf demselben Server landen. Auch für Blue-Green Deployments dürfen die Maschinen keinen Zustand teilen, der sich ändert.

Da unser Ansatz jeden Request erneut signiert, muss keine Session oder dergleichen im Server gehalten werden, die aufwändig zwischen den einzelnen Instanzen synchronisiert werden müsste.

Im Falle von HTTP-Digest, das eigentlich ein natürlicher Kandidat für die Problemstellung wäre, muss entweder die Nonce für jeden Benutzer in allen Servern synchronisiert oder der langsame initiale Handshake bei jeder Anfrage erneut ausgeführt werden.

Resilient gegenüber Replay-Angriffen

Bei diesen Angriffen wird versucht, zuvor abgehörte Requests erneut abzuschicken. Das ist Erfolg versprechend, weil der abgehörte Request ja vermutlich erfolgreich gewesen ist und die Angreiferin hoffen kann, dass auch der erneut gesendete Request erfolgreich sein wird. Diese Angriffe sind zwar theoretisch noch möglich, aber durch die zeitliche Begrenzung erheblich schwieriger – Requests haben einen eingebauten Verfallszeitpunkt.

Gelingt es Angreifern den Replay innerhalb der kurzen Gültigkeitsdauer durchzuführen, so sind die Angriffsmöglichkeiten für einzelne Requests recht eingeschränkt: ein POST ist sinnlos (eine neue Ressource erneut anlegen), ein PUT wird die durchgeführte Änderung an der Ressource erneut durchführen und sie damit zementieren, ein DELETE die bereits gelöschte Ressource erneut löschen und ein GET die nicht-vertraulichen Daten preisgeben. Lediglich bei direkt aufeinander folgenden Requests ist es denkbar, dass die Angreiferin genug Zeit hat, nach einem Request den vorigen erneut abzusetzen und so beispielsweise kürzlich gelöschte Ressourcen wieder herzustellen oder eine Konfiguration auf einen früheren Zeitpunkt zurückzudrehen. Allerdings sind unsere Business-Prozesse in dieser Hinsicht ohnehin sehr tolerant, da sie menschliche Fehler berücksichtigen müssen, daher könnte ein Angreifer im besten Falle hoffen, ein wenig Verwirrung zu stiften.

Eine Variante davon ist, den ursprünglichen Request zu verändern. So könnte eine Angreiferin beispielsweise versuchen einen GET zu einem DELETE Request zu ändern, um eine zuvor angefragte Resource zu löschen. Da allerdings sowohl das HTTP-Verb als auch der Request-Body Teil der Signatur sind, führen Manipulationen daran lediglich dazu, dass die Signatur nicht mehr zum Request passt und er abgelehnt wird.

Lediglich die unbeteiligten Request-Header bleiben Angreifern, um sie zu manipulieren und dort sind üblicherweise keine interessanten Daten enthalten.

Performance

Die Methode ist ausgesprochen schnell, insbesondere im Vergleich zu Methoden mit integrierter Verschlüsselung. Falls vertrauliche Daten übermittelt werden sollen, ist die Kombination von HMAC mit einer Transportverschlüsselung wie SSL immer noch erheblich schneller als Methoden im Application-Layer wie oAuth2 oder die verschiedenen wsSecurity-Varianten.

Einfach …

Security ist schwer, aber wir wollen trotzdem möglichst keine Bugs in diesem Bereich.
Insbesondere in unserem multilingualen Umfeld, in dem möglicherweise Teams eigene Implementierungen des Standards umsetzen müssen, ist es von großem Vorteil eine Methode zu nutzen, die mit wenigen Worten umfassend beschrieben werden und die insbesondere auch ohne fortgeschrittene mathematische Methoden umgesetzt werden kann

… aber nur für Maschinen

Durch die umständliche Berechnung der Header und die zeitliche Begrenzung der Requestgültigkeit ist es sehr schwer, manuell korrekte Requests zu generieren. Um dennoch Test-Requests an HMAC-gesicherte Schnittstellen senden zu können, gibt es im Github-Project einen Proxy, der lokal gestartet werden kann und der Requests entgegennimmt, die notwendigen Header erzeugt und den korrekten Request weiterleitet. Die Benutzung bleibt aber trotz allem recht sperrig und ist nicht für technisch unbedarfte Benutzer geeignet.

 

Tom Vollerthun hat über 10 Jahre Erfahrung mit Entwicklung und Betrieb kleiner und großer E-Commerce-Systeme. Er arbeitet als Hacker und Prozessmasseur für otto.de in Hamburg.

Tagged with: , ,
Veröffentlicht in Development, Grundlagen
4 comments on “Full Disclosure: Sicherung interner Kommunikation
  1. Alexander Kiel sagt:

    Danke für den fundierten Artikel. Ich habe zwei Anmerkungen: Wie stellt Ihr sicher, dass der PSK geheim bleibt und wie bewertet Ihr die Performance Eures Ansatzes, der ein Zugriff auf ein Userrepository benötigt, im Vergleich zu Ansätzen wie oAuth + JWT, die dies nicht benötigen? Bzgl. meines ersten Punktes, würde mich speziell interessieren, wie Ihr PSK’s verteilt und im Falle eines Exploids invalidiert.

    • TomVollerthun sagt:

      Das „Userrepository“ ist bei uns üblicherweise lediglich eine HashMap, in der die höchstens 10-15 PSKs der anderen Systeme liegen, mit denen zur Laufzeit kommuniziert werden muss. Da die Daten also im lokalen Speicher liegen, gibt es keinen nennenswerten Performance-Einfluss. Es handelt sich halt nicht um öffentliche APIs, zu denen Endbenutzer Zugriff benötigen würden.

      Gegen oAuth spricht für uns hauptsächlich die Session, die standardmäßig als lokaler State im Speicher der Server liegt. Um das zu vermeiden, müsste sie in der Datenbank gehalten werden, was die Authentifizierung erheblich komplexer (und langsamer) macht. Alternativ könnte man versuchen, bei jedem Zugriff eine neue Session zu beginnen, aber der Handshake ist vermutlich nochmal um einige Größenordnungen langsamer und ehrlich gesagt haben wir das gar nicht erst versucht.

      JWT war vor rund vier Jahren, als wir uns mit dem Thema beschäftigen mussten, noch nicht einsatzfähig – ich glaube, ich habe letztes Jahr überhaupt zum ersten Mal davon gehört. Aber wenn ich https://jwt.io/introduction/ richtig lese, benutzt auch dieses Protokoll HMAC-Signaturen, wenn die Nachrichten nicht verschlüsselt werden müssen.
      Allerdings habe ich keine tiefergehende Analyse oder gar praktische Erfahrungen dazu – vielleicht möchte mich also jemand in dieser Hinsicht korrigieren?

      Unsere PSKs werden wie alle anderen Geheimnisse in Vault gehalten und beim Hochfahren der Applikation in den Speicher geladen.
      Da die Schnittstellen nur für interne Kommunikation gedacht sind, benutzen wir häufig Jabber mit OTR-Verschlüsselung um die Keys zum Nachbarteam zu transportieren, aber nur weil sie zu lang für einen Zettel sind – wir versuchen nicht allzu paranoid zu werden 🙂
      Das ist im wesentlich auch die aktuelle Strategie, um Geheimnisse zu ändern. Zwar beginnnen wir uns Gedanken zu automatischen regelmäßigen PSK-Änderungen zu machen, aber das ist noch nicht weit fortgeschritten.

      • >Gegen oAuth spricht für uns hauptsächlich die Session, die standardmäßig als lokaler State im Speicher der Server liegt.

        Ich bin im Zuge von oAuth noch nicht mit Sessions in Berührung gekommen. Requests auf den Resource Server sind auch bei oAuth stateless und enthalten lediglich einen Token. Requests müssen allerdings per HTTPS transportiert werden, da die Token geheim sind. Neben den JWT Tokens kann man auch einfache Bearer Tokens verwenden. Bearer Tokens sind einfach nur zufällige Zeichenketten. Ein Bearer Token muss dann allerdings vom Resource Server am Authorization Server verifiziert werden [RFC7662].

        >Aber wenn ich https://jwt.io/introduction/ richtig lese, benutzt auch dieses Protokoll HMAC-Signaturen, wenn die Nachrichten nicht verschlüsselt werden müssen.

        Ein JWT Token ist vom Authorization Server signiert. Für diese Signatur kann man symmetrische oder asymmetrische Verfahren einsetzen. Der Vorteil bei asymmetrischen Verfahren ist, dass die Resource Server, welche das Token verifizieren müssen nicht über einen geheimen Schlüssel verfügen müssen. Man kann JWT’s auch zusätzlich verschlüsseln, was in meinen Augen allerdings kaum notwendig ist, da das Token in jedem Fall über HTTPS transportiert werden muss.

        Die Idee von JWT’s ist, dass diese Claims beinhalten, welche vom Authorization Server signiert sind und besagen, dass der Inhaber dieses Tokens bestimmte Rechte hat. Dadurch muss ein Resource Server keinerlei Benutzerdatenbank führen. Der Resource Server vertraut einfach dem Authorization Server und handelt entsprechend. Der Vorteil ist, dass man nur einen Punkt (den Authorization Server) hat, der Benutzer und Rechte kennen muss.

        Bei OAuth hat man allerdings immer den Aufwand sich ein Token vom Authorization Server holen zu müssen. Diese Token ist dann üblicherweise auch nur eine begrenzte Zeit gültig, so dass man diese Tokens auch noch refreshen muss. Es gibt also auch hier nichts umsonst.

      • TomVollerthun sagt:

        > … und enthalten lediglich einen Token …

        Mit „Session“ meinte ich jede Art von veränderlichem Zustand, der serverseitig gehalten werden muss, also nicht zwangsläufig eine HTTP-Session – das ist zugegebenermaßen etwas unscharf formuliert.

        Ich habe zwar noch keinen produktiven Einsatz von oAuth begleitet, aber für mich hört sich das so an, als wäre der Token, den der Client vom Authorization Server bekommt, genau so ein mutable state, weil der Server immer wissen muss, ob und wann der Token abläuft oder ob er überhaupt jemals gültig war.
        Zwar kann diese Authorisierung in einen Authorization Server ausgelagert werden, aber den müssten wir ja auch wieder ausfallsicher clustern – zu einem „Authorization Cluster“. Wenn ich das jetzt alles richtig verstanden habe hieße das doch, dass der Zustand des Tokens im „Authorization Cluster“ synchronisiert werden muss und genau das wollen wir ja verhindern.

        Aber da du offenbar schon tiefergehende Erfahrung mit oAuth hast, kannst du mir vielleicht sagen, ob es bei oAuth auch eine zustandslose Variante gibt?

        > … Requests müssen allerdings per HTTPS transportiert werden …

        Das schließt oAuth für unseren Anwendungsfall leider ohnehin aus. Aber wenn eh HTTPS eingesetzt werden muss, würde ich vermutlich HTTP-Basic für die interne Kommunikation bevorzugen und den Authorization-Header direkt mitschicken.
        Auf mich wirkt es so, als seien öffentliche APIs ein passenderer Anwendungsfall für oAuth: SSL Transport, zentrale Verwaltung von Benutzern und Rollen, „Single Sign-On“-Fähigkeit über den zentralen Auth-Server …

        Vielen Dank übrigens für die aufschlussreiche Erklärung von JWT im Zusammenhang mit oAuth – ich hatte bislang gedacht, dass das konkurrierende Konzepte sind 🙂

        Da Vault auch die Generierung von Passworten mit Ablaufzeitpunkt anbietet, könnten wir uns interessanterweise ja auch durchaus noch in die Richtung eines Authorization Servers bewegen: beim Überfliegen der Doku hatte ich den Eindruck, dass die Gültigkeitsdauer Teil des Tokens ist und damit stateless, was genau zu uns passen würde.
        Dann hätten wir allerdings auch den zusätzlichen Request den du in deinem letzten Absatz beschrieben hast und ich fürchte fast, dass das dann zu teuer würde.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: