PowerShell を使用して Always Encrypted キーをローテーションする

適用対象:SQL ServerAzure SQL DatabaseAzure SQL Managed Instance

この記事では、SqlServer PowerShell モジュールを使用し、Always Encrypted キーを交換する手順について説明します。 Always Encrypted に SqlServer PowerShell を使用する方法については、「 PowerShell を使用した Always Encrypted の構成」を参照してください。

Microsoftでは、Always Encrypted PowerShell スクリプトを実行するときに PowerShell 7 以降を使用することをお勧めします。 PowerShell 7 では、多くの Always Encrypted シナリオに必要な、クロスプラットフォームのサポートが強化され、パフォーマンスが向上し、SqlServer モジュール (v22 以降) との最新の互換性が提供されます。

Always Encrypted キーのローテーションは、既存のキーを新しいキーに置き換えるプロセスです。 キーが侵害された場合はキーのローテーションが必要になる場合や、定期的な暗号化キーのローテーションを義務付ける組織のポリシーまたはコンプライアンス規制に準拠することが必要になる場合があります。

Always Encrypted は 2 種類のキーを使用するので、列マスター キーの交換と列暗号化キーの交換という 2 つの高レベルのキー交換ワークフローがあります。

  • 列暗号化キーの交換 - 現在のキーで暗号化されたデータの復号化と、新しい列暗号化キーを使用した再暗号化が行われます。 列暗号化キーの交換には、キーとデータベースの両方にアクセスする必要があるので、列暗号化キーの交換は、役割を分離せずに行う必要があります。
  • 列マスター キーの交換 では、現在の列マスター キーで保護された列暗号化キーを暗号化解除し、新しい列マスター キーを使用して列暗号化キーを再度暗号化し、両方の種類のキーについてメタデータを更新するという処理が必要です。 列マスター キーの交換は、(SqlServer PowerShell モジュールを使用する場合) 役割の分離の有無にかかわらず実行できます。

役割の分離を行わない列マスターキーのローテーション

このセクションで説明する列マスター キーのローテーション方法では、セキュリティ管理者と DBA の間のロールの分離はサポートされていません。 次の手順の一部では、物理キーに対する操作とキー メタデータに対する操作を組み合わせます。 組織で DevOps モデルを使用している場合、またはデータベースがクラウドでホストされていて、主な目的はクラウド管理者 (オンプレミスの DBA ではなく) が機密データにアクセスできないようにする場合に、このワークフローを使用します。 潜在的な敵対者に DBA が含まれている場合や、DBA が機密データにアクセスできない場合は、この方法を使用しないでください。

タスク [アーティクル] プレーンテキストのキー/キーストアへのアクセス データベースへのアクセス
ステップ 1. キー ストアで新しい列マスター キーを作成します。

注意: SqlServer PowerShell モジュールでは、この手順はサポートされていません。 コマンドラインからこのタスクを実行するには、キー ストアに固有のツールを使用する必要があります。 キー ストアとして Azure Key Vault を使用する場合、マルチテナントカスタマー マネージド キーローテーションはサポートされていません。 新しいカスタマー マネージド キーが既存のテナントと同じテナントにあることを確認します。
Always Encrypted の列マスター キーを作成して保存する はい いいえ
ステップ 2. PowerShell 環境を起動し、SqlServer モジュールをインポートします SqlServer モジュールのインポート いいえ いいえ
手順 3. サーバーとデータベースに接続します。 データベースに接続する いいえ はい
ステップ 4: 新しい列マスター キーの場所に関する情報を含む SqlColumnMasterKeySettings オブジェクトを作成します。 SqlColumnMasterKeySettings は、メモリ (PowerShell) に存在するオブジェクトです。 作成するには、キー ストアに固有のコマンドレットを使用する必要があります。 New-SqlAzureKeyVaultColumnMasterKeySettings

新しい-SqlCertificateStoreColumnMasterKeySettings

新しい-SqlCngColumnMasterKeySettings

New-SqlCspColumnMasterKeySettings (SQLのCSPカラムマスターキー設定の新規作成)
いいえ いいえ
ステップ 5: データベースの新しい列マスター キーに関するメタデータを作成します。 新しい-SqlColumnMasterKey

注: このコマンドレットでは、キー メタデータを作成するために、CREATE COLUMN MASTER KEY (Transact-SQL) ステートメントを発行します。
いいえ はい
ステップ 6. 現在の列マスター キーまたは新しい列マスター キーが Azure Key Vault のキー コンテナーまたはマネージド HSM に格納されている場合は、Azure に対して認証します Connect-AzAccount (接続-AzAccount) はい いいえ
手順 7. 列マスター キーが Azure Key Vault に格納されている場合は、Azure Key Vault のアクセス トークンを取得します。 Get-AzAccessToken の取得 いいえ いいえ
手順 8. 現在は古い列マスター キーで保護されている各列暗号化キーを、新しい列マスター キーを使用して暗号化することで交換を開始します。 この手順を実行した後、影響を受ける各列暗号化キーは、古い列マスターキーと関連付けられ(交換中)、新旧両方の列マスターキーで暗号化され、データベースのメタデータに2つの暗号化された値が格納されます。 SqlColumnMasterKeyRotation を呼び出す はい はい
手順 9. データベース内の暗号化された (そして古い列マスター キーで保護されている) 列をクエリするすべてのアプリケーションの管理者と調整して、アプリケーションから新しい列マスター キーにアクセスできるようにします。 列マスター キーを作成して保存する (Always Encrypted) はい いいえ
手順 10 回転を完了する

