Wie funktionieren SAS Token mit Azure Blob Storage
07.07.2020, Christian Mäder

Azure Blob Storage ist ein Service in Azure, in dem Dateien abgelegt werden können. Das Angebot ist vergleichbar mit dem bekannten S3 Storage von Amazon Web Services (AWS).

Mittels SAS Token können Client-Applikationen direkt Manipulationen auf dem Azure Blob Storage durchführen. Diese Token sind zeitlich begrenzt gültig und legen genau fest, an welchen Dateien welche Manipulationen durchgeführt werden dürfen.

Dieser Artikel erläutert, wie solche SAS Tokens eingesetzt werden und wann dies sinnvoll ist.

Azure Blob Storage

Wird die Azure Cloud genutzt, kommt fast immer auch eine Azure Storage Instanz zum Einsatz. Es gibt verschiedene Ausprägungen im entsprechenden Produkt. In diesem Artikel dreht sich jedoch alles um den Azure Blob Storage.

Der Hauptzweck dieses Produktes ist es, darin mittels einer REST API kleine und grosse Dateien abzulegen – und später wieder abzurufen. Es ist vergleichbar mit dem Produkt Simple Storage Service von Amazon Web Services (AWS), besser bekannt als S3, welches ganz ähnlich funktioniert und auch über eine REST API angesprochen wird.

Eine Instanz eines Blob Storage wird von Microsoft Account genannt. Ein Account kann mehrere Container haben, und in diesen Containern werden verschiedene Blobs abgelegt. (Blob ist eine in der Informatik gebräuchliche Abkürzung für Binary Large Object.) In diesem Artikel entspricht eine Datei jeweils einem Blob, was vermutlich auch der gebräuchlichste Anwendungsfall ist.

Visualisierung der Hierarchie der Begriffe Account, Container und Blob.

Jeder Storage Account hat einen Azure-weit eindeutigen und nicht veränderbaren Namen. Auf die API eines bestimmten Storage Accounts wird über diesen Namen zugegriffen. Man setzt dazu Requests auf die URL http://storageaccountname.blob.core.windows.net ab.

Container in einem Storage Account können öffentlich gemacht werden. Dann können alle Leute über das Internet ohne Authentifizierung auf die abgelegten Dateien zugreifen, wenn sie den entsprechenden Pfad zur Datei wissen (oder erraten). Im Normalfall sind die Container jedoch nicht öffentlich zugänglich. Dann braucht es für jeden Request ein gültiges Access Token.

Von diesen Access Tokens gibt es pro Storage Account genau zwei. Die Tokens sind wie Passwörter zu verstehen: Wer das Token besitzt, hat Zugriff auf alle Dateien des gesamten Storage Accounts. Möchten Benutzer also Dateien vom Blob Storage beziehen oder dorthin hochladen, dann sollte nicht jedem Benutzer das eigentliche Access Token bekannt gegeben werden. Denn dann könnten alle Benutzer alle Dateien ansehen, verändern oder unkontrolliert neue Dateien hinzufügen.

Shared Access Signature

Dies ist der Punkt, wo die Shared Access Signatures (kurz SAS) ins Spiel kommen, um die es in diesem Artikel geht. Dabei handelt es sich um eine spezielle Art von Access Tokens, die einer Drittpartei (z.B. einer Benutzer-Applikation) übergeben werden können, um damit vordefinierte Aktionen während einer beschränkten Zeit auf dem Storage Account auszuführen.

Die Aussage vordefinierte Aktionen während einer beschränkten Zeit im letzten Satz ist sehr wichtig. Einerseits haben SAS-Tokens eine eingeschränkte Gültigkeit: Diese beginnt zu einem bestimmten Zeitpunkt (welcher auch in der Zukunft liegen kann) und endet irgendwann wieder. Andererseits muss genau angegeben werden wofür ein SAS-Token gültig ist: Zum Beispiel für welchen Account, Container oder Blob eine Aktion erlaubt ist und welche Aktionen überhaupt zugelassen sind (beispielsweise anlegen/hochladen, lesen, bearbeiten/überschreiben, löschen, umbenennen, und so weiter).

Es gibt verschiedene Arten von SAS Token. Diese können an einen bestimmten Azure Dienst gekoppelt sein, an einen Benutzer des Azure Active Directory, oder sie können Ad-Hoc sein. Letzteres bedeutet, dass die Token grundsätzlich anonym sind und auch an Dritte herausgegeben werden können, die ansonsten nicht in Azure bekannt sind.

