Поворот ключей Always Encrypted с использованием PowerShell

Применимо к:SQL ServerБаза данных SQL AzureУправляемый экземпляр SQL Azure

В этой статье описаны действия по смене ключей постоянного шифрования с помощью модуля SqlServer PowerShell. Сведения о том, как начать использовать модуль SqlServer PowerShell для Always Encrypted, см. в разделе "Настройка Always Encrypted" с помощью PowerShell.

Примечание.

Microsoft рекомендует использовать PowerShell 7 или более поздней версии при выполнении скриптов PowerShell always Encrypted. PowerShell 7 обеспечивает улучшенную кроссплатформенную поддержку, более высокую производительность и последнюю совместимость с модулем SqlServer (версии 22+), который необходим для многих сценариев Always Encrypted.

Ротация ключей Always Encrypted — это процесс замены существующего ключа новым. Возможно, потребуется сменить ключ, если он скомпрометирован или соответствовать политикам вашей организации или нормативным требованиям, которые требуют регулярного смены криптографических ключей.

Always Encrypted использует два типа ключей, поэтому существуют два высокоуровневых рабочих процесса по смене ключей: смена мастер-ключей столбцов и смена ключей шифрования столбцов.

  • Смена ключа шифрования столбцов — включает расшифровку данных, зашифрованных с помощью текущего ключа, и повторное шифрование данных с помощью нового ключа шифрования столбца. Поскольку для смены ключа шифрования столбца требуется доступ к ключам и базе данных, смена ключей шифрования столбцов осуществляется только без разделения ролей.
  • Поворот главного ключа столбца — включает расшифровку ключей шифрования столбцов, защищенных с помощью текущего главного ключа столбца, повторного шифрования их с помощью нового главного ключа столбца и обновления метаданных для обоих типов ключей. Смена главного ключа столбца может быть выполнена как с разделением ролей, так и без него, при использовании модуля SqlServer PowerShell.

Смена главного ключа столбца без разделения ролей

Метод, описанный в этом разделе для смены главного ключа столбца, не поддерживает разделение ролей между администратором безопасности и DBA. Некоторые из следующих шагов объединяют операции с физическими ключами с операциями с метаданными ключей. Используйте этот рабочий процесс, если ваша организация использует модель DevOps или когда база данных размещена в облаке, а основная цель — ограничить доступ к конфиденциальным данным администраторам облачных служб (но не локальным базам данных). Не используйте этот метод, если среди потенциальных злоумышленников могут быть администраторы баз данных или если у администраторов баз данных не должно быть доступа к конфиденциальным данным.

Задача Статья Доступ к ключам с открытым текстом или хранилищу ключей Доступ к базе данных
Шаг 1. Создайте новый главный ключ столбца в хранилище ключей.

Заметка: Модуль SqlServer PowerShell не поддерживает этот шаг. Для выполнения этой задачи из командной строки используйте средства, поддерживаемые выбранным хранилищем ключей. При использовании Azure Key Vault в качестве хранилища ключей смена клиентом управляемых ключей для многопользовательских клиентов не поддерживается. Убедитесь, что новый ключ, управляемый клиентом, находится в том же арендаторе, что и существующий.
Создание и хранение главных ключей столбцов для Always Encrypted Да Нет
Шаг 2. Запуск среды PowerShell и импорт модуля SqlServer. Импорт модуля SqlServer Нет Нет
Шаг 3. Соединение с сервером и базой данных. Подключение к базе данных Нет Да
Шаг 4. Создание объекта SqlColumnMasterKeySettings, содержащего сведения о расположении нового главного ключа столбца. SqlColumnMasterKeySettings — это объект, который существует в памяти (PowerShell). Чтобы создать его, используйте командлет, предназначенный для вашего хранилища ключей. New-SqlAzureKeyVaultColumnMasterKeySettings

Новые настройки мастер-ключа столбца SqlCertificateStore

Новая настройка ключа мастер-колонки SqlCng

New-SqlCspColumnMasterKeySettings
Нет Нет
Шаг 5. Создайте метаданные о новом главном ключе столбца в базе данных. New-SqlColumnMasterKey