注: この手順を実行する前に、古い列マスター キーで保護されている暗号化された列をクエリするすべてのアプリケーションが、新しい列マスター キーを使用するように構成されていることを確認してください。 この手順を先に実行すると、一部のアプリケーションがデータを復号化できない可能性があります。 古い列マスター キーで作成された、暗号化された値をデータベースから削除して交換を完了します。 この操作で、古い列マスター キーと、それが保護している列暗号化キーとの関連付けが削除されます。
Complete-SqlColumnMasterKeyRotation(SQLカラムマスターキーのローテーションを完了) いいえ はい
手順 11 古い列マスター キーからメタデータを削除します。 SqlColumnMasterKey の削除 いいえ はい

回転の後、古い列マスター キーは完全に削除しないを強くお勧めします。 そこで、古い列マスター キーを現在のキー ストアに保存するか、セキュリティで保護された別の場所にアーカイブします。 バックアップ ファイルからデータベースを復元して、新しい列マスター キーを構成する の時点まで戻る場合は、古いキーでデータにアクセスする必要があります。

役割分離をしないで列マスター キーをローテーションする (Windows 証明書の例)

次のスクリプトは、既存の列マスター キー (CMK1) を新しい列マスター キー (CMK2) で置き換えるエンドツーエンドの例です。