Dieser Artikel beschäftigt sich nur mit den Ad-Hoc Token. Um ein solches Token zu erstellen, müssen die Gültigkeit und die Aktionen, die erlaubt sind, definiert werden. Dazu werden Key/Value-Paare gebildet. Alle Values werden danach in einem vordefinierten Format zu einem String zusammengefügt, welcher mit HMAC-SHA256 signiert wird. Der Schlüssel für den HMAC-SHA256-Algorithmus ist der Storage Account Key.

Die Server-Anwendung kennt den Storage Account Key und berechnet damit das SAS Token, welches der Client-Anwendung auf Anfrage übermittel wird. Somit braucht diese für Aktionen auf dem Azure Blob Storage den eigentlichen Storage Account Key nicht zu kennen. Trotzdem kann sie mit dem SAS Token die vordefinierten Aktionen auf dem Azure Blob Storage Account durchführen. Wenn dies im SAS Token erlaubt ist, kann sie so zum Beispiel eine Datei mit einer HTTP PUT-Aktion hochladen:

# Um den Befehl 'http' nutzen zu können muss https://httpie.org/ installiert sein.
cat sasblob.txt | \
 http PUT "https://storageaccountname.blob.core.windows.net/sascontainer/sasblob.txt?sv=2019-02-02&st=2019-04-29T22%3A18%3A26Z&se=2019-04-30T02%3A23%3A26Z&sr=b&sp=rw&sip=168.1.5.60-168.1.5.70&spr=https&sig=koLniLcK0tMLuMfYeuSQwB%2bBLnWibhPqnrINxaIRbvU%3d"

Token Deep-Dive

Alle, die sich nicht so sehr für die technischen Details interessieren, die können direkt zum Use Case springen.

Dieser Teil beschreibt, wie ein SAS-Token aufgebaut ist und wie es erstellt wird. Die folgende URL enthält zum Beispiel ein SAS-Token:

https://storageaccountname.blob.core.windows.net/sascontainer/sasblob.txt?sv=2019-02-02&st=2019-04-29T22%3A18%3A26Z&se=2019-04-30T02%3A23%3A26Z&sr=b&sp=rw&sip=168.1.5.60-168.1.5.70&spr=https&sig=koLniLcK0tMLuMfYeuSQwB%2bBLnWibhPqnrINxaIRbvU%3d

Alle URL Parameter, die nach dem Fragezeichen kommen, machen hierbei das SAS Token aus. Aufgeschlüsselt bedeuten die einzelnen Parameter folgendes:

  • https://storageaccountname.blob.core.windows.net/sascontainer/sasblob.txt ist die URL zum entsprechenden Blob Objekt.
  • sv=2019-02-02 heisst, dass die Version 2019-02-02 der REST API verwendet wurde, um die Signatur zu berechnen.
  • st=2019-04-29T22%3A18%3A26Z enthält die Startzeit der Gültigkeit in UTC; hier 29. April 2019, 22:18:26.

    Wegen der URL Kodierung sieht der Wert sehr kryptisch aus. Ohne URL Kodierung ist dieser schon merklich lesbarer:
    st=2019-04-29T22:18:26Z

  • se=2019-04-30T02%3A23%3A26Z enthält die Endzeit der Gültigkeit in UTC; hier 30. April 2019, 02:23:26.

    Auch dieser Wert sieht wegen der URL Kodierung sehr kryptisch aus. Ohne URL Kodierung ist auch dieser lesbarer:
    st=2019-04-29T02:23:26Z

  • sr=b zeigt an, dass die Rechte für einen bestimmten Blob gelten.
  • sp=rw zeigt an, dass Token-Inhaber*innen auf dem Blob lesen und schreiben dürfen.
  • sip=168.1.5.60-168.1.5.70 bedeutet, dass nur Benutzer, deren Anfragen aus dem entsprechenden IP-Bereich 168.1.5.60 bis 168.1.5.70 stammt, das Token nutzen dürfen.
  • spr=https heisst, dass nur TLS-verschlüsselte Verbindungen erlaubt sind.
  • sig=koLniLcK0tMLuMfYeuSQwB%2bBLnWibhPqnrINxaIRbvU%3d ist die mit Base64-encodierte Signatur, die mit dem HMAC-SHA256-Verfahren generiert wurde.