Примечание: внутренне этот командлет использует инструкцию CREATE COLUMN MASTER KEY (Transact-SQL) для создания метаданных ключа.
Нет Да
Шаг 6. Выполните проверку подлинности в Azure, если ваш текущий главный ключ столбца или новый главный ключ столбца хранится в хранилище ключей или в управляемом модуле HSM в Azure Key Vault. Connect-AzAccount Да Нет
Шаг 7. Получите токен доступа для Azure Key Vaults, если ваш главный ключ столбца хранится в Azure Key Vault. Get-AzAccessToken Нет Нет
Шаг 8. Начните ротацию, зашифровав каждый ключ шифрования столбца, который в данный момент защищен с помощью старого главного ключа столбца, используя новый главный ключ столбца. После выполнения этого шага каждый затронутый ключ шифрования столбца (связанный со старым сменяемым главным ключом столбца) шифруется с помощью старого и нового главных ключей и имеет два зашифрованных значения в метаданных базы данных. Invoke-SqlColumnMasterKeyRotation Да Да
Шаг 9. Работа с администраторами всех приложений, выполняющих запросы к зашифрованным столбцам в базе данных (и защищенных с помощью старого главного ключа столбца), по обеспечению доступа приложений к новому главному ключу столбца. Создание и хранение мастер-ключей столбцов (Всегда зашифровано) Да Нет
Шаг 10. Завершите ротацию.

Примечание. Перед выполнением этого шага убедитесь, что все приложения, запрашивающие зашифрованные столбцы, защищенные старым главным ключом столбца, были настроены для использования нового главного ключа столбца. В случае преждевременного выполнения этого шага некоторые приложения не смогут расшифровывать данные. Завершите ротацию, удалив из базы данных зашифрованные значения, созданные с помощью старого главного ключа столбца. При этом будет удалена связь между старым главным ключом столбца и защищаемыми ими ключами шифрования столбцов.
Complete-SqlColumnMasterKeyRotation Нет Да
Шаг 11. Удалите метаданные из старого главного ключа столбца. Remove-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.'

Смена основного ключа столбца с разделением ролей

В описанном в этом разделе процессе смены главного ключа столбца существует разделение ролей между администраторами безопасности и администраторами баз данных.

Внимание

Перед выполнением каких-либо действий, где в таблице ниже указано доступ к открытым текстовым ключам/хранилищу ключей=Да (шаги, которые обращаются к открытым текстовым ключам или хранилищу ключей), убедитесь, что среда PowerShell работает на защищенном компьютере, отличном от компьютера, на котором размещена база данных. Дополнительные сведения см. в разделе "Вопросы безопасности" для управления ключами.

Часть 1. Администратор баз данных

Администратор баз данных получает метаданные о подлежащем смене главном ключе столбца и о затрагиваемых ключах шифрования столбцов, которые связаны с текущим главным ключом столбца. Администратор баз данных предоставляет эти сведения администратору безопасности.

Задача Статья Доступ к ключам с открытым текстом или хранилищу ключей Доступ к базе данных
Шаг 1. Запуск среды PowerShell и импорт модуля SqlServer. Импорт модуля SqlServer Нет нет
Шаг 2. Соединение с сервером и базой данных. Подключение к базе данных Нет Да
Шаг 3. Получение метаданных о старом главном ключе столбца. Get-SqlColumnMasterKey Нет Да
Шаг 4. Получение метаданных о ключах шифрования столбцов, защищенных старым главным ключом столбца, включая их зашифрованные значения. Get-SqlColumnEncryptionKey (Ключ шифрования) Нет Да
Шаг 5. Предоставьте информацию о расположении главного ключа столбца, включая имя поставщика и путь к этому ключу, а также зашифрованные значения соответствующих ключей шифрования столбцов, защищенные с помощью старого главного ключа столбца. См. следующие примеры. Нет Нет

Часть 2. Администратор безопасности

Администратор безопасности создает новый главный ключ столбца, повторно шифрует затронутые ключи шифрования столбцов с помощью нового главного ключа столбца и предоставляет администратору баз данных сведения о новом главном ключе столбца, а также о наборе новых зашифрованных значений для затронутых ключей шифрования столбцов.

Задача Статья Доступ к ключам с открытым текстом или хранилищу ключей Доступ к базе данных
Шаг 1. Получите у администратора баз данных сведения о расположении старого главного ключа столбца и зашифрованные значения соответствующих ключей шифрования столбцов, защищенные старым главным ключом столбца. Н/П
См. следующие примеры.
Нет Нет
Шаг 2. Создайте новый главный ключ столбца в хранилище ключей.

Заметка: Модуль SqlServer не поддерживает этот шаг. Для выполнения этой задачи из командной строки используйте средства, поддерживаемые выбранным хранилищем ключей. При использовании Azure Key Vault в качестве хранилища ключей смена клиентом управляемых ключей для многопользовательских клиентов не поддерживается. Убедитесь, что новый ключ, управляемый клиентом, находится в том же арендаторе, что и существующий.
Создание и хранение главных ключей столбцов для Always Encrypted Да Нет
Шаг 3. Запуск среды PowerShell и импорт модуля SqlServer. Импорт модуля SqlServer Нет Нет
Шаг 4. Создайте объект SqlColumnMasterKeySettings, содержащий сведения о расположении старого главного ключа столбца. SqlColumnMasterKeySettings — это объект, который существует в памяти (PowerShell). New-SqlColumnMasterKeySettings Нет Нет
Шаг 5. Создайте объект SqlColumnMasterKeySettings, содержащий сведения о расположении нового главного ключа столбца. SqlColumnMasterKeySettings — это объект, который существует в памяти (PowerShell). Чтобы создать его, используйте командлет, предназначенный для вашего хранилища ключей. New-SqlAzureKeyVaultColumnMasterKeySettings

Новые настройки мастер-ключа столбца SqlCertificateStore

Новая настройка ключа мастер-колонки SqlCng

New-SqlCspColumnMasterKeySettings
Нет Нет
Шаг 6. Выполните аутентификацию в Azure, если старый (текущий) или новый главный ключ столбца хранится в хранилище ключей или в управляемом модуле HSM в Azure Key Vault. Connect-AzAccount Да Нет
Шаг 7. Получите токен доступа для Azure Key Vaults, если ваш главный ключ столбца хранится в Azure Key Vault. Get-AzAccessToken Нет Нет
Шаг 8. Пере-шифруйте каждое значение ключа шифрования столбца, который в данный момент защищен старым главным ключом столбца, используя новый главный ключ столбца. Новый зашифрованный ключ шифрования столбца SQL

Заметка: При вызове этого командлета передайте объекты SqlColumnMasterKeySettings как для старого, так и нового главного ключа столбца вместе со значением ключа шифрования столбца, который будет повторно зашифрован.
Да Нет
Шаг 9. Поделитесь с вашим администратором баз данных информацией о расположении нового главного ключа столбца (имя поставщика и путь к главному ключу столбца), а также набором новых зашифрованных значений ключей шифрования данных столбца. См. следующие примеры. Нет Нет

Примечание.

Настоятельно рекомендуется не удалять навсегда старый главный ключ столбца после смены. Вместо этого, вам следует сохранить старый главный ключ столбца в его текущем хранилище ключей или архивировать его в другое надежное место. При восстановлении базы данных из файла резервной копии на момент до настройки нового главного ключа столбца вам потребуется старый ключ для доступа к данным.

Часть 3. Администратор баз данных

Администратор баз данных создает метаданные для нового главного ключа столбца и обновляет метаданные затронутых ключей шифрования столбцов для добавления нового набора зашифрованных значений. На этом этапе администратор баз данных работает вместе с администраторами приложений, выполняющих запросы к столбцам шифрования. Администраторы приложений обеспечивают доступ приложений к новому главному ключу столбца. После настройки всех приложений для использования нового главного ключа столбца администратор баз данных удаляет старый набор зашифрованных значений и метаданные старого главного ключа столбца.

Задача Статья Доступ к ключам с открытым текстом или хранилищу ключей Доступ к базе данных
Шаг 1. Получение от администратора безопасности сведений о расположении нового главного ключа столбца и нового набора зашифрованных значений соответствующих ключей шифрования столбцов, защищенных с помощью старого главного ключа столбца. См. следующие примеры. Нет Нет
Шаг 2. Запуск среды PowerShell и импорт модуля SqlServer. Импорт модуля SqlServer Нет Нет
Шаг 3. Соединение с сервером и базой данных. Подключение к базе данных Нет Да
Шаг 4. Создание объекта SqlColumnMasterKeySettings, содержащего сведения о расположении нового главного ключа столбца. SqlColumnMasterKeySettings — это объект, который существует в памяти (PowerShell). New-SqlColumnMasterKeySettings Нет Нет
Шаг 5. Создайте метаданные о новом главном ключе столбца в базе данных. New-SqlColumnMasterKey

Примечание: Фактически этот командлет выполняет оператор CREATE COLUMN MASTER KEY (Transact-SQL) для создания метаданных ключа.
Нет Да
Шаг 6. Извлеките метаданные о ключах шифрования столбцов, защищенных старым главным ключом столбца. Get-SqlColumnEncryptionKey (Ключ шифрования) Нет Да
Шаг 7. Добавление нового зашифрованного значения (созданного с помощью нового главного ключа столбца) в метаданные для каждого затронутого ключа шифрования столбца. Add-SqlColumnEncryptionKeyValue Нет Да
Шаг 8. Работа с администраторами всех приложений, выполняющих запросы к зашифрованным столбцам в базе данных (и защищенных с помощью старого главного ключа столбца), по обеспечению доступа приложений к новому главному ключу столбца. Создание и хранение основных ключей столбца (Всегда шифруется) Нет Нет
Шаг 9. Завершите ротацию, удалив из базы данных зашифрованные значения, связанные со старым главным ключом столбца.

Заметка: Перед выполнением этого шага убедитесь, что все приложения, запрашивающие зашифрованные столбцы, защищенные с помощью старого главного ключа столбца, настроены для использования нового главного ключа столбца. В случае преждевременного выполнения этого шага некоторые приложения не смогут расшифровывать данные.

На этом шаге удаляется связь между старым главным ключом столбца и защищаемыми ими ключами шифрования столбцов.
Complete-SqlColumnMasterKeyRotation

Кроме того, можно использовать Remove-SqlColumnEncryptionKeyValue
Нет Да
Шаг 10. Удалите метаданные старого главного ключа столбца из базы данных. Remove-SqlColumnMasterKey Нет Да

Смена мастер-ключа столбца с разделением ролей (на примере сертификата Windows)

Приведенный ниже скрипт — законченный пример создания нового главного ключа столбца, который является сертификатом в хранилище сертификатов Windows, смены существующего (текущего) главного ключа столбца для его замены новым главным ключом столбца. В скрипте предполагается, что целевая база данных содержит главный ключ столбца с именем CMK1 (подлежащий смене), который шифрует некоторые ключи шифрования столбцов.

Часть 1. Администратор баз данных

[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. Администратор баз данных

[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'

Вращение ключа шифрования столбца

Процедура смены ключа шифрования столбца включает в себя действия по расшифровке данных во всех столбцах, которые зашифрованы с помощью подлежащего смене ключа, и повторному шифрованию данных с помощью нового ключа шифрования столбца. Поскольку для этого процесса требуется доступ как к ключам, так и к базе данных, его выполнение невозможно при разделении ролей. Смена ключа шифрования столбца может занимать много времени, если таблицы, содержащие столбцы, зашифрованные поворачиваемым ключом, очень большие. Поэтому организации необходимо тщательно спланировать процесс смены ключа шифрования столбца.

Ключ шифрования столбца можно сменить в двух режимах — вне сети или в сети. Первый способ быстрее, но приложения не смогут записывать данные в затронутые таблицы. Последний подход, скорее всего, займет больше времени, но можно ограничить интервал времени, в течение которого затронутые таблицы недоступны для приложений. Дополнительные сведения см. в разделе "Настройка шифрования столбцов с помощью Always Encrypted с помощью PowerShell и Set-SqlColumnEncryption".

Задача Статья Доступ к ключам с открытым текстом или хранилищу ключей Доступ к базе данных
Шаг 1. Запуск среды PowerShell и импорт модуля SqlServer. Импорт модуля SqlServer Нет Нет
Шаг 2. Соединение с сервером и базой данных. Подключение к базе данных Нет Да
Шаг 3. Пройдите аутентификацию в Azure, если ключ главного столбца (который защищает ключ шифрования столбца и подлежит смене) хранится в хранилище ключей или в управляемом модуле HSM в Azure Key Vault. Connect-AzAccount Да Нет
Шаг 4. Получите токен доступа для Azure Key Vaults, если ваш главный ключ столбца хранится в Azure Key Vault. Get-AzAccessToken Нет Нет
Шаг 5. Сгенерируйте новый ключ шифрования столбца, зашифруйте его с помощью главного ключа столбца и создайте метаданные ключа шифрования столбца в базе данных. New-SqlColumnEncryptionKey

Заметка: Используйте вариант командлета, который создает и шифрует ключ шифрования столбца.
Внутренне этот командлет выполняет инструкцию CREATE COLUMN ENCRYPTION KEY (Transact-SQL) для создания метаданных ключа.
Да Да
Шаг 6. Найдите все столбцы, зашифрованные старым ключом шифрования столбца. Руководство по программированию объектов управления SQL Server (SMO) Нет Да
Шаг 7. Создайте объект SqlColumnEncryptionSettings для каждого затронутого столбца. SqlColumnEncryptionSettings — это объект, который существует в памяти (PowerShell). Он определяет целевую схему шифрования столбца. В этом случае объект должен указывать, что затронутый столбец следует зашифровать с помощью нового ключа шифрования столбца. Новые настройки шифрования столбца SQL (New-SqlColumnEncryptionSettings) Нет Нет
Шаг 8. Повторное шифрование столбцов, определенных на шаге 5, с помощью нового ключа шифрования столбца. Set-SqlColumnEncryption

Заметка: Этот шаг может занять много времени. Приложения не будут иметь доступ к таблицам во время выполнения операции или ее части в зависимости от выбранного режима (в сети или вне сети).
Да Да
Шаг 9. Удаление метаданных для старого ключа шифрования столбца. Удалить ключ шифрования столбца SQL (Remove-SqlColumnEncryptionKey) Нет Да

Пример: ротация ключа шифрования столбца

В приведенном ниже скрипте демонстрируется смена ключа шифрования столбца. В скрипте предполагается, что целевая база данных содержит несколько столбцов, зашифрованных с помощью ключа шифрования 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'

Следующие шаги

См. также