[CmdletBinding()]
param(
	[Parameter(Mandatory = $false)]
	[string]$ServerName = '<server name>',

	[Parameter(Mandatory = $false)]
	[ValidateNotNullOrEmpty()]
	[string]$DatabaseName = '<database name>',

	[Parameter(Mandatory = $false)]
	[string]$CertificateSubject = 'AlwaysEncryptedCertNew',

	[Parameter(Mandatory = $false)]
	[ValidateNotNullOrEmpty()]
	[string]$OldCmkName = 'CMK1',

	[Parameter(Mandatory = $false)]
	[ValidateNotNullOrEmpty()]
	[string]$NewCmkName = 'CMK2'
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

Import-Module SqlServer -MinimumVersion 22.0.50 -ErrorAction Stop

Write-Host '[AE] Step 1: Creating a new self-signed certificate for the new CMK'
$cert = New-SelfSignedCertificate `
	-Subject $CertificateSubject `
	-CertStoreLocation 'Cert:CurrentUser\My' `
	-KeyExportPolicy Exportable `
	-Type DocumentEncryptionCert `
	-KeyUsage KeyEncipherment `
	-KeySpec KeyExchange `
	-KeyLength 2048
Write-Host "[AE] Certificate created with thumbprint: $($cert.Thumbprint)"

Write-Host "[AE] Step 2: Connecting to SQL Server '$ServerName' / Database '$DatabaseName'"
$connStr = "Server=$ServerName;Database=$DatabaseName;Integrated Security=True;Encrypt=True;TrustServerCertificate=True;Connection Timeout=30"

try {
	$database = Get-SqlDatabase -ConnectionString $connStr -ErrorAction Stop
}
catch {
	Write-Error "Failed to connect to '$ServerName' / '$DatabaseName'. Verify instance, database, and local permissions."
	throw
}

Write-Host "[AE] Step 3: Validating that old CMK '$OldCmkName' exists"
$oldCmk = Get-SqlColumnMasterKey -InputObject $database | Where-Object { $_.Name -eq $OldCmkName }
if (-not $oldCmk) {
	throw "Old CMK '$OldCmkName' does not exist. Cannot rotate."
}
Write-Host "[AE] Old CMK '$OldCmkName' found."

Write-Host "[AE] Step 4: Creating CMK settings for new certificate"
$newCmkSettings = New-SqlCertificateStoreColumnMasterKeySettings -CertificateStoreLocation 'CurrentUser' -Thumbprint $cert.Thumbprint

Write-Host "[AE] Step 5: Registering new CMK '$NewCmkName' in the database"
$newCmk = Get-SqlColumnMasterKey -InputObject $database | Where-Object { $_.Name -eq $NewCmkName }
if ($newCmk) {
	Write-Host "[AE] New CMK '$NewCmkName' already exists. Skipping creation."
}
else {
	New-SqlColumnMasterKey -Name $NewCmkName -InputObject $database -ColumnMasterKeySettings $newCmkSettings | Out-Null
	Write-Host "[AE] New CMK '$NewCmkName' registered."
}

Write-Host "[AE] Step 6: Initiating CMK rotation from '$OldCmkName' to '$NewCmkName'"
Write-Host "[AE] (This re-encrypts all associated CEKs under the new CMK...)"
Invoke-SqlColumnMasterKeyRotation `
	-SourceColumnMasterKeyName $OldCmkName `
	-TargetColumnMasterKeyName $NewCmkName `
	-InputObject $database
Write-Host "[AE] Rotation initiated."

Write-Host "[AE] Step 7: Completing the CMK rotation"
Complete-SqlColumnMasterKeyRotation `
	-SourceColumnMasterKeyName $OldCmkName `
	-InputObject $database
Write-Host "[AE] Rotation completed."

Write-Host "[AE] Step 8: Verifying CEKs are now under '$NewCmkName'"
$query = "SELECT name FROM sys.column_encryption_keys WHERE name = N'$($NewCmkName)'"
$rotatedCeks = Invoke-SqlCmd -ServerInstance $ServerName -Database $DatabaseName -Query $query -TrustServerCertificate  -ErrorAction SilentlyContinue
if ($rotatedCeks) {
	$cekCount = @($rotatedCeks).Count
	if ($cekCount -eq 0) { $cekCount = 1 }
	Write-Host "[AE] Verified: $cekCount CEK(s) now under '$NewCmkName'"
	@($rotatedCeks) | ForEach-Object { Write-Host "  - $($_.name)" }
}

Write-Host "[AE] Step 9: Removing old CMK metadata '$OldCmkName'"
Remove-SqlColumnMasterKey -Name $OldCmkName -InputObject $database
Write-Host "[AE] Old CMK '$OldCmkName' removed."

Write-Host '[AE] ========== Rotation Complete =========='
Write-Host "[AE] Old CMK: $OldCmkName (deleted)"
Write-Host "[AE] New CMK: $NewCmkName (active)"
Write-Host '[AE] All CEKs have been re-encrypted under the new CMK.'

役割の分離を行う列マスター キーのローテーション

このセクションで説明する列マスターキーのローテーションワークフローは、セキュリティ管理者とDBAを分離することを保証します。

重要

以下の表で、 プレーンテキストのキー/キーストアへのアクセス=はい の手順 (プレーンテキスト キーまたはキー ストアにアクセスする手順) を実行する前に、データベースをホストするコンピューターとは異なるセキュリティで保護されたコンピューターで PowerShell 環境が実行されていることを確認します。 詳細については、「 キー管理でのセキュリティに関する考慮事項」を参照してください。

パート 1: DBA

DBA は、ローテーションする列マスター キーと、現在の列マスター キーと関連付けられた、影響を受ける列暗号化キーに関するメタデータを取得します。 DBA は、セキュリティ管理者とすべての情報を共有します。

タスク [アーティクル] プレーンテキストのキー/キーストアへのアクセス データベースへのアクセス
ステップ 1. PowerShell 環境を起動し、Sql Server のモジュールをインポートします。 SqlServer モジュールのインポート いいえ なし
ステップ 2. サーバーとデータベースに接続します。 データベースに接続する いいえ はい
手順 3. 古い列マスター キーに関するメタデータを取得します。 Get-SqlColumnMasterKey (SQL 列マスターキーを取得) いいえ はい
ステップ 4: 古い列マスター キーで保護されている列マスター キーに関するメタデータ (暗号化値など) を取得します。 Get-SqlColumnEncryptionKey いいえ はい
ステップ 5: 列マスター キーの場所 (プロバイダー名と列マスター キーのキー パス) と、前の列マスター キーで保護されている対応する列暗号化キーの暗号化値を共有します。 次の例を参照してください。 いいえ いいえ

パート 2: セキュリティ管理者

セキュリティ管理者は、新しい列マスター キーを生成し、影響を受ける列暗号化キーを新しい列マスター キーで再暗号化し、新しい列マスター キーに関する情報と、影響を受ける列暗号化キーの新しい暗号化値セットを、DBA と共有します。

タスク [アーティクル] プレーンテキストのキー/キーストアへのアクセス データベースへのアクセス
ステップ 1. 古い列マスター キーの場所と、古い列マスター キーで保護されている対応する列暗号化キーの暗号化値を、DBA から取得します。 該当なし
次の例を参照してください。
いいえ いいえ
ステップ 2. キー ストアで新しい列マスター キーを作成します。

注意: SqlServer モジュールでは、この手順はサポートされていません。 コマンドラインからこのタスクを実行するには、キー ストアの種類に固有のツールを使用する必要があります。 キー ストアとして Azure Key Vault を使用する場合、マルチテナントカスタマー マネージド キーローテーションはサポートされていません。 新しいカスタマー マネージド キーが既存のテナントと同じテナントにあることを確認します。
Always Encrypted の列マスター キーを作成して保存する はい いいえ
手順 3. PowerShell 環境を起動し、Sql Server のモジュールをインポートします。 SqlServer モジュールのインポート いいえ いいえ
ステップ 4: 古い 列マスター キーの場所に関する情報を含む SqlColumnMasterKeySettings オブジェクトを作成します。 SqlColumnMasterKeySettings は、メモリ (PowerShell) に存在するオブジェクトです。 New-SqlColumnMasterKeySettings いいえ いいえ
ステップ 5: 新しい 列マスター キーの場所に関する情報を含む SqlColumnMasterKeySettings オブジェクトを作成します。 SqlColumnMasterKeySettings は、メモリ (PowerShell) に存在するオブジェクトです。 作成するには、キー ストアに固有のコマンドレットを使用する必要があります。 New-SqlAzureKeyVaultColumnMasterKeySettings

新しい-SqlCertificateStoreColumnMasterKeySettings

新しい-SqlCngColumnMasterKeySettings

New-SqlCspColumnMasterKeySettings (SQLのCSPカラムマスターキー設定の新規作成)
いいえ いいえ
ステップ 6. 古い (現在の) 列マスター キーまたは新しい列マスター キーが Azure Key Vault のキー コンテナーまたはマネージド HSM に格納されている場合は、Azure に対して認証します。 Connect-AzAccount (接続-AzAccount) はい いいえ
手順 7. 列マスター キーが Azure Key Vault に格納されている場合は、Azure Key Vault のアクセス トークンを取得します。 Get-AzAccessToken の取得 いいえ いいえ
手順 8. 現在は古い列マスター キーで保護されている各列暗号化キー値を、新しい列マスター キーを使用して再暗号化します。 新しい SqlColumnEncryptionKeyEncryptedValue

注: このコマンドレットを呼び出すときは、SqlColumnMasterKeySettings オブジェクトと、列暗号化キー値を新旧両方の列マスター キーに渡して、再暗号化します。
はい いいえ
手順 9. 新しい列マスター キーの場所 (プロバイダー名と列マスター キーのキー パス) と、列暗号化キーの新しい暗号化値セットを DBA と共有します。 次の例を参照してください。 いいえ いいえ

回転の後、古い列マスター キーは完全に削除しないを強くお勧めします。 そこで、古い列マスター キーを現在のキー ストアに保存するか、セキュリティで保護された別の場所にアーカイブします。 バックアップ ファイルからデータベースを復元して、新しい列マスター キーを構成する の時点まで戻る場合は、古いキーでデータにアクセスする必要があります。

パート 3: DBA

DBA は、新しい列マスター キーのメタデータを作成し、影響を受ける列暗号化キーのメタデータを更新して、新しい暗号化値セットを追加します。 この手順で、DBA は、アプリケーションから新しい列マスター キーにアクセスできるようにする、暗号化列をクエリするアプリケーションの管理者とも調整します。 新しいマスター キーを使用するようにすべてのアプリケーションがセットアップされたら、DBA は古い暗号化値セットと古い列マスター キー メタデータを削除します。

タスク [アーティクル] プレーンテキストのキー/キーストアへのアクセス データベースへのアクセス
ステップ 1. 新しい列マスター キーの場所と、古い列マスター キーで保護されている対応する列暗号化キーの新しい暗号化値セットを、セキュリティ管理者から取得します。 次の例を参照してください。 いいえ いいえ
ステップ 2. PowerShell 環境を起動し、Sql Server のモジュールをインポートします。 SqlServer モジュールのインポート いいえ いいえ
手順 3. サーバーとデータベースに接続します。 データベースに接続する いいえ はい
ステップ 4: 新しい列マスター キーの場所に関する情報を含む SqlColumnMasterKeySettings オブジェクトを作成します。 SqlColumnMasterKeySettings は、メモリ (PowerShell) に存在するオブジェクトです。 New-SqlColumnMasterKeySettings いいえ いいえ
ステップ 5: データベースの新しい列マスター キーに関するメタデータを作成します。 新しい-SqlColumnMasterKey

メモ: このコマンドレットでは、キー メタデータを作成するための CREATE COLUMN MASTER KEY (Transact-SQL) ステートメントを発行します。
いいえ はい
ステップ 6. 古い列マスターキーで保護されている列暗号化キーに関するメタデータを取得します。 Get-SqlColumnEncryptionKey いいえ はい
手順 7. 影響を受ける各列暗号化キーについて、新しい暗号化値 (新しい列マスター キーを使用して保護されている値) をメタデータに追加します。 SQL列暗号化キー値の追加 いいえ はい
手順 8. データベース内の暗号化された (そして古い列マスター キーで保護されている) 列をクエリするすべてのアプリケーションの管理者と調整して、アプリケーションから新しい列マスター キーにアクセスできるようにします。 列マスター キーの作成と保存 (Always Encrypted) いいえ いいえ
手順 9. 古い列マスター キーと関連付けられた暗号化値をデータベースから削除して交換を完了します。

注: この手順を実行する前に、古い列マスター キーで保護されている暗号化された列をクエリするすべてのアプリケーションが、新しい列マスター キーを使用するように構成されていることを確認してください。 この手順を先に実行すると、一部のアプリケーションがデータを復号化できない可能性があります。

この手順で、古い列マスター キーとそれによって保護されている列暗号化キーの関連付けが削除されます。
Complete-SqlColumnMasterKeyRotation(SQLカラムマスターキーのローテーションを完了)

また、 Remove-SqlColumnEncryptionKeyValueを使用することもできます
いいえ はい
手順 10 古い列マスター キーをデータベースから削除する SqlColumnMasterKey の削除 いいえ はい

役割を分離して列マスターキーをローテーションする (Windows 証明書の例)

次のスクリプトは、Windows 証明書ストアの証明書である新しい列マスター キーを生成し、既存の (現在の) 列マスター キーを交換し、新しい列マスター キーで置き換えるエンドツーエンドの例です。 このスクリプトでは、対象のデータベースに (交換対象の) CMK1 という列マスター キーが含まれている想定です。このキーで、一部の列暗号化キーが暗号化されます。

パート 1: DBA

[CmdletBinding()]
param(
	[Parameter(Mandatory = $false)]
	[ValidateNotNullOrEmpty()]
	[string]$ServerName = '<server name>',

	[Parameter(Mandatory = $false)]
	[ValidateNotNullOrEmpty()]
	[string]$DatabaseName = '<database name>',

	[Parameter(Mandatory = $false)]
	[ValidateNotNullOrEmpty()]
	[string]$OldCmkName = 'CMK2',

	[Parameter(Mandatory = $false)]
	[ValidateNotNullOrEmpty()]
	[string]$OutputFolder = 'C:\temp'
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

Import-Module SqlServer -MinimumVersion 22.0.50 -ErrorAction Stop

Write-Host "[CEK Export] Starting CMK and CEK data export"

# Validate output folder
if (-not (Test-Path -Path $OutputFolder -PathType Container)) {
	Write-Host "[CEK Export] Creating output folder: $OutputFolder"
	New-Item -Path $OutputFolder -ItemType Directory | Out-Null
}

# Connect to database
Write-Host "[CEK Export] Connecting to '$ServerName' / '$DatabaseName'"
$connStr = "Server=$ServerName;Database=$DatabaseName;Integrated Security=True;TrustServerCertificate=True;Connection Timeout=30"

try {
	$database = Get-SqlDatabase -ConnectionString $connStr -ErrorAction Stop
}
catch {
	Write-Error "Failed to connect to '$ServerName' / '$DatabaseName'."
	throw
}

# Retrieve old CMK
Write-Host "[CEK Export] Retrieving CMK '$OldCmkName'"
$oldCmk = Get-SqlColumnMasterKey -InputObject $database | Where-Object { $_.Name -eq $OldCmkName }
if (-not $oldCmk) {
	throw "CMK '$OldCmkName' not found in database '$DatabaseName'."
}

# Export CMK metadata using fixed text file name
$cmkFile = Join-Path $OutputFolder "oldcmkdata.txt"
Write-Host "[CEK Export] Exporting CMK metadata to: $cmkFile"
"CMKName|KeyStoreProviderName|KeyPath" | Set-Content -Path $cmkFile -Encoding UTF8
"$OldCmkName|$($oldCmk.KeyStoreProviderName)|$($oldCmk.KeyPath)" | Add-Content -Path $cmkFile -Encoding UTF8
Write-Host "[CEK Export]   ✓ CMK metadata exported"

# Discover and export CEKs using fixed text file name
Write-Host "[CEK Export] Discovering CEKs associated with '$OldCmkName'"
$ceks = Get-SqlColumnEncryptionKey -InputObject $database
$cekFile = Join-Path $OutputFolder "oldcekvalues.txt"
"CEKName|CEKEncryptedValue|HasMultipleEncryptedValues" | Set-Content -Path $cekFile -Encoding UTF8

$exportedCount = 0
$multiValueCount = 0

foreach ($cek in $ceks) {
	if (-not $cek.ColumnEncryptionKeyValues) {
		continue
	}

	# Check if this CEK has multiple encrypted values
	if ($cek.ColumnEncryptionKeyValues.Count -gt 1) {
		# CEK has multiple encrypted values - check if any reference the old CMK
		$refersToOldCmk = $cek.ColumnEncryptionKeyValues | Where-Object { $_.ColumnMasterKeyName -eq $OldCmkName }
		if ($refersToOldCmk) {
			Write-Warning "CEK '$($cek.Name)' has $($cek.ColumnEncryptionKeyValues.Count) encrypted values. One references '$OldCmkName'. This CEK cannot be rotated automatically."
			"$($cek.Name)|MULTIPLE_ENCRYPTED_VALUES|True" | Add-Content -Path $cekFile -Encoding UTF8
			$multiValueCount++
		}
	}
	else {
		# CEK has single encrypted value - check if it references the old CMK
		if ($cek.ColumnEncryptionKeyValues[0].ColumnMasterKeyName -eq $OldCmkName) {
			$encryptedValueHex = "0x" + -join ($cek.ColumnEncryptionKeyValues[0].EncryptedValue | ForEach-Object { $_.ToString("X2") })
			"$($cek.Name)|$encryptedValueHex|False" | Add-Content -Path $cekFile -Encoding UTF8
			$exportedCount++
		}
	}
}

Write-Host "[CEK Export]   ✓ CEK encrypted values exported"
Write-Host "[CEK Export]     - Exported: $exportedCount CEK(s)"
if ($multiValueCount -gt 0) {
	Write-Warning "      - Multi-valued CEKs (manual review needed): $multiValueCount"
}

Write-Host "[CEK Export] ===== Export Complete ====="
Write-Host "[CEK Export] CMK Metadata:   $cmkFile"
Write-Host "[CEK Export] CEK Values:     $cekFile"

パート 2: セキュリティ管理者

[CmdletBinding()]
param(
    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$ShareFolder = 'C:\Temp\',

    [Parameter(Mandatory = $false)]
    [ValidateSet('CurrentUser', 'LocalMachine')]
    [string]$StoreLocation = 'CurrentUser',

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$CertificateSubject = 'AlwaysEncryptedCert'
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

Import-Module SqlServer -MinimumVersion 22.0.50 -ErrorAction Stop

function Import-DelimitedTextFile {
    param(
        [Parameter(Mandatory = $true)] [string]$Path,
        [Parameter(Mandatory = $true)] [string[]]$RequiredColumns
    )

    if (-not (Test-Path -Path $Path -PathType Leaf)) {
        throw "Required file not found: $Path"
    }

    $raw = Get-Content -Path $Path -Raw
    if ([string]::IsNullOrWhiteSpace($raw)) {
        throw "File is empty: $Path"
    }

    $delimiter = if ($raw -match '\|') { '|' } else { ',' }
    $rows = @(Import-Csv -Path $Path -Delimiter $delimiter)
    if ($rows.Count -eq 0) {
        throw "No data rows found in file: $Path"
    }

    $first = $rows[0]
    $RequiredColumns | ForEach-Object {
        if (-not $first.PSObject.Properties[$_]) {
            throw "Missing required column '$_' in file: $Path"
        }
    }

    return $rows
}

if (-not (Test-Path -Path $ShareFolder -PathType Container)) {
    throw "Share folder does not exist: $ShareFolder"
}

$oldCmkDataFile = Join-Path $ShareFolder 'oldcmkdata.txt'
$oldCekValuesFile = Join-Path $ShareFolder 'oldcekvalues.txt'
$newCmkDataFile = Join-Path $ShareFolder 'newcmkdata.txt'
$newCekValuesFile = Join-Path $ShareFolder 'newcekvalues.txt'

Write-Host "[AE] Reading old CMK data from '$oldCmkDataFile'"
$oldCmkDataRows = Import-DelimitedTextFile -Path $oldCmkDataFile -RequiredColumns @('KeyStoreProviderName', 'KeyPath')
$oldCmkData = $oldCmkDataRows[0]

Write-Host "[AE] Reading old CEK values from '$oldCekValuesFile'"
$oldCekValues = Import-DelimitedTextFile -Path $oldCekValuesFile -RequiredColumns @('CEKName', 'CEKEncryptedValue')

Write-Host "[AE] Finding or creating certificate '$CertificateSubject' in $StoreLocation\\My"
$certPath = "Cert:$StoreLocation\My"
$cert = Get-ChildItem -Path $certPath |
    Where-Object { $_.Subject -eq "CN=$CertificateSubject" } |
    Sort-Object NotAfter -Descending |
    Select-Object -First 1

if (-not $cert) {
    $cert = New-SelfSignedCertificate `
        -Subject $CertificateSubject `
        -CertStoreLocation $certPath `
        -KeyExportPolicy Exportable `
        -Type DocumentEncryptionCert `
        -KeyUsage DataEncipherment `
        -KeySpec KeyExchange
}

Write-Host '[AE] Building CMK settings'
$oldCmkSettings = New-SqlColumnMasterKeySettings `
    -KeyStoreProviderName $oldCmkData.KeyStoreProviderName `
    -KeyPath $oldCmkData.KeyPath

$newCmkSettings = New-SqlCertificateStoreColumnMasterKeySettings `
    -CertificateStoreLocation $StoreLocation `
    -Thumbprint $cert.Thumbprint

Write-Host "[AE] Re-encrypting CEK values and writing '$newCekValuesFile'"
"CEKName|CEKEncryptedValue" | Set-Content -Path $newCekValuesFile -Encoding UTF8

$oldCekValues | ForEach-Object {
    $newValue = New-SqlColumnEncryptionKeyEncryptedValue `
        -TargetColumnMasterKeySettings $newCmkSettings `
        -ColumnMasterKeySettings $oldCmkSettings `
        -EncryptedValue $_.CEKEncryptedValue

    "$($_.CEKName)|$newValue" | Add-Content -Path $newCekValuesFile -Encoding UTF8
}

Write-Host "[AE] Writing new CMK data to '$newCmkDataFile'"
"KeyStoreProviderName|KeyPath" | Set-Content -Path $newCmkDataFile -Encoding UTF8
"$($newCmkSettings.KeyStoreProviderName)|$($newCmkSettings.KeyPath)" | Add-Content -Path $newCmkDataFile -Encoding UTF8

Write-Host '[AE] Completed successfully'
Write-Host "[AE] Output files: $newCmkDataFile , $newCekValuesFile"

パート 3: DBA

[CmdletBinding()]
param(
    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$ServerName = '<server name>',

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$DatabaseName = '<database name>',

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$OldCmkName = 'CMK1',

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$NewCmkName = 'CMK2',

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$InputFolder = 'C:\temp'
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

Import-Module SqlServer -MinimumVersion 22.0.50 -ErrorAction Stop

function Import-DelimitedTextFile {
    param(
        [Parameter(Mandatory = $true)] [string]$Path,
        [Parameter(Mandatory = $true)] [string[]]$RequiredColumns
    )

    if (-not (Test-Path -Path $Path -PathType Leaf)) {
        throw "Required file not found: $Path"
    }

    $raw = Get-Content -Path $Path -Raw
    if ([string]::IsNullOrWhiteSpace($raw)) {
        throw "File is empty: $Path"
    }

    $delimiter = if ($raw -match '\|') { '|' } else { ',' }
    $rows = @(Import-Csv -Path $Path -Delimiter $delimiter)
    if ($rows.Count -eq 0) {
        throw "No data rows found in file: $Path"
    }

    $first = $rows[0]
    $RequiredColumns | ForEach-Object {
        if (-not $first.PSObject.Properties[$_]) {
            throw "Missing required column '$_' in file: $Path"
        }
    }

    return $rows
}

if (-not (Test-Path -Path $InputFolder -PathType Container)) {
    throw "Input folder not found: $InputFolder"
}

$newCmkDataFile = Join-Path $InputFolder 'newcmkdata.txt'
$newCekValuesFile = Join-Path $InputFolder 'newcekvalues.txt'

Write-Host "[AE] Reading new CMK data from '$newCmkDataFile'"
$newCmkRows = Import-DelimitedTextFile -Path $newCmkDataFile -RequiredColumns @('KeyStoreProviderName', 'KeyPath')
$newCmkData = $newCmkRows[0]

Write-Host "[AE] Reading new CEK values from '$newCekValuesFile'"
$newCekValues = Import-DelimitedTextFile -Path $newCekValuesFile -RequiredColumns @('CEKName', 'CEKEncryptedValue')

Write-Host "[AE] Connecting to '$ServerName' / '$DatabaseName'"
$connStr = "Server=$ServerName;Database=$DatabaseName;Integrated Security=True;TrustServerCertificate=True;Connection Timeout=30"
$database = Get-SqlDatabase -ConnectionString $connStr -ErrorAction Stop

Write-Host "[AE] Ensuring target CMK '$NewCmkName' exists"
$newCmkSettings = New-SqlColumnMasterKeySettings -KeyStoreProviderName $newCmkData.KeyStoreProviderName -KeyPath $newCmkData.KeyPath
$existingNewCmk = Get-SqlColumnMasterKey -InputObject $database | Where-Object { $_.Name -eq $NewCmkName }
if (-not $existingNewCmk) {
    New-SqlColumnMasterKey -Name $NewCmkName -InputObject $database -ColumnMasterKeySettings $newCmkSettings | Out-Null
}

Write-Host "[AE] Adding new encrypted CEK values under '$NewCmkName'"
$ceks = Get-SqlColumnEncryptionKey -InputObject $database

$ceksToRotate = @(
    $ceks | Where-Object {
        $_.ColumnEncryptionKeyValues -and
        @($_.ColumnEncryptionKeyValues | Where-Object { $_.ColumnMasterKeyName -eq $OldCmkName }).Count -gt 0
    }
)

$ceksToRotate | ForEach-Object {
    $cek = $_
    if (@($cek.ColumnEncryptionKeyValues).Count -gt 1) {
        throw "CEK '$($cek.Name)' already has multiple encrypted values and still references '$OldCmkName'."
    }

    $newValueRow = @($newCekValues | Where-Object { $_.CEKName -eq $cek.Name }) | Select-Object -First 1
    if (-not $newValueRow) {
        throw "No new encrypted value found for CEK '$($cek.Name)' in file '$newCekValuesFile'."
    }

    Add-SqlColumnEncryptionKeyValue `
        -ColumnMasterKeyName $NewCmkName `
        -Name $cek.Name `
        -EncryptedValue $newValueRow.CEKEncryptedValue `
        -InputObject $database | Out-Null
}

Write-Host "[AE] Completing rotation for source CMK '$OldCmkName'"
Complete-SqlColumnMasterKeyRotation -SourceColumnMasterKeyName $OldCmkName -InputObject $database

Write-Host "[AE] Removing source CMK '$OldCmkName' metadata"
Remove-SqlColumnMasterKey -Name $OldCmkName -InputObject $database

Write-Host '[AE] Completed successfully'

列暗号化キーの回転

列暗号化キーを交換するには、交換するキーで暗号化されたすべての列のデータを暗号化解除し、新しい列暗号化キーを使用してデータを再度暗号化する必要があります。 この交換ワークフローでは、キーとデータベースの両方にアクセスする必要があるので、役割の分離ありでは実行できません。 ローテーションするキーで暗号化された列を含むテーブルが大きい場合、列暗号化キーのローテーションに長時間かかることがあります。 したがって、組織で列暗号化キーを交換する場合は、慎重に計画を立てる必要があります。

オフラインまたはオンラインの手法で、列暗号化キーを回転できます。 前者の方法のほうが速いですが、お使いのアプリケーションでは、影響を受けるテーブルに書き込めません。 後者の手法は時間がかかりますが、時間間隔を設定できます。その間、アプリケーションでは影響を受けるテーブルを利用できません。 詳細については、「PowerShell で Always Encrypted を使用した列暗号化を構成する」および「Set-SqlColumnEncryption」を参照してください。

タスク [アーティクル] プレーンテキストのキー/キーストアへのアクセス データベースへのアクセス
ステップ 1. PowerShell 環境を起動し、Sql Server のモジュールをインポートします。 SqlServer モジュールのインポート いいえ いいえ
ステップ 2. サーバーとデータベースに接続します。 データベースに接続する いいえ はい
手順 3. Azure Key Vault の鍵コンテナーまたはマネージド HSM に列暗号化キーを保護する列マスターキーが格納されていて、このキーのローテーションが必要な場合は、Azure に対して認証を行います。 Connect-AzAccount (接続-AzAccount) はい いいえ
ステップ 4: 列マスター キーが Azure Key Vault に格納されている場合は、Azure Key Vault のアクセス トークンを取得します。 Get-AzAccessToken の取得 いいえ いいえ
ステップ 5: 新しい列暗号化キーを生成し、それを列マスター キーで暗号化し、データベースで列の暗号化キー メタデータを作成します。 新しい SqlColumnEncryptionKey

注: 内部で列の暗号化キーを生成し、暗号化するコマンドレットのバリエーションを使用します。
このコマンドレットでは、キー メタデータを作成するための CREATE COLUMN ENCRYPTION KEY (Transact-SQL) ステートメントを発行します。
はい はい
ステップ 6. 古い列暗号化キーで暗号化されたすべての列を検索します。 SQL Server 管理オブジェクト (SMO) プログラミング ガイド いいえ はい
手順 7. 影響を受ける各列について SqlColumnEncryptionSettings オブジェクトを作成します。 SqlColumnEncryptionSettings は、(PowerShell の) メモリに存在するオブジェクトです。 列のターゲット暗号化方式を指定します。 この場合、オブジェクトには、影響を受ける列を新しい列暗号化キーで暗号化することを指定します。 New-SqlColumnEncryptionSettings(新しいSQLカラム暗号化設定) いいえ いいえ
手順 8. 新しい暗号化キーを使用して、手順 5 で指定した列を再暗号化します。 Set-SqlColumnEncryption (英語)

注: この手順の実行には時間がかかる場合があります。 アプリケーションでは、選択されたアプローチ (オンラインまたはオフライン) に応じて、操作全体または一部の操作でテーブルにアクセスできなくなります。
はい はい
手順 9. 古い列暗号化キーのメタデータを削除します。 Remove-SqlColumnEncryptionKey(SQL列暗号化キーを削除するコマンド) いいえ はい

例 - 列暗号化キーのローテーション

次のスクリプトは、列暗号化キーを交換する例です。 このスクリプトでは、対象データベースに、(交換対象の) CEK1 という列暗号化キーで暗号化されたいくつかの列が含まれていると仮定しています。このキーは、CMK1 という列マスター キーを使用して保護されています (列マスター キーは Azure Key Vault に格納されていません)。

[CmdletBinding()]
param(
    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$ServerName = '<server name>',

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$DatabaseName = '<database name>',

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$OldCekName = 'CEK1',

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$NewCekName = 'CEK2',

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$CmkName = 'CMK2',

    [Parameter(Mandatory = $false)]
    [ValidateRange(0, 3600)]
    [int]$MaxDowntimeInSeconds = 120,

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$LogFileDirectory = '.'
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

Import-Module SqlServer -MinimumVersion 22.0.50 -ErrorAction Stop

if ($OldCekName -eq $NewCekName) {
    throw 'OldCekName and NewCekName must be different.'
}

if (-not (Test-Path -Path $LogFileDirectory -PathType Container)) {
    New-Item -Path $LogFileDirectory -ItemType Directory | Out-Null
}

Write-Host "[AE] Connecting to '$ServerName' / '$DatabaseName'"
$connStr = "Server=$ServerName;Database=$DatabaseName;Integrated Security=True;TrustServerCertificate=True;Connection Timeout=30"
$database = Get-SqlDatabase -ConnectionString $connStr -ErrorAction Stop

Write-Host "[AE] Ensuring CMK '$CmkName' exists"
$cmk = Get-SqlColumnMasterKey -InputObject $database | Where-Object { $_.Name -eq $CmkName }
if (-not $cmk) {
    throw "Column master key '$CmkName' was not found."
}

Write-Host "[AE] Ensuring target CEK '$NewCekName' exists"
$existingNewCek = Get-SqlColumnEncryptionKey -InputObject $database | Where-Object { $_.Name -eq $NewCekName }
if (-not $existingNewCek) {
    New-SqlColumnEncryptionKey -Name $NewCekName -InputObject $database -ColumnMasterKey $CmkName | Out-Null
}

Write-Host "[AE] Discovering encrypted columns using '$OldCekName'"
$settings = @()
$tables = @($database.Tables)
$tables | ForEach-Object {
    $table = $_
    @($table.Columns) | ForEach-Object {
        $column = $_
        if ($column.IsEncrypted -and $column.ColumnEncryptionKeyName -eq $OldCekName) {
            $columnName = "{0}.{1}.{2}" -f $table.Schema, $table.Name, $column.Name
            $settings += New-SqlColumnEncryptionSettings -ColumnName $columnName -EncryptionType $column.EncryptionType -EncryptionKey $NewCekName
        }
    }
}

if ($settings.Count -eq 0) {
    Write-Warning "No encrypted columns found that reference '$OldCekName'. Nothing to rotate."
    return
}

Write-Host "[AE] Re-encrypting $($settings.Count) column(s) to '$NewCekName'"
Set-SqlColumnEncryption `
    -ColumnEncryptionSettings $settings `
    -InputObject $database `
    -UseOnlineApproach `
    -MaxDowntimeInSeconds $MaxDowntimeInSeconds `
    -LogFileDirectory $LogFileDirectory

Write-Host "[AE] Validating no columns still reference '$OldCekName'"
$stillUsingOld = $false
@($database.Tables) | ForEach-Object {
    @($_.Columns) | ForEach-Object {
        if ($_.IsEncrypted -and $_.ColumnEncryptionKeyName -eq $OldCekName) {
            $stillUsingOld = $true
        }
    }
}

if ($stillUsingOld) {
    throw "At least one encrypted column still references '$OldCekName'. Aborting CEK removal."
}

Write-Host "[AE] Removing old CEK '$OldCekName'"
Remove-SqlColumnEncryptionKey -Name $OldCekName -InputObject $database

Write-Host '[AE] Completed successfully'

次のステップ

関連項目