🦖

SASトークンを活用したセキュアなBlob Storageファイルアップロード設計

2024/12/07に公開

1. この記事の範囲

本記事では、Azure FunctionsとAzure Blob Storageを活用したファイルアップロードの設計変更について解説します。特に、SAS(Shared Access Signature)トークンを導入することで、Blob Storageのリソースに対して一時的かつ限定的なアクセスを付与する仕組みを活用します。この変更により、これまでFunctionsが担っていたアップロード処理をクライアント側に委譲し、システム全体の効率性とセキュリティを向上させます。

2. SASトークン導入による設計変更とその背景

2-1. 背景と課題

これまでの設計 では、Azure Functionsがクライアントからファイルを受け取り、Azure Blob Storageにアップロードする中継役を担っていました。この方法では、以下のような課題がありました。

  1. セキュリティリスク
    • ストレージアカウントキーや接続文字列をクライアントに共有することはできません。これらの情報が漏洩すると、Blob Storage全体にフルアクセスを許可することとなり、致命的なセキュリティリスクとなります。
  2. アクセス制御の課題
    • ストレージアカウントキーを使ったアクセスでは、すべてのBlobやコンテナに対して読み取り・書き込みなどのフルアクセス権が付与されるため、細かい権限管理ができません。
  3. パフォーマンスとコストの問題
    • Azure Functionsを中継役とする設計では、すべてのアップロードトラフィックがFunctionsを通過するため、以下のデメリットが発生しました
      • Functionsの負荷が増加し、リクエスト遅延が発生。
      • スケールアップに伴う実行コストの増加。
      • FunctionsとBlob Storage間のデータ転送が発生し、ネットワーク転送料が増大。

2-2. 解決策:SASトークンの導入

これらの課題を解決するため、SAS(Shared Access Signature)トークンを導入しました。

2-1. SASトークンとは

SASトークンは、Blob Storageリソースに対して一時的かつ限定的なアクセスを提供する仕組みです。以下のような柔軟なアクセス制御が可能です。

  • 有効期限を指定。
  • 特定のBlobやコンテナへのアクセス権の制限。
  • 権限(読み取り、書き込み、削除など)の細かい設定。
  • IPアドレスやHTTPSのみの制限を追加可能。

2-3. 設計変更の内容

2-3-1. 新しい設計

SASトークンを活用し、クライアントが直接Blob Storageにファイルをアップロードできるように設計変更を行いました。以下が新しいプロセスです。

  1. SASトークン生成APIをAzure Functionsで実装
    • クライアントがアップロードするファイル名をリクエストとして送信。
    • FunctionsがBlob Storageへの書き込み権限を持つ一時的なSASトークンを生成し、クライアントに返却。
  2. クライアントが直接Blob Storageにファイルをアップロード
    • 受け取ったSASトークン付きURLを使用してアップロードを実行。

2-3-2. 設計変更のメリット

  • セキュリティの向上:
    • クライアントにはSASトークンのみを公開し、ストレージアカウントキーの漏洩リスクを解消。
    • 必要最低限のアクセス権のみを付与し、権限管理を強化。
  • パフォーマンスの向上:
    • Azure Functionsの負荷を削減し、スケーラブルな設計を実現。
    • クライアントが直接Blob Storageにアクセスすることで、アップロード遅延を軽減。
  • コスト削減:
    • Functionsの実行時間が大幅に短縮され、実行コストを削減。
    • FunctionsとBlob Storage間のデータ転送料が不要になり、運用コストを削減。

3. 改修後のコード

using Azure.Storage;
using Azure.Storage.Blobs;
using Azure.Storage.Sas;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

namespace SampleFunctionApp
{
    public class GenerateSasTokenFunction
    {
        private readonly ILogger<GenerateSasTokenFunction> _logger;

        public GenerateSasTokenFunction(ILogger<GenerateSasTokenFunction> logger)
        {
            _logger = logger;
        }

        [Function("GenerateSasTokenFunction")]
        public static IActionResult Run(
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = "api/files/generate-sas")] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("GenerateSasTokenFunction triggered");

