はじめに

Blob Storage をパブリックに公開しなければならない場面があったとします。このとき、アクセスを完全に遮断することはできなくても、「アップロードだけ」に権限を絞ることができれば、Blob Storage を経由した情報漏洩のリスクを軽減できるはずです。攻撃者や悪意のある内部者がファイルをダウンロードしたり一覧を取得したりする手段を奪えるからです。

しかしこの考えが実際に成立するかどうかは、RBAC の DataAction レベルで本当に操作を分離できるかどうかにかかっています。今回は「アップロードだけに権限を限定できるのか」を実機で検証するため、以下の 2 つのカスタム RBAC ロールを用意し、それぞれを割り当てたサービスプリンシパルで各操作の可否を確認しました。

ロール名 付与した DataAction
Custom Blob Write Only blobs/writeblobs/add/action のみ
Custom Blob Read Only blobs/read のみ

環境構成

セキュアな Blob Storage の設定として、以下を施しました。

項目 設定値
アクセスキー認証 無効(AllowSharedKeyAccess=False)
パブリック Blob アクセス 無効(AllowBlobPublicAccess=False)
ネットワーク DefaultAction Deny
認証方式 Entra ID 認証のみ(SAS・アクセスキー禁止)

ネットワークを DefaultAction Deny にしたため、ローカル端末からのアクセスはプライベートエンドポイント経由のみとなります。既存の VNet サブネットにプライベートエンドポイントを作成し、プライベート DNS ゾーン privatelink.blob.core.windows.net に A レコードを追加して名前解決できるようにしました。

ストレージアカウントの作成コマンドは以下のとおりです。

New-AzStorageAccount `
    -ResourceGroupName "rg-blob-rbac-eval" `
    -Name "stblobrbaceval01" `
    -Location "japaneast" `
    -SkuName "Standard_LRS" `
    -Kind "StorageV2" `
    -AllowBlobPublicAccess $false `
    -AllowSharedKeyAccess $false `
    -MinimumTlsVersion "TLS1_2"

プライベートエンドポイントの作成コマンドは以下の通りです。

$peSubnetObj = Get-AzVirtualNetworkSubnetConfig -ResourceId $peSubnetId

$privateLinkServiceConnection = New-AzPrivateLinkServiceConnection `
    -Name "plsc-storage" `
    -PrivateLinkServiceId $storage.Id `
    -GroupId "blob"

New-AzPrivateEndpoint `
    -ResourceGroupName "rg-blob-rbac-eval" `
    -Name "pe-storage-blob" `
    -Location "japaneast" `
    -Subnet $peSubnetObj `
    -PrivateLinkServiceConnection $privateLinkServiceConnection

カスタムロールの作成

書き込み専用ロール(Custom Blob Write Only)

blobs/writeblobs/add/action だけを DataAction として持つ最小権限ロールです。

$roleDefWrite = [Microsoft.Azure.Commands.Resources.Models.Authorization.PSRoleDefinition]::new()
$roleDefWrite.Name            = "Custom Blob Write Only"
$roleDefWrite.Description     = "ストレージ Blob に対する書き込み専用カスタムロール"
$roleDefWrite.IsCustom        = $true
$roleDefWrite.Actions         = @()
$roleDefWrite.DataActions     = @(
    "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write",
    "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/add/action"
)
$roleDefWrite.NotDataActions  = @()
$roleDefWrite.AssignableScopes = @("/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")

New-AzRoleDefinition -Role $roleDefWrite

作成後、Get-AzRoleDefinition -Name "Custom Blob Write Only" で確認すると以下のように表示されました。

Name             : Custom Blob Write Only
Id               : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
IsCustom         : True
DataActions      : {Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write,
                   Microsoft.Storage/storageAccounts/blobServices/containers/blobs/add/action}
AssignableScopes : {/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}

カスタムロール定義の補足

ロール定義で設定したプロパティについて補足します。

Actions はコントロールプレーン(Azure Resource Manager)に対する操作権限、DataActions はデータプレーン(実際のデータ)に対する操作権限です。Blob のアップロードやダウンロードはデータプレーン操作なので DataActions に設定します。Actions を空にすることで、ストレージアカウント自体の設定変更などのポータル管理操作は一切できない純粋なデータ操作専用ロールになります。

AssignableScopes はロールを割り当てできるスコープの範囲を制限するものです。今回はサブスクリプション全体を指定していますが、特定のリソースグループや個別のストレージアカウントのリソース ID に絞ることもできます。

読み取り専用ロール(Custom Blob Read Only)

blobs/read だけを DataAction として持つ最小権限ロールです。

$roleDefRead = [Microsoft.Azure.Commands.Resources.Models.Authorization.PSRoleDefinition]::new()
$roleDefRead.Name             = "Custom Blob Read Only"
$roleDefRead.Description      = "ストレージ Blob に対する読み取り専用カスタムロール"
$roleDefRead.IsCustom         = $true
$roleDefRead.Actions          = @()
$roleDefRead.DataActions      = @(
    "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read"
)
$roleDefRead.NotDataActions   = @()
$roleDefRead.AssignableScopes = @("/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")

New-AzRoleDefinition -Role $roleDefRead

作成後、Get-AzRoleDefinition -Name "Custom Blob Read Only" で確認すると以下のように表示されました。

Name             : Custom Blob Read Only
Id               : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
IsCustom         : True
DataActions      : {Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read}
AssignableScopes : {/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}

検証用サービスプリンシパルの作成とロール割り当て

書き込み用と読み取り用のサービスプリンシパルを Entra ID アプリケーションとして作成し、ストレージアカウントスコープで各ロールを割り当てました。

# 書き込み用サービスプリンシパルの作成
$spContributorApp = New-AzADApplication -DisplayName "sp-blob-rbac-contributor-test"
$spContributor    = New-AzADServicePrincipal -ApplicationId $spContributorApp.AppId
$spContributorSecret = New-AzADAppCredential `
    -ObjectId $spContributorApp.Id `
    -EndDate (Get-Date).AddDays(1)

# 読み取り用サービスプリンシパルの作成
$spReaderApp    = New-AzADApplication -DisplayName "sp-blob-rbac-reader-test"
$spReader       = New-AzADServicePrincipal -ApplicationId $spReaderApp.AppId
$spReaderSecret = New-AzADAppCredential `
    -ObjectId $spReaderApp.Id `
    -EndDate (Get-Date).AddDays(1)
$storage = Get-AzStorageAccount `
    -ResourceGroupName "rg-blob-rbac-eval" `
    -Name "stblobrbaceval01"

# 書き込み用サービスプリンシパル → Custom Blob Write Only
New-AzRoleAssignment `
    -ObjectId $spContributor.Id `
    -RoleDefinitionName "Custom Blob Write Only" `
    -Scope $storage.Id

# 読み取り用サービスプリンシパル → Custom Blob Read Only
New-AzRoleAssignment `
    -ObjectId $spReader.Id `
    -RoleDefinitionName "Custom Blob Read Only" `
    -Scope $storage.Id

動作検証

各サービスプリンシパルでコンテキストを切り替え、Blob のアップロード・一覧取得・ダウンロード・削除を試みました。各サービスプリンシパルへの切り替えは以下のパターンで行いました。

$cred = New-Object System.Management.Automation.PSCredential (
    $spContributorApp.AppId,
    ($spContributorSecret.SecretText | ConvertTo-SecureString -AsPlainText -Force)
)
Connect-AzAccount -ServicePrincipal -Credential $cred -TenantId $tenantId

ストレージコンテキストはアクセスキー認証が無効なため、Entra ID トークンを使う -UseConnectedAccount で取得します。

$ctx = New-AzStorageContext -StorageAccountName "stblobrbaceval01" -UseConnectedAccount

書き込み専用ロール(Custom Blob Write Only)での検証

アップロード(成功):

PS> Set-AzStorageBlobContent `
    -File "$env:TEMP\test.txt" `
    -Container "testcontainer" `
    -Blob "test.txt" `
    -Context $ctx `
    -Force

   AccountName: stblobrbaceval01, ContainerName: testcontainer

Name      BlobType  Length ContentType         LastModified
----      --------  ------ -----------         ------------
test.txt  BlockBlob 16                         2026-03-15 ...

一覧取得(失敗):

PS> Get-AzStorageBlob -Container "testcontainer" -Context $ctx

Get-AzStorageBlob:
This request is not authorized to perform this operation using this permission.
HTTP Status Code: 403
ErrorCode: AuthorizationPermissionMismatch

ダウンロード(失敗):

PS> Get-AzStorageBlobContent `
    -Container "testcontainer" `
    -Blob "test.txt" `
    -Destination "$env:TEMP\downloaded.txt" `
    -Context $ctx `
    -Force

Get-AzStorageBlobContent:
This request is not authorized to perform this operation using this permission.
HTTP Status Code: 403
ErrorCode: AuthorizationPermissionMismatch

削除(失敗):

PS> Remove-AzStorageBlob -Container "testcontainer" -Blob "test.txt" -Context $ctx

Remove-AzStorageBlob:
This request is not authorized to perform this operation using this permission.
HTTP Status Code: 403
ErrorCode: AuthorizationPermissionMismatch

読み取り専用ロール(Custom Blob Read Only)での検証

アップロード(失敗):

PS> Set-AzStorageBlobContent `
    -File "$env:TEMP\test.txt" `
    -Container "testcontainer" `
    -Blob "test-reader.txt" `
    -Context $ctx `
    -Force

Set-AzStorageBlobContent:
This request is not authorized to perform this operation using this permission.
HTTP Status Code: 403
ErrorCode: AuthorizationPermissionMismatch

一覧取得(成功):

PS> Get-AzStorageBlob -Container "testcontainer" -Context $ctx

   AccountName: stblobrbaceval01, ContainerName: testcontainer

Name      BlobType  Length ContentType              LastModified
----      --------  ------ -----------              ------------
test.txt  BlockBlob 16     application/octet-stream 2026-03-15 ...

ダウンロード(成功):

PS> Get-AzStorageBlobContent `
    -Container "testcontainer" `
    -Blob "test.txt" `
    -Destination "$env:TEMP\downloaded-reader.txt" `
    -Context $ctx `
    -Force

   AccountName: stblobrbaceval01, ContainerName: testcontainer

Name      BlobType  Length ContentType              LastModified
----      --------  ------ -----------              ------------
test.txt  BlockBlob 16     application/octet-stream 2026-03-15 ...

削除(失敗):

PS> Remove-AzStorageBlob -Container "testcontainer" -Blob "test.txt" -Context $ctx

Remove-AzStorageBlob:
This request is not authorized to perform this operation using this permission.
HTTP Status Code: 403
ErrorCode: AuthorizationPermissionMismatch

検証結果まとめ

操作 Custom Blob Write Only Custom Blob Read Only
アップロード(blobs/write) 成功 失敗(403)
一覧取得(blobs/read) 失敗(403) 成功
ダウンロード(blobs/read) 失敗(403) 成功
削除(blobs/delete) 失敗(403) 失敗(403)

Get-AzStorageBlob(REST API の List Blobs に相当)は、blobs/list という別の DataAction が必要かと思っていましたが、blobs/read の範囲内として扱われることが確認できました。つまり blobs/read だけを付与すれば、ダウンロードと一覧取得はどちらも可能になります。

削除は blobs/delete DataAction が必要であり、blobs/write には含まれません。書き込みロールのサービスプリンシパルでファイルの上書きアップロードはできましたが、削除は 403 になりました。

まとめ

Azure Blob Storage に対してカスタム RBAC ロールを使い、DataAction レベルで最小権限を設定した場合の動作を確認しました。blobs/write のみを付与したロールは Blob の書き込みだけを許可し、blobs/read のみを付与したロールは読み取りと一覧取得を許可することを実機で確認できました。書き込みしかできない RBAC を使うと仮想ネットワークの中にファイルを持ち込むことだけを許可できるので、意外と使いどころがある気がします。

参考リンク

  • 当サイトは個人のブログです。このブログに示されている見解や意見は個人的なものであり、所属組織の見解や意見を表明するものではありません。
  • 公開情報を踏まえて正確な情報を掲載するよう努めますが、その内容の完全性や正確性、有用性、安全性、最新性について一切保証しません。
  • 添付文章やリンク先などを含む本サイトの内容は作成時点でのものであり、予告なく変更される場合があります。