Im Rahmen eines Projekts bei Tramino habe ich mich genauer mit einem Modul für nginx beschäfigt. Dieses erlaubt es, Anfragen zu signieren um einen unberechtigten Zugriff zu verhindern, ohne dass der Client nochmal extra Zugangsdaten eingeben muss (wie bei HTTP Basic Auth).
Alle Konfigurations-Beispiele und Skripte sind auch über das Code-Repository für diesen Blogpost herunterladbar.
Warum über nginx?
Natürlich kann man sich so eine Funktionalität in seine eigene Anwendung integrieren, und hat dann die Möglichkeit Zugriff jederzeit und mit beliebiger Komplexität zu prüfen.
In diesem Fall jedoch geht um die Auslieferung von Bildern, welche teilweise direkt aus dem Dateisystem kommen. Hier ist das Ziel, dass die Dateien möglichst schnell ausgeliefert werden, und ein zusätzlichen Backend kann hier die Latenz ordentlich erhöhen und bei hoher Last zu Fehlern bei der Auslieferung führen.
nginx bietet für diesen Anwendungsfall bereits ein optnales Modul names secure_link an, welches genau dies abdeckt. In diesem Blogeintrag, welcher für mich eine Lerneinheit darstellt, will ich ein konkretes Beispiel für dieses Modul geben.
Überlegungen
Das secure_link-Modul ist recht flexibel, indem es mit Hilfe von Variablen erlaubt, verschiedene Szenarien zur Prüfung abzubilden.
Im einfachsten Fall wird lediglich ein Secret (was nur nginx sowie der Webdienst welcher URLs generiert weiß), sowie ein UNIX-Zeitstempel um die Lebensdauer der Links einzuschränken.
Dies deckt die meisten Anwendungsfälle bereits ab, die Dokumentation des Moduls liefert jedoch noch eine weitere Idee: Indem man die IP-Adresse oder den User-Agent mit einbindet, kann man Zugriffe noch effektiver einschränken und verhindert das Teilen von Links zwischen Benutzern.
Nachteil dieser Lösung ist jedoch, dass der Einsatz in einem CMS, welches ganze Seiten cachet, unmöglich ist, da dieses ja für jeden Client eigene URLs erzeugen muss.
Wir beschränken uns daher in diesem Tutorial auf die die einfacheren Lösungen.
Installation
Bevor wir beginnen können, brauchen wir einen nginx. Dieser kann auf Linux-Systemen mit dem jeweiligen Paketmanager (apt/apt-get
/ yum/dnf
) installiert werden, auf macOS ist Homebrew empfehlenswert (brew install nginx
).
Für diesen Test lassen wir nginx auch mit einer eigenen minimalen Instanz laufen, um nicht den bestehenden Webserver anpassen zu müssen.
Auf Ubuntu ist das Paket nginx-extras
notwendig, andere Varianten wie nginx-full
enthalten das Modul nicht.
Konfiguration
Das secure_link-Modul bietet insgesamt drei Direkten, für zwei unterschiedliche Modi wie das Mould verwendet werden kann:
Signierung über URL-Hash (secure_link_secret)
Dies ist die einfachere Variante: Es werden vom Webdienst URLs in folgendem Format gebildet:
- Der Präfix ist das "Unterverzeichnis" auf dem Webserver und dient der Abtrennung von Routen. In nginx wird dies über die
location
-Direktive erzeugt. - Der Hash ist die MD5-Hex-Summe von
<Link><Secret>
, wobei der <Secret>
der Wert der Direktive secure_link_secret
ist. - Der eigentliche Link ist der Pfad, welcher eigentlich aufgerufen werden soll.
Um dies besser zu veranschaulichen, nehme ich das Beispiel aus der Modul-Dokumentation und versehe es mit weiteren Kommentaren:
Generierung der URLs
Zur Generierung der Links können die MD5-Funktionen verwendet werden, sie sie nahezu jede Programmiersprache anbietet. Als Beispiel die Generierung für Python:
Weitere Beispiele für Bash + Perl im Code-Repository für diesen Blogpost (Dateien hashlink.*
).
Signierung über Parameter (secure_link + md5)
Wie bereits oben kurz beschrieben kann das secure_link URLs nicht nur mit statischen Signaturen (dem Hash), sondern auch dynamisch (Zeitstempel, Parameter, HTTP-Header) validieren.
Für diese Variante werden werden vom Webdienst URLs in folgendem Format gebildet:
- Der Präfix ist das "Unterverzeichnis" auf dem Webserver und dient der Abtrennung von Routen. In nginx wird dies über die
location
-Direktive erzeugt. Im Gegensatz zur anderen Methode ist dieser Teil des Hashings. - Der eigentliche Link ist der Pfad, welcher eigentlich aufgerufen werden soll.
- Der Hash ist die MD5-Hex-Summe von
<Link><Secret>
, wobei der <Secret>
der Wert der Direktive secure_link_secret
ist. - Expire ist der UNIx-Zeitstempel, bis wann der Link gültig ist. Dieser wird auch in den Hash gemixt, um Manipulationen zu verhindern.
Für diese Variante sind zwei Einstellungen zu konfigurieren, und es gibt nun drei Ausgänge (Signatur OK; Signatur falsch; Signatur abgelaufen).
Eine Warnung aus eigener Erfahrung: Zeit ist schwerer als man gerne denkt. Insbesondere bei UNIX-Zeitstempeln ist auf zwei Dinge zu achten: Ist dieser in UTC oder lokale Zeit, und ist dieser in Sekunden (Standard) oder Millisekunden (JavaScript)? Falls hier keine einheitliche Linie im System durchgesetzt wird, wird die Signierung an obskuren Stellen brechen und das Debugging aufwändig und lästig.
Und wieder ein kommentiertes Konfigurations-Beispiel:
Den secure_link_md5
habe ich 1:1 aus der nginx-Dokumentation übernommen, es sollte jedoch vor dem Einsatz genau geprüft werden, ob dies den Anforderungen der Anwendung enspricht. In unserem Fall wollen wir die IP-Adresse ($remote_addr
) nicht, da Ressourcen nur zeitlich aber nicht pro Client abgesichert werden sollen.
Aufgrund des immer häufigeren Einsatz von Carrier-grade NAT, insbesondere in Mobilfunk und Kabelnetzen, ist die IP-Adresse eines Clients nicht unbedingt stabil zwischen Anfragen. In Fällen höherer Sicherheitsanforderungen sollte man die Frage stellen, ob eine andere Lösung nicht sinnvoller wäre, die Requests noch besser absichern kann.
Beispiele zur Generierung befinden sich im Code-Repository für diesen Blogpost (Dateien signlink.*
).
Achtung Falle: nginx verwendet als Hash in der URL nicht den MD5 in hexadezimaler Schreibweise, sondern die base64-encodierte Variante der Binärversion mit Anpassungen.
Tipp: Während der Fehlersuche kann es hilfreich sein, den String zurückzugeben, welchen nginx zur Validierung verwendet. Dies hilft Annahmen bezüglich des Pfades oder anderer Variablen sicherzustellen.
Was nun nehmen?
Wie bei vielem: Hängt davon ab. Im konkreten Projekt für Tramino werden wir wahrscheinlich beide Varianten verwenden:
- In Anwendungsfall A brauchen wir stabile (=ändern sich nicht) URLs, welche jedoch nur durch uns generierbar sein dürfen, da das Backend hinter den URLs viel Dynamik erlaubt.
- In Anwendungsfall B brauchen wir zwingend eine Signierung mit ablaufenden URLs, da es sich um sensitive Daten handelt die nicht jeder abrufen darf.
Generell rate ich für die meisten Anwendungsfälle für die Generierung über signierte Parameter, da die Hashed-URL-Variante keine Möglichkeit anbietet, einen Ablauf der URLs zu definieren. Dies kann jedoch in manchen Anwendungsfällen in Ordnung sein, etwa wenn der Zugriff auf temporäre Dateien gewährt werden soll, welche nach dem Ablauf direkt vom Server gelöscht werden.
Da eine spätere Änderung von URLs häufig schwierig und arbeitsaufwändig ist, sollte diese Entscheidung bereits vor dem ersten Deployment einer Anwendung getroffen sein.
Abschluss
Durch das Schreiben dieses Blogposts habe ich viel über dieses Modul lernen, und dadurch auch das Konzept für unser Webseiten-Storage weiter verfeinern können.
Ich hoffe diese Informationen helfen auch meinen Lesern, die Auslieferung ihrer Dateien besser abzusichern.
Für Feedback: Meine E-Mail-Adresse ist unter "Über" :-)
Quellen und Referenzen