            try
            {
                // クエリパラメータまたはリクエストボディからファイル名を取得
                var fileName = req.Query["fileName"];
                if (string.IsNullOrEmpty(fileName))
                {
                    return new BadRequestObjectResult("File name is required.");
                }

                // ストレージ接続文字列とコンテナ名の設定
                var connectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage");
                var containerName = "your-container-name";

                // Blobクライアントを取得
                var blobServiceClient = new BlobServiceClient(connectionString);
                var containerClient = blobServiceClient.GetBlobContainerClient(containerName);

                // フォルダ構造を利用してBlob名を生成 
                var folderPath = $"uploads/{DateTime.UtcNow:yyyyMMdd}";
                var blobName = $"{folderPath}/{Path.GetFileNameWithoutExtension(file.FileName)}_{DateTime.UtcNow:HHmmss}{Path.GetExtension(file.FileName)}";

                // SASトークンを生成
                var sasBuilder = new BlobSasBuilder
                {
                    BlobContainerName = containerName,
                    BlobName = blobName,
                    Resource = "b", // Blobへのアクセス
                    ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(30) // 30分間有効
                };

                // 書き込み権限を付与
                sasBuilder.SetPermissions(BlobSasPermissions.Write);

                // SASトークン付きURLを作成
                var sasToken = sasBuilder.ToSasQueryParameters(new StorageSharedKeyCredential(
                    blobServiceClient.AccountName,
                    Environment.GetEnvironmentVariable("AzureStorageAccountKey"))).ToString();

                var sasUrl = $"{blobClient.Uri}?{sasToken}";

                // SAS URLをレスポンスとして返却
                return new OkObjectResult(new
                {
                    Message = "SAS token generated successfully.",
                    SasUrl = sasUrl,
                    BlobName = blobName,
                    Expiry = sasBuilder.ExpiresOn
                });
            }
            catch (Exception ex)
            {
                log.LogError($"Error in GenerateSasTokenFunction: {ex.Message}");
                return new ObjectResult(new { Error = "Failed to generate SAS token", Details = ex.Message })
                {
                    StatusCode = StatusCodes.Status500InternalServerError
                };
            }
        }
    }
}

アップロード例

using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

public class FileUploader
{
    public async Task UploadFileAsync(string sasUrl, string filePath)
    {
        using var httpClient = new HttpClient();
        using var fileStream = File.OpenRead(filePath);

        var content = new StreamContent(fileStream);
        content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

        // SAS URLを使ってPUTリクエストでアップロード
        var response = await httpClient.PutAsync(sasUrl, content);

        if (response.IsSuccessStatusCode)
        {
            Console.WriteLine("File uploaded successfully.");
        }
        else
        {
            Console.WriteLine($"File upload failed. Status code: {response.StatusCode}");
        }
    }
}

3-1. 改修のポイント

  • アップロード処理の削除:
    • 元のコードで行っていたファイルアップロード処理を削除。
    • FunctionsはSASトークンの生成に専念します。
  • SASトークン生成ロジックの追加:
    • BlobSasBuilderを使用して、書き込み専用のSASトークンを生成。
    • 必要に応じて有効期限やアクセス制限(例: IP制限やHTTPS通信の強制)を設定。
  • フォルダ構造を維持:
    • 元の設計と同様に、Blob名にフォルダ構造を反映するロジックを保持。
  • レスポンスの変更:
    • アップロード結果ではなく、生成されたSASトークン付きURLをクライアントに返却。
    • クライアントはSAS URLと有効期限を受け取り、アップロード処理を完結。

3-2. SASトークンのセキュリティ設定

IP制限やHTTPSの強制が設定されていないため、SASトークンが第三者に渡った場合、不正利用されるリスクがあります。IP制限やHTTPSの強制を追加する場合は以下。

sasBuilder.IPRange = new SasIPRange(IPAddress.Parse("192.168.0.1"), IPAddress.Parse("192.168.0.255"));
sasBuilder.Protocol = SasProtocol.Https; // HTTPSのみを許可

4. まとめ

この記事では、SASトークンの導入により、セキュリティ、パフォーマンス、コストの課題を解決しました。新しい設計では、クライアントが直接Blob Storageにアクセス可能となり、Functionsの負荷を大幅に軽減。さらに、必要なアクセス権のみをSASトークンで柔軟に管理できるため、セキュアで効率的なファイルアップロードを実現しました。
一方で、Azure FunctionsやBlob Storageの利用においては、SASトークン以外にも考慮すべきセキュリティ対策があります。例えば、Functionsのエンドポイント保護、CORS設定、キー管理の徹底、ストレージアカウントへのIP制限などが挙げられます。これらのプラクティスを深く理解し、より堅牢なシステム設計を目指すことが重要です。

今後も、Azureを活用したセキュリティ対策のベストプラクティスを学びながら、実践に活かしていきたいです。

Discussion