Zum Generieren der Signatur werden die obig definierten Einschränken zu einem String zusammengeführt. Dieser wird UTF-8 encodiert und danach mit dem HMAC-SHA256 Algorithmus signiert, wobei der Storage Account Key als geheimer Schlüssel verwendet wird.

Um die Signatur zu überprüfen muss der Azure Blob Storage Server genau dieselbe Berechnung machen. Dazu nimmt er die übermittelten Werte aus der URL, sowie den ihm bekannten Storage Account Key, und berechnet damit die Signatur selber mit demselben Algorithmus. Die Aktion wird nur durchgeführt, wenn die übermittelte mit der berechneten Signatur übereinstimmt.

Deshalb ist der Aufbau des zu signierenden Strings vorgegeben. Da dieser je nach API Version ein wenig anders aufgebaut ist, muss die API Version im sv Parameter in der URL stets angegeben werden. Der zu signierende String im obigen Beispiel benutzt die API Version 2019-02-02 und ist wie folgt aufgebaut:

StringToSign = signedPermissions + "\n" +
               signedStart + "\n" +
               signedExpiry + "\n" +
               canonicalizedResource + "\n" +
               signedIdentifier + "\n" +
               signedIP + "\n" +
               signedProtocol + "\n" +
               signedVersion + "\n" +
               signedResource + "\n"
               signedSnapshotTime + "\n" +
               rscc + "\n" +
               rscd + "\n" +
               rsce + "\n" +
               rscl + "\n" +
               rsct

Die Werte der einzelnen Variablen sind dabei gleich anzugeben, wie sie auch in der URL angegeben werden. Zum Beispiel wird signedPermissions mit dem Wert rw ersetzt.

Eine Spezialität ist das Zustandekommen des Wertes für canonicalizedResource. Dem Pfad zur Datei muss noch das Präfix /blob sowie der Account Name vorangestellt werden, wie nachfolgend gezeigt:

URL = "https://storageaccountname.blob.core.windows.net/sascontainer/sasblob.txt"
canonicalizedResource = "/blob/storageaccountname/sascontainer/sasblob.txt"

Unbenutzte Felder bleiben leer. Im Beispiel von oben ergibt dies den nachfolgenden StringToSign. Dieser muss anschliessend in UTF-8 encodiert werden.

stringToSign = UTF8(
  "rw"                     + "\n" +   // signedPermissions
  "2019-04-29T22:18:26Z"   + "\n" +   // signedStart
  "2019-04-30T02:23:26Z"   + "\n" +   // signedExpiry
  "/blob/storageaccountname/sascontainer/sasblob.txt" + "\n" + // canonicalizedResource
  ""                       + "\n" +   // signedIdentifier
  "168.1.5.60-168.1.5.70"  + "\n" +   // signedIP
  "https"                  + "\n" +   // signedProtocol
  "2019-02-02"             + "\n" +   // signedVersion
  "b"                      + "\n" +   // signedResource
  ""                       + "\n" +   // signedSnapshotTime
  ""                       + "\n" +   // rscc
  ""                       + "\n" +   // rscd
  ""                       + "\n" +   // rsce
  ""                       + "\n" +   // rscl
  "")                                 // rsct

accountKey   = Base64_Decode("jkjRQqRC7Cp3dQhbBegWUOPTfSbDhpSRXslbIHi7XWaPoVEbKOACGhQO7ENqs4r+6wobqZXOEAznojEsWnbGJQ==")

hmac         = HMAC_SHA256(accountKey, stringToSign)
signature    = URL_Encode(Base64_Encode(hmac))
// ==> "koLniLcK0tMLuMfYeuSQwB%2bBLnWibhPqnrINxaIRbvU%3d"

Auf cryptii.com können Pipelines mit kryptografischen Funktionen erstellt werden, um das obige Beispiel relativ einfach nachzustellen. Nachfolgend die Hex-codierte Binärversion des AccountKey, um diesen einfach als HMAC-Key eingeben zu können:

8e48 d142 a442 ec2a 7775 085b 05e8 1650
e3d3 7d26 c386 9491 5ec9 5b20 78bb 5d66
8fa1 511b 28e0 021a 140e ec43 6ab3 8afe
eb0a 1ba9 95ce 100c e7a2 312c 5a76 c625

Zudem der zusammengesetzte StringToSign – man beachte die sechs Leerzeilen am Schluss!

rw
2019-04-29T22:18:26Z
2019-04-30T02:23:26Z
/blob/storageaccountname/sascontainer/sasblob.txt

168.1.5.60-168.1.5.70
https
2019-02-02
b






Codebeispiel (Server)

Das ganze Verfahren des Signierens muss aber nicht selber implementiert werden. Microsoft hat dies in seinen Libraries schon gemacht. Damit lassen sich die Tokens auch schnell und zuverlässig erzeugen. Das nachfolgende Codebeispiel nutzt die Angaben aus den oberen Beispielen:

private const string StorageAccountConnectionString =
  "DefaultEndpointsProtocol=https;" +
  "AccountName=storageaccountname;" +
  "AccountKey=jkjRQqRC7Cp3dQhbBegWUOPTfSbDhpSRXslbIHi7XWaPoVEbKOACGhQO7ENqs4r+6wobqZXOEAznojEsWnbGJQ==";
private readonly CloudStorageAccount storageAccount =
  CloudStorageAccount.Parse(StorageAccountConnectionString);

private string GetSasParameters(
  string containerName, string blobName)
{
    var startsOn = DateTimeOffset.Parse("2019-04-29T22:18:26Z");
    var expiresOn = DateTimeOffset.Parse("2019-04-30T02:23:26Z");
    var ipRange = new SasIPRange(IPAddress.Parse("168.1.5.60"),
                                 IPAddress.Parse("168.1.5.70"));
    var blobSasBuilder = new BlobSasBuilder
    {
      BlobContainerName = containerName,
      BlobName = blobName,
      StartsOn = startsOn,
      ExpiresOn = expiresOn,
      Resource = "b",
      IPRange = ipRange,
      Protocol = SasProtocol.Https,
    };
    blobSasBuilder.SetPermissions(BlobSasPermissions.Write |
                                  BlobSasPermissions.Create);

    var storageAccountCredentials = storageAccount.Credentials;

    var incomingFileStorageCredentials =
      new StorageSharedKeyCredential(
        storageAccountCredentials.AccountName,
        storageAccountCredentials.ExportBase64EncodedKey());

    var blobSasQueryParameters =
      blobSasBuilder.ToSasQueryParameters(incomingFileStorageCredentials);
    return blobSasQueryParameters.ToString();
}

Use Case

Auf den ersten Blick sieht das Implementieren und Nutzen von SAS Tokens nach viel Aufwand aus. Es gibt aber einige berechtigte Gründe, seine Anwendung so zu gestalten. Zuerst aber möchte ich den herkömmlichen Ansatz erläutern, um dann daran die Vorteile des indirekten Ansatzes mittels SAS Tokens aufzuzeigen.

Als Beispiel soll eine einfache Dokumentenablage dienen, die über das Web erreichbar ist. Eine solche Anwendung hat – herkömmlicherweise – sämtliche Dateien selber direkt angenommen, um diese auf das lokale Dateisystem zu schreiben. Wurde ein Dokument angefragt, hat die Applikation das Dokument vom Dateisystem gelesen und dem Client ausgeliefert.

Eine grafische Darstellung, wie der Fluss der Dateien bei lokalem Speicher ist.

Mit dem Umzug in die Azure Cloud wurde die Anwendung so erweitert, dass diese die Dateien nicht mehr vom lokalen Dateisystem liest, respektive dorthin schreibt, sondern dass die Dateien von der Anwendung aus dem Azure Blob Storage gelesen werden, respektive dorthin geschrieben werden.

Eine grafische Darstellung, wie der Fluss der Dateien bei einer direkten Portierung auf Azure Blob Storage ist.

Mit dieser Umsetzung ergeben sich jedoch einige Nachteile:

  • Häufig gibt es Einschränkungen bezüglich der Grösse der HTTP Requests vom Client zur Applikation. Bei Azure Functions ist dies zum Beispiel aktuell 100 MB pro HTTP Request.
  • In der Cloud wird oft der Transfer von Daten verrechnet. Die Applikation nimmt nun eine Datei entgegen und schreibt diese in ein entferntes Dateisystem. Dabei fallen die Transfergebühren zweimal an – einmal vom Client zur Anwendung und einmal von der Anwendung zum Azure Blob Storage. Dasselbe gilt natürlich auch beim Lesen der Dateien.
  • Eine Applikation kann nur eine bestimmte Anzahl von Anfragen gleichzeitig beantworten. Und jeder Up- und Download-Vorgang von der / zur Applikation bindet Ressourcen (wie Rechenzeit, Memory und Sockets). Diese Ressourcen stehen für die Beantwortung anderer Anfragen nicht zur Verfügung.
  • Wenn mehrere Dateien gleichzeitig hoch- bzw. heruntergeladen werden sollen, dann bindet dies noch mehr Ressourcen pro Benutzer.
  • Je nachdem wie die Applikation in der Cloud betrieben wird, wird die Rechenzeit sekundengenau abgerechnet. Wenn die Applikation nun damit beschäftigt ist, Dateien hoch- und herunterzuladen, kostet dies unnötig Geld.

Diese Nachteile lassen sich alle eliminieren, wenn Anwendungen so umgebaut werden, dass es ihre einzige Aufgabe (bezüglich des Datei-Up- und -Downloads) ist, ein SAS-Token auszustellen. Damit verbindet sich der Client anschliessend direkt mit dem Azure Blob Storage und kann so die nötigen Dateien beziehen – respektive dorthin übermitteln.

Damit können Dateitransfers beliebig parallelisiert werden, ohne dass dies die Ressourcen der Anwendung belastet. Up- und Downloads, die abgebrochen wurden, weil beispielsweise das Mobiltelefon vom Wifi aufs Mobilfunknetz gewechselt hat, können durch das Gerät wieder aufgenommen werden. So können auch sehr grosse Dateien ohne Probleme hochgeladen werden.

Weiter sind die Dateitransfers nicht mehr durch die Rechenleistung der Anwendung limitiert. Oder anders gesagt: Allenfalls kann die Anwendung herunterskaliert werden, sodass ihr weniger Rechenleistung zur Verfügung steht, weil dies nun für die übrig gebliebenen Aufgaben ausreicht. Das reduziert die laufenden Kosten weiter.

Eine grafische Darstellung, wie der Fluss der Dateien bei einer Portierung auf Azure Blob Storage ist, wenn SAS Token eingesetzt werden.

Codebeispiel (Client)

Das folgende Beispiel zeigt, wie ein BlobClient initialisiert wird, um damit eine Datei hochzuladen.

async System.Threading.Tasks.Task UploadFile() {
  const string sasToken = "sv=2019-02-02&st=2019-04-29T22%3A18%3A26Z&se=2019-04-30T02%3A23%3A26Z&sr=b&sp=rw&sip=168.1.5.60-168.1.5.70&spr=https&sig=koLniLcK0tMLuMfYeuSQwB%2bBLnWibhPqnrINxaIRbvU%3d"
  var file = new System.IO.FileInfo("sasblob.txt");
  var blobUri = new System.Uri("https://storageaccountname.blob.core.windows.net/sascontainer/sasblob.txt");

  var blobUriBuilder = new System.UriBuilder(blobUri)
  {
      Query = sasToken
  };

  var authorizedBlobUri = blobUriBuilder.Uri;
  var blobClient = new Azure.Storage.Blobs.BlobClient(authorizedBlobUri);

  await using var fileStream = file.OpenRead();
  var response = await blobClient.UploadAsync(fileStream, cancellationToken);
}

Erfahrungsbericht

Dieser Artikel ist nicht aus reiner Wissbegierde entstanden, sondern wir haben dieses Konzept auf die harte Tour kennen gelernt. Denn bald schon nach der direkten Portierung einer Applikation unseres Kunden Rodix zu Azure bekamen wir die Einschränkungen bezüglich der Grösse der HTTP Requests zu spüren.

Aufgrund dessen haben wir die entsprechende Applikation – sowie die dazugehörende Client-Applikation – wie oben beschrieben umgebaut. Mit der neuen Lösung haben wir bisher fast nur gute Erfahrungen gemacht. (Bei einem Endbenutzer gab es Probleme, weil die Azure Blob Storage URL auf dem ausgehenden Proxy Server noch nicht erlaubt war.)

Weiterführende Informationen

In der Azure Dokumentation ist es nicht immer einfach, die richtigen Informationen zu finden. Deshalb nachfolgend eine Liste mit für diesen Artikel relevanten Azure Seiten: