Azure Blob Storageのユーザ委任SASを使いこなす

これを読めばAzure Blob Storageのユーザ委任SASマスターです。

Azure Blob Storageのアクセス方法の歴史
-----------

なぜアクセス方法が様々あるのか、というのを知ると選ぶのが簡単になるので書きます。

アクセスの仕方は概ね4つです。Azure Storageでは「承認」と呼んでいます。

https://learn.microsoft.com/ja-jp/azure/storage/common/authorize-data-access?toc=%2Fazure%2Fstorage%2Fblobs%2Ftoc.json&bc=%2Fazure%2Fstorage%2Fblobs%2Fbreadcrumb%2Ftoc.json&tabs=blobs#authorization-for-data-operations

- Microsoft Entra ID
- 共有キー (ストレージアカウントキー)
- Shared Access Signature (SAS)
- 匿名読み取りアクセス（論外なので触れません）


一番大きいのは、2019年3月に Entra ID(旧:Azure AD)によるアクセス制御がGAになったところでしょう。

https://azure.microsoft.com/en-us/blog/azure-storage-support-for-azure-ad-based-access-control-now-generally-available/

それ以前はアカウントキー,SAS,パブリックアクセスさせるしか術がなかったと記憶しています。そのSASもアカウントキーを使うサービスSASかアカウントSASのみです。

その後、2019年9月にユーザ委任SASがプレビューで出ます。Entra ID認証がGAしたあとです。

https://azure.microsoft.com/ja-jp/blog/announcing-user-delegation-sas-tokens-preview-for-azure-storage-blobs/

作られた目的を見ても、アカウントキーを切り離すためということは想像できるでしょう。
ユーザ委任SASがないとSASで実現できていたパターンがカバーできなくなりますからね。

そして現在は[アカウントキー自体を使わない設定](https://learn.microsoft.com/ja-jp/azure/storage/common/shared-key-authorization-prevent?tabs=portal)も提供されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/cc287638-38bd-cf10-f41f-d64d6cc7c2f4.png)

これを設定し、SASが必要な場面にはユーザ委任SASを使い、アカウントキーを使わないようにすることが[Azure Storageのセキュリティベースライン](https://learn.microsoft.com/en-us/security/benchmark/azure/baselines/storage-security-baseline#azure-ad-authentication-required-for-data-plane-access)にも上がっています。

歴史をたどると、以下のように判断できるようになるでしょう。

- まずはEntra ID 認証で操作する
- SASが最適ならユーザ委任SASを使う
- アカウントキーを使うのは悪


SASの基礎知識
--------

細かい説明はドキュメントに任せて端折っていきます。

### SAS (共有アクセス署名) の概要と役割

概要をご覧ください

https://learn.microsoft.com/ja-jp/rest/api/storageservices/delegate-access-with-shared-access-signature

ざっくり上げます。

- Azure Storageのサービス、コンテナ、オブジェクトに対する操作を **SASを知っている人に限り一時的に許可できる** 仕組み
- 直接Azure Storageのサービスと通信できる
- SASには必要に応じて操作対象、有効期間、IP制限を含めることができる
- SASには、アカウントSAS, サービスSAS, ユーザ委任SASがある

### ユーザ委任SAS: 特徴と他のSASとの違い

https://learn.microsoft.com/ja-jp/rest/api/storageservices/create-user-delegation-sas

他のSASとの違いをざっくり上げます

- オブジェクトの操作に対してSASを発行できる
- ユーザ委任キーを使う。ストレージアカウント キーを使わない
- ユーザ委任キー自体の有効期限は最大でも7日後まで

「オブジェクトの操作のみ？サービスやコンテナに対する操作はできないのか？」というのはあるかもしれません。しかしそれはEntra ID認証でできるはずです。どうしても必要であればアカウントSAS等を使うしかありませんが、新規で採用することはないと思います。

ユーザ委任SASの利用
----------

### ユーザ委任SASの活用場面とメリット

では、SASを使うべき場面って何なのか？SASがEntra IDと比べて優れている点は以下だと考えます。

- 特定のBlobに、特定の期間だけ、読み取り・書き込みをさせたい
- Azure Storage と直接やりとりできることによる高パフォーマンス

このわかりやすい例は「ブラウザを介したファイルのアップロード/ファイルのダウンロード」だと思われます。

（これ以外が思いつかないので、何かいい使い方があれば教えていただきたい…）

これにくわえ、アカウントキーに依存しないのはユーザ委任SASのみです。

### ユーザ委任キーを使った実装をするには

以下にドキュメントがあり、とてつもない量ですが、やっていることは、「キーの取得」と「設定（対象、操作権限、有効期間の指定）」と「設定に対する署名処理」だけです。

https://learn.microsoft.com/ja-jp/rest/api/storageservices/create-user-delegation-sas

このドキュメントの内容はAzureが決めたお作法なので、覚える労力に見合いません。

SDKを通してどう実装するかを見るとすぐにわかるはずです。SDK利用者は次の各言語の実装を参照してください。

- [.NET を使用してコンテナーまたは BLOB 用のユーザー委任 SAS を作成する](https://learn.microsoft.com/ja-jp/azure/storage/blobs/storage-blob-user-delegation-sas-create-dotnet?tabs=packages-dotnetcli%2Ccontainer)
- [Java を使用してコンテナーまたは BLOB 用のユーザー委任 SAS を作成する](https://learn.microsoft.com/ja-jp/azure/storage/blobs/storage-blob-user-delegation-sas-create-java?tabs=container)
- [Azure Blob Storage と JavaScript を使用してユーザー委任 SAS トークンを作成する](https://learn.microsoft.com/ja-jp/azure/storage/blobs/storage-blob-create-user-delegation-sas-javascript)
- [Python を使用してコンテナーまたは BLOB 用のユーザー委任 SAS を作成する](https://learn.microsoft.com/ja-jp/azure/storage/blobs/storage-blob-user-delegation-sas-create-python?tabs=container)
- [Golang - Example (UserDelegationSAS)](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/storage/azblob@v1.5.0/sas)

実装する際は、必ずSDKを通して署名処理を行いましょう。

なお、キーの中身はキーID(SignedOid)とキー値(Value)が含まれています。このうち**キー値は秘匿情報**であり絶対に漏らしてはなりません。SDKを使えばまず漏れるような実装にはなりません。

もし署名処理を理解したい場合、汎用性の高く、理屈も単純で、いろんなところで使われている[HMAC](https://ja.wikipedia.org/wiki/HMAC)を勉強するとよいです。

### 例:Blobのダウンロードをユーザ委任SASへ組み込む

例えばBlobにあるファイルのダウンロードを考えます。だれがダウンロードできるべきか判断したりする必要があると、以下のような実装になるでしょう。

```mermaid
sequenceDiagram
    
    participant B as Browser
    participant A as Web API
    participant S as Blob Storage
    
    B->>+A: ダウンロード要求
    A->>A: アクセス権限の確認
    A->>+S: BlobのGET処理
    S->>-A: ストリーム返却
    A->>-B: ストリーム返却

```

ユーザ委任SASを使う場合は、少し手数が増えますが、以下のように実装することになります。

```mermaid
sequenceDiagram
    
    participant B as Browser
    participant A as Web API
    participant S as Azure Blob Storage
    

    B->>+A: ダウンロード要求（Blobを特定できる情報を含める）
    A->>A: アクセス権限の確認
    A->>A: ダウンロード対象BlobのURLを生成
    A->>+S: ユーザ委任キー取得
    S->>-A: キー発行
    A->>A: SASトークンの生成
    A->>-B: SAS付きダウンロード対象BlobのURLを返却

    B->>+S: SAS付きダウンロード対象BlobのURLでアクセス
    S->>-B: ストリーム返却
```

こういう感じになります。APIも増えて手数も増えてちょっと手間に見えますが、ユーザへ快適なダウンロード速度を与えることができます。

Blobのアクセスのセキュリティの担保はSASトークンで取ります。SASトークンを知っているのはリクエストしてきたユーザとアプリのみです。

SASの期限に関してはリダイレクトが終わるまでしか必要ないので非常に短くても問題ありません。短ければユーザがうっかりURLを漏らしてもすぐ失効になるので流出を防げます。

先にSASを発行して画面に返す（ファイルの一覧APIに含めて返す等）という手もありますが、ユーザがその画面でどれぐらい放置するかもわからないためSASの期限を延ばしがちになりますし、単純にSASの署名自体を行う処理が無駄になる可能性が高いので、必要になるまであと回しにさせるのがよいです。でも、要件にマッチするならばやっても良いと思います。



小ネタ
-----------

まとめられなかったものとかです。

### ユーザ委任キーの期限

ユーザ委任キーはAPIを通して発行し、有効期間も定める必要があります。最大7日間です。

あくまでユーザ委任キーの有効期限なので、SASの有効期限は別扱いです。

つまり、ユーザ委任SASには有効期限が2つあるということになります。基本的にユーザ委任キーの有効期限に納まるようにSASの有効期限を設定することになります。もちろん、どちらかでも期限外になれば使えなくなります。

複数のSASに署名するので少し期間を長めにする、リクエストをまたいで再利用したいので長めに設定する、といった事は可能です。ここはセキュリティとパフォーマンスのトレードオフといえます。後述します。




### ユーザ委任キーのLimitやQuotaについて

ドキュメントに明記がなく、未調査です…(秒間どれぐらい作れるか？最大数があるか？等)

手元で試している分にはほぼ制限はないように見えます（一般的な Rate Limitに準ずるか？）

### ユーザ委任キーのコストとオーバーヘッド

- 料金
	- 「その他操作」扱いなので 10000回の操作で$0.004 かかります
	- 操作数によりますが、これが問題になることはあまりありません
- 通信
	- ユーザ委任キーの取得にはAPIリクエストが必要で、300msぐらいかかります。結構遅い

通信オーバーヘッドを考えると、せめて1リクエスト中に1回には収めたいところです。もし複数のBlobにSASをつける要件があるなら、複数個をまとめて受け付けて返すようなバッチ処理を提供するのも良いかもしれません。

ユーザ委任キー自体は使いまわしてもよいのでポリシーが許すならリクエストを跨いで利用できるようオンメモリにキャッシュするのもいいでしょう。



### ユーザ委任キーのセキュリティ

ユーザ委任キーが漏れると任意の操作が行える署名を生成できてしまいます。これは該当のストレージアカウントに対するコンテナの読み書きができてしまうことを意味しますので、絶対に漏らしてはなりません。

一応、漏れたら期限まで出血し続けてしまうというのは良くないためか、[ユーザ委任キーの失効](https://learn.microsoft.com/en-us/rest/api/storagerp/storage-accounts/revoke-user-delegation-keys?view=rest-storagerp-2023-05-01&tabs=HTTP)という操作が行えます。これはストレージアカウント単位で行うことになります。

しかし結局失効するまでの間は自由に悪用できてしまう状態です。なので発覚したらとりあえず失効して止血、そのあとに影響調査や原因調査を行う必要があります。そもそも流出してから発覚するまでのリードタイムはおそらく推定することが難しいでしょう。

こういった流出に対してはできることはあまりありません。

- あらかじめ発行したキーIDはログに記録しておきます。システム以外から漏れているかどうかに必要です（他のEntra IDでキーを発行したかどうか区別するため。通常はシステムで発行したユーザ委任キーIDを使ったアクセスか、システム自身のアクセスのみになるはずです。）

ただ、私個人の意見では、キー値の流出はSDKを適切に使い、外部ストレージに保存しない限り、ほぼ起きえない事象ともとらえています。

ストレージアカウントキーの流出のリスクは、以下が考えられます。（Azureは完全に信頼するものとします）

- 意図しないユーザにAzure Portalから見えてしまう（App Serviceの環境変数など）
- 開発者がインフラの設定ファイルに記載されている内容が見えてしまう（手順書やbicepやterraformなど）
- 実装不備でログにストレージアカウントキーを出力し、ログを閲覧できる者に見られてしまう（デバッグ用のつもりのログ出力が本番で動くなど）
- 脆弱性で任意コード実行を行われ盗み取られた

ユーザ委任キーで考えると、このうち上2つはそもそも設定するものではないので起きません。3つ目は起きえますが、そういう実装をしなければいいです。コードレビューで頑張るしかありません。4つ目は基本起きないものであるべきですので、利用ライブラリはもちろん外部の入力に常に注意を払いましょう。


### ユーザ委任キーの期間とセキュリティとパフォーマンス

セキュリティポリシーによりますが、私は上記の通り、ユーザ委任キーをある程度使いまわしても問題ないものと考えています。

それによるAPIを呼びだしの回数を減らしてパフォーマンスをなるべく良くしたいです。

具体的には、期限を60分程度にして、ストレージアカウント単位でオンメモリにキャッシュさせます。

ということで、JavaならConcurrentHashMapのシングルトンでもっておくという実装例です。Spring BootのServiceとして書いてますが、ちょっと書き換えれば使えるでしょう。

https://gist.github.com/fukasawah/6084c04b4d5234e44699f3e4a7c33949

外部ストレージに置けばインスタンス間で共有できますが、Redisの内容が流出したときに影響を受けてしまう（アタックサーフェスを増やす）リスクが生まれます。そもそも再生成は抑えたいが再生成自体のコストもそこまで高いわけではないのでリスク見合わないよね、という考えです。

### ユーザ委任キーやSASの期限切れが起きたらどうなるのか？

アクセスを処理するタイミングで期限のチェックを行われるので、リクエスト時に期限が切れていれば弾かれます。期限内ならリクエストが受け付けられ、レスポンス中に期限が切れても問題なく処理されます。

ただ、並列アップロードする場合は、必要に応じて複数回リクエストするので、そのリクエストをするタイミングで期限が切れていてはいけません。ユーザの回線の調子にもよるので、対策としてはユーザ委任キーとSASの期限を長めにするしかないです。


### ブラウザから並列アップロードするには？

SAS関係ないですが。

モダンなフロントエンドフレームワークを使っているならば普通に`npm install`してビルドすれば使えるはずです。

- [new BlockBlobClient(SAS付きURL)](https://learn.microsoft.com/ja-jp/javascript/api/%40azure/storage-blob/blockblobclient?view=azure-node-latest#constructors)して
- [uploadData](https://learn.microsoft.com/ja-jp/javascript/api/%40azure/storage-blob/blockblobclient?view=azure-node-latest#@azure-storage-blob-blockblobclient-uploaddata)メソッドを呼ぶだけ
　　- 進捗用のコールバック(onProgress)も、AbortSignalもsignalで与えられます

また、アップロード処理はWebWorkerに切り出せるといいのですが、JavaScript SDKはDOMParserに依存しており、WebWorkerではDOMParserが使えないため、実装を用意する必要があります。

https://learn.microsoft.com/ja-jp/javascript/api/overview/azure/storage-blob-readme?view=azure-node-latest#web-workers

ドキュメントでは`jsdom`を上げていますが、結構サイズが大きくなってしまいました。また、DOMの細かい操作(querySelector)などは不要でありオーバースペック感があります。

代替を探したところ、[`@xmldom/xmldom`](https://github.com/xmldom/xmldom)でPolyfillでき、Polyfill実装のサイズも70KBぐらいで済みました。これ以外の有力な実装は無い気がします。
WebWorkerに切り出せるとスループットの向上とアップロード中の画面描画かなり改善されるはずです。


また、事情がありビルドできない環境でJavaScriptを直接書く必要がある場合は、次を参考に頑張ってバンドルされたJavaScriptファイルを作る必要があります。

https://github.com/Azure/azure-sdk-for-js/blob/main/documentation/Bundling.md

何もしないと1.5MBと結構大きいサイズになりますが、rollupとterserでtree-shakingとminifyを掛けると400KBぐらいになります。

バンドルファイルを作るためのリポジトリを置いておきました。ご利用ください。

https://github.com/fukasawah/js-azure-storage-blob-bundle