🔒

Azure Bicep で Storage Account の SSE を設定する

2023/03/19に公開

Azure Bicep で Storage Account の SSE (サーバー側暗号化) を設定してみようとしたところ、思ったより難しかったのと、やりたいことそのままのサンプルコードがなかったため、調査した内容を公開してみます。

この記事で書いてあること

  • Azure Bicep を使用して Storage Account の SSE を設定する方法

サンプルコード

早く使い方とコードを見たい、という方向けにまずはサンプル コードについて記載します。
この記事で説明するサンプル コードの全体は下記を参照ください。

https://github.com/kiyo-s/create_tfstate_az

使い方

上記のサンプル コードの使い方は下記の通りです。

  1. サンプルコードのリポジトリを clone する。
  2. terraform/example/bicepParams.json を参考に Bicep のパラメーターファイルを構成する
  3. azure cli を使いデプロイを実行する
az deployment sub create --template-file ./main.bicep --parameters @../example/bicepParams.json --location japaneast

全体構成

サンプル コードが作成するリソースの全体像は下図の赤枠内となります。

System overview

ファイル構成

サンプル コードのファイル構成は下記の通りです。

.
├── README.md
├── docs
│   └── images
│       └── tfstate_overview.drawio.png
├── main.bicep
└── modules
    ├── keyVault.bicep
    ├── storageAccount.bicep
    └── userAssignedManagedId.bicep

.bicep の拡張子がつくファイルが Bicep のコードであり、それぞれの簡単な役割は下記の通りです。
Azure Bicep は、Terraform と異なり、実行の際は単一のコードを指定して実行するため、main.bicep を呼び出し、modules/ 配下のコードは main.bicep が呼び出します。

  • main.bicep: リソース グループの作成と、modules 配下のコードを呼び出す
  • modules/keyVault.bicep: Storage Account の暗号化のための Azure Key Vault および Key Vault Key、Azure Key Vault に関する権限設定を行う
  • modules/storageAccount.bicep: Storage Account の作成と暗号化、Blob コンテナーの作成を行う
  • modules/userAssignedManagedId.bicep: Storage Account に Azure Key Vault の権限を付与するための User Assigned Managed ID を設定する

背景

説明をしていく前に、この記事を書こうと思った背景を記載します。

Bicep により、Terraform の state を格納する Blob Storage を作成しようと思い、調べました。

Terraform のためのリソースを Bicep で作成しようと思ったモチベーションは、Terraform では、state を格納するストレージを Terraform で管理すると常に差分が出てしまうため、Terraform 以外で作成しようと思い、Azure の 1st party の IaC ツールとして Bicep を使用することを考えました。

しかし、この記事を書き始めてから「Terraform で管理したいリソース (例えば、VNet や Azure VM など)」と「state 格納用の Blob Storage」の state を分ければ上記のような問題が出ないことを指摘されたので、必ずしも別のツールを使う必要はないと思います。

同じことができる複数のツールを使うことで学習コストが高くなるため、ツールの選定は慎重にしたいと思いました。
Terraform と Bicep では、構文はもちろん、state の考え方が違うため、戸惑うことがあると思います。

SSE を設定する流れ

この記事では、SSE の鍵管理方式の選択肢のうち、Azure Key Vault を使用する Customer-managed Key 方式を選択します。
鍵管理方式の選択肢や、それぞれの比較については、公式ドキュメントで「保存データに対する Azure Storage 暗号化 | Microsoft Learn」を参照してください。

Customer-managed Key を用いた SSE には下記の設定が必要となります。

  1. Storage Account からキーストアである Azure Key Vault に認証するためのシステム アカウントである User Assigned Managed ID を作成する
  2. Azure Key Vault を作成する
  3. Azure Key Vault の権限を付与する
  4. SSE で使用するキーを作成する
  5. Storage Account を作成し、User Assigned Managed ID を紐づける
  6. Azure Key Vault を使用して暗号化を行う

以下の各章で上記について説明をしていきます。

1. Storage Account からキーストアである Azure Key Vault に認証するためのシステム アカウントである User Assigned Managed ID を作成する

ここでは、Storage Account がキーストアである Azure Key Vault から鍵を取得するための認証で使うシステム アカウントを作成します。

Azure におけるシステム アカウントは Managed ID と呼ばれるものを利用します。
Managed ID を使用することで、ユーザー名やパスワードなどの資格情報を入力することなく、Azure リソース間の認証を行うことができます。

Managed ID は「System Assigned Managed ID」と「User Assigned Managed ID」の二種類があります。

二つの違いは下記の通りです。

  1. System Assigned Managed ID
    • VM や Storage Account などの Azure リソースに対して 1:1 で紐づく ID が作成される方式
    • Azure リソースと同じライフサイクルとなり、Azure リソースが削除されたタイミングで削除される。
  2. User Assigned Managed ID
    • Azure リソースから独立した ID
    • 一つの ID を複数の Azure リソースで使用することができる。

Managed ID の詳細や二つの違いについては、公式ドキュメント「新しいストレージ アカウントのカスタマー マネージド キーを構成する - Azure Storage | Microsoft Learn」を参照してください。

上記ドキュメントにある通り、Storage Account の作成と同時に暗号化を行いたい場合、User Assigned Managed ID しか使用できないため、今回は User Assigned Managed ID を使用します。

User Assigned Managed ID を作成する Bicep コードを抜粋します。
User Assigned Managed ID の作成は特殊な設定は必要がなく、リソース名とリージョンを指定することで作成できます。

resource userAssignManagedId 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' = {
  name: '${resourceName}-tfstate-sse'
  location: region
  tags: {
    Service: service
    Env: env
    Usage: tagUsage
    midUsage: 'storage_account_server_side_ecnryption'
  }
}

2. Azure Key Vault を作成する

Storage Account の SSE の鍵を管理するキーストアとして使用する Azure Key Vault を作成します。

resource tfstateKeyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
  // 中略
  properties: {
    createMode: 'default'
    enablePurgeProtection: true
    enableRbacAuthorization: true
    enableSoftDelete: true
    networkAcls: {
      bypass: 'AzureServices'
      defaultAction: 'Deny'
      ipRules: networkAclsIpRules
    }
  // 中略
  }
}

ここでの考慮事項は下記の二つです。

  1. 権限制御の方式として RBAC を使用する
  2. ネットワーク ACL を設定する

1. 権限制御の方式として RBAC を使用する

Azure Key Vault では、鍵やシークレット、証明書などのサブリソースがあります。
Azure Key Vault ではそれぞれのサブリソースに対する権限を細かく制御することができます。

Azure Key Vault のサブリソースの権限制御の方式には、「コンテナ アクセス ポリシー」と 「RBAC」の二つのモデルがあります。
コンテナ アクセス ポリシー モデルは従来からある権限制御のモデルとなり、Azure Key Vault の設定として権限設定ができます。
一方、RBAC モデルは Azure リソースに関する権限制御の方法に統合された権限制御モデルとなります。
元々 RBAC はリソース単位の権限制御のみを提供しており、リソース内のデータやサブリソースに対する権限制御は提供していませんでしたが、機能拡張により、リソース単位の制御と同じ仕組みでデータ・サブリソースの権限制御もできるようになりました。

それぞれメリットとデメリットがありますが、今回は「権限制御の方法を統一できる」というメリットを重視し、RBAC モデルを採用します。

その他のメリット・デメリットについては、公式ドキュメント「Azure ロールベースのアクセス制御への移行 | Microsoft Learn」を参照してください。

環境やユースケースによっては RBAC モデルのデメリットはいずれも無視できないものとなるため、選択には考慮が必要となります。

RBAC モデルにより権限制御をする場合、権限設定のほかに Key Vault のプロパティで RBAC モデルを使用することを指定する必要があります。
下記のサンプルコードの properties.enableRbacAuthorization: true がそれにあたります。

resource tfstateKeyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
  // 中略
  properties: {
    createMode: 'default'
    enablePurgeProtection: true
    enableRbacAuthorization: true
    enableSoftDelete: true
    networkAcls: {
      bypass: 'AzureServices'
      defaultAction: 'Deny'
      ipRules: networkAclsIpRules
    }
  // 中略
  }
}

2. ネットワーク ACL を設定する

Azure Key Vault は重要なデータを保管するリソースとなるため、多重でセキュリティの対策を行ったほうが良いでしょう。

上記で記載した権限制御のほかに、ネットワーク制御が該当します。

Storage Account の SSE では、操作を行うユーザーの IP アドレスと共に、Storage Account からのアクセスを許可する必要があります。
下記のサンプルコードの properties.networkAcls.ipRules が操作を行うユーザーの IP アドレス設定、properties.networkAcls.bypass: 'AzureServices' が Storage Account からのアクセス許可にあたります。

resource tfstateKeyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
  // 中略
  properties: {
    createMode: 'default'
    enablePurgeProtection: true
    enableRbacAuthorization: true
    enableSoftDelete: true
    networkAcls: {
      bypass: 'AzureServices'
      defaultAction: 'Deny'
      ipRules: networkAclsIpRules
    }
  // 中略
  }
}

3. Azure Key Vault の権限を付与する

下記の二種類の Azure Key Vault の権限を付与します。

  1. Terraform 実行ユーザーに対して、SSE で使用するキーを作成する権限
  2. Storage Account (User Assigned Manage ID) にキーを閲覧する権限

1. Terraform 実行ユーザーに対して、SSE で使用するキーを作成する権限

Terraform 実行ユーザーに権限を付与します。

// 前略

@description('Specify Principal ID to whom resource group role will be assignment.')
param operatorPrincipalId string

@description('Specify Principal Type of Principal ID.')
param operatorPrincipalType string

@description('Specify the RBAC role definition ID to be assigned to operator principal.')
param operatorRoleDefinitionIds array

// 中略

var operatorRoleAssignNames = [for operatorRoleDefinitionId in operatorRoleDefinitionIds: guid(
  resourceGroup().id, operatorPrincipalId, operatorRoleDefinitionId
)]

// 中略
resource operatorRoleDefinitions 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = [for operatorRoleDefinitionId in operatorRoleDefinitionIds: {
  scope: subscription()
  name: operatorRoleDefinitionId
}]

resource tfstateKeyVaultOperatorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for (operatorRoleAssignName, i) in operatorRoleAssignNames: {
  name: operatorRoleAssignName
  scope: resourceGroup()
  properties: {
    principalId: operatorPrincipalId
    principalType: operatorPrincipalType
    roleDefinitionId: operatorRoleDefinitions[i].id
  }
}]

Terraform 実行ユーザーには Key Vault Crypto OfficerKey Vault Contributor の二つの権限を付与するため、for により繰り返し処理を行っています。
権限を付与する対象について、複数のユーザーに権限を付与する場合、Security Group にユーザーをまとめて、権限付与の対象を Security Group とすることを想定するため、権限付与を行う対象が複数になることは想定しません。

Bicep の仕様上、RoleDefenition を取得するリソースと権限設定を行うリソースが別となります。
権限設定を行うリソースでは、name に一意の文字列を指定する必要があるため、variable によって guid 値を生成しています。

2. Storage Account (User Assigned Manage ID) にキーを閲覧・作成する権限

Storage Account のための権限設定を行います。
Bicep の実装は基本的には Terraform ユーザー用と変わりません。

4. SSE で使用するキーを作成する

SSE で使用するキーを作成します。

resource tfstateKeyVaultKey 'Microsoft.KeyVault/vaults/keys@2022-07-01' = {
  name: 'sse-key'
  dependsOn: [
    tfstateKeyVaultOperatorRoleAssignment
  ]
  tags: {
    Service: service
    Env: env
    Usage: tagUsage
    keyUsage: 'storage_account_server_side_encryption'
  }
  parent: tfstateKeyVault
  properties: {
    attributes: {
      enabled: true
    }
    keySize: 4096
    kty: 'RSA'
    rotationPolicy: {
      attributes: {
        expiryTime: 'P28D'
      }
      lifetimeActions: [
        {
          action: {
            type: 'rotate'
          }
          trigger: {
            timeBeforeExpiry: 'P7D'
          }
        }
      ]
    }
  }
}

SSE で使用するキーは、2,048、3,072、および 4,096 のサイズの RSA キーと RSA-HSM キーがサポートされています。
サポートされる中で最も強固な 4,096 の RSA キーを使用します。

また、Storage Account は SSE で使用しているキーの自動更新をサポートしています。
よりセキュアにするため、自動更新を有効にします。
Storage Account では、一日に一度キーを確認し、新しいバージョンがあれば交換します。
そのため、新しいバージョンの作成から、古いバージョンの有効期限が切れるまでの間隔を 24 時間以上空ける必要があります。
properties.rotationPolicy.lifetimeActions.trigger.timeBeforeExpiry が新しいバージョンを作成するタイミングを定義し、properties.rotationPolicy.attributes.expiryTime が古いバージョンの有効期限切れまでの期間を定義します。
ここでは、有効期限が切れる 7 日前に新しいバージョンを作成し、バージョン作成から 28 日間で有効期限が切れる設定となっています。

5. Storage Account を作成し、User Assigned Managed ID を紐づける

SSE を行う際、Azure Key Vault から暗号鍵を取得するために、権限を付与した User Assigned Managed ID と Storage Account を紐付けます。
User Assgined Managed ID を管理している userAssignedManagedId.bicep にて、User Assigned Managed ID の principal ID を別のコードで使用できるようにするために、output で公開します。

// 前略

// resources
resource userAssignManagedId 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' = {
  name: '${resourceName}-tfstate-sse'
  location: region
  tags: {
    Service: service
    Env: env
    Usage: tagUsage
    midUsage: 'storage_account_server_side_ecnryption'
  }
}

// outputs
output managedIdentity object = {
  id: userAssignManagedId.id
  principalId: userAssignManagedId.properties.principalId
}

Storage Account を管理している storageAccount.bicep で principal ID を指定する param を用意し、Storage Account の identity.userAssignedIdentitiesproperties.encryption.identity.userAssignedIdentity で ID を指定します。

この時注意が必要なのは、identity.userAssignedIdentities では ID は value ではなく、key で使用することです。
また、この時 identity.type'UserAssigned' を指定することを忘れないようにします。

// 前略

@description('Specify Principal Type of Principal ID.')
param principalType string

// 中略

// resources
resource tfstateStorageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' = {
  name: '${resourceNamePrefix}tfstate'
  location: region
  // 中略
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${managedIdentityPrincipalId}': {}
      // reference
      // https://github.com/MicrosoftDocs/azure-docs/blob/main/articles/logic-apps/create-managed-service-identity.md#create-user-assigned-identity-in-an-arm-template
    }
  }
  properties: {
  // 中略
  /// storage encryption
  encryption: {
    identity: {
      userAssignedIdentity: managedIdentityPrincipalId
    }
    keySource: 'Microsoft.Keyvault'
    keyvaultproperties: {
      keyvaulturi: keyVaultUri
      keyname: keyVaultKeyName
    }
    requireInfrastructureEncryption: true
  }
  // 中略
  }
}

6. Azure Key Vault を使用して暗号化を行う

最後に Azure Key Vault を使用して、Storage Account の暗号化を行っていきます。

Storage Account で SSE を行う際、Azure Key Vault の URI と暗号化で使用するキーの名前が必要となるため、Key Vault の Bicep コードで output の設定を行います。

// outputs
output keyVaultUri string = tfstateKeyVault.properties.vaultUri
output keyVaultKeyName string = tfstateKeyVaultKey.name

output で公開した Azure Key Vault の URI とキーの名前を Storage Account 側の param で受け取り、properties.encryption.keyvaultproperties で指定して完了です。

// 前略

@description('Specify Azure Key Vault URI for SSE.')
param keyVaultUri string

@description('Specify Azure Key Vault Key name for SSE.')
param keyVaultKeyName string

// 中略

// resources
resource tfstateStorageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' = {
  name: '${resourceNamePrefix}tfstate'
  location: region
  // 中略
  properties: {
  // 中略
  /// storage encryption
  encryption: {
    identity: {
      userAssignedIdentity: managedIdentityPrincipalId
    }
    keySource: 'Microsoft.Keyvault'
    keyvaultproperties: {
      keyvaulturi: keyVaultUri
      keyname: keyVaultKeyName
    }
    requireInfrastructureEncryption: true
  }
  // 中略
  }
}

まとめ

以上で Bicep により Storage Account の SSE を設定することができました。
久しぶりに Bicep に触れたのもありましたが、設定箇所が多かったのもあり、予想以上に苦労しました。

Bicep の記事は ARM Template や Terraform と比べて少ない印象なので、誰かの役に立つと嬉しいです。

Discussion