Rotación de claves de Always Encrypted con PowerShell

Se aplica a:SQL ServerAzure SQL DatabaseAzure SQL Managed Instance

En este artículo se proporcionan los pasos necesarios para rotar claves de Always Encrypted con el módulo SqlServer PowerShell. Para obtener información sobre el uso del módulo SqlServer PowerShell para Always Encrypted, vea Configure Always Encrypted using PowerShell (Configurar Always Encrypted con PowerShell).

Nota:

Microsoft recomienda usar PowerShell 7 o posterior al ejecutar scripts de PowerShell de Always Encrypted. PowerShell 7 proporciona compatibilidad multiplataforma mejorada, un mejor rendimiento y la compatibilidad más reciente con el módulo SqlServer (v22+), que es necesario para muchos escenarios de Always Encrypted.

La rotación de claves Always Encrypted es el proceso de reemplazar una clave existente por una nueva. Es posible que tenga que rotar una clave si está en peligro o cumplir las directivas de su organización o las regulaciones de cumplimiento que exigen la rotación normal de claves criptográficas.

Always Encrypted emplea dos tipos de claves, por lo que hay dos flujos de trabajo de rotación de claves de alto nivel: rotación de claves maestras de columna y rotación de claves de cifrado de columna.

  • Rotación de claves de cifrado de columna : implica descifrar los datos cifrados con la clave actual y volver a cifrarlos con la nueva clave de cifrado de columna. Como la rotación de una clave de cifrado de columna exige el acceso a las claves y a la base de datos, la rotación de claves de cifrado de columna solo se puede realizar sin separación de roles.
  • Rotación de claves maestras de columna : implica descifrar las claves de cifrado de columna que están protegidas con la clave maestra de columna actual, volver a cifrarlas con la nueva clave maestra de columna y actualizar los metadatos de ambos tipos de claves. La rotación de claves maestras de columna se puede realizar con o sin separación de roles (cuando se usa el módulo SqlServer PowerShell).

Rotación de claves maestras de columna sin separación de roles

El método descrito en esta sección para rotar una clave maestra de columna no admite la separación de roles entre un administrador de seguridad y un DBA. Algunos de los pasos siguientes combinan operaciones en las claves físicas con operaciones en metadatos de clave. Use este flujo de trabajo si la organización usa el modelo de DevOps o cuando la base de datos está hospedada en la nube y el objetivo principal es restringir a los administradores de la nube (pero no a los DBA locales) para acceder a datos confidenciales. No use este método si los posibles adversarios incluyen DBA o si los DBA no deben tener acceso a datos confidenciales.

Tarea Artículo Accede a claves de texto no cifrado o a almacén de claves Accede a base de datos
Paso 1. Cree una nueva clave maestra de columna en un almacén de claves.

Nota: El módulo SqlServer de PowerShell no admite este paso. Para realizar esta tarea desde la línea de comandos, debe usar herramientas específicas del almacén de claves. Al usar Azure Key Vault como almacén de claves, no se admite la rotación de claves administradas por el cliente multiinquilino. Asegúrate de que la nueva clave administrada por el cliente esté en la misma entidad que la existente.
Creación y almacenamiento de claves maestras de columna para Always Encrypted No
Paso 2. Inicie un entorno de PowerShell e importe el módulo SqlServer. Importar el módulo SqlServer No No
Paso 3. Conecte con el servidor y la base de datos. Conexión a una base de datos No
Paso 4. Cree un objeto SqlColumnMasterKeySettings que contenga información sobre la ubicación de la nueva clave maestra de columna. SqlColumnMasterKeySettings es un objeto que existe en memoria (en PowerShell). Para crearlo, debe usar el cmdlet específico del almacén de claves. New-SqlAzureKeyVaultColumnMasterKeySettings

New-SqlCertificateStoreColumnMasterKeySettings

New-SqlCngColumnMasterKeySettings

New-SqlCspColumnMasterKeySettings
No No
Paso 5. Cree los metadatos sobre la nueva clave maestra de columna en la base de datos. New-SqlColumnMasterKey

Nota: En segundo plano, este cmdlet emite la CREATE COLUMN MASTER KEY instrucción (Transact-SQL) para crear metadatos de clave.
No
Paso 6. Autentíquese en Azure si la clave maestra de columna actual o la nueva se almacena en un almacén de claves o un HSM administrado en Azure Key Vault. Connect-AzAccount No
Paso 7. Obtenga un token de acceso para las instancias de Azure Key Vault, si la clave maestra de columna está almacenada en Azure Key Vault. Get-AzAccessToken No No
Paso 8. Comience la rotación cifrando con la nueva clave maestra de columna cada una de las claves de cifrado de columna, que actualmente están protegidas con la antigua clave maestra de columna. Después de este paso, cada clave de cifrado de columna afectada (asociada a la antigua clave maestra de columna y rotada) se cifra con la antigua y la nueva clave maestra de columna y tiene dos valores cifrados en los metadatos de la base de datos. Invoke-SqlColumnMasterKeyRotation
Paso 9. Coordínese con los administradores de todas las aplicaciones que consultan columnas cifradas de la base de datos (y protegidas con la clave maestra de columna anterior) para que puedan garantizar el acceso de las aplicaciones a la nueva clave maestra de columna. Create and Store Column Master Keys (Always Encrypted) (Crear y almacenar claves maestras de columna (Always Encrypted)) No
Paso 10. Finalice la rotación.

Nota: Antes de ejecutar este paso, asegúrese de que todas las aplicaciones que consultan columnas cifradas protegidas con la clave maestra de columna anterior se hayan configurado para usar la nueva clave maestra de columna. Si realiza este paso antes de tiempo, es posible que algunas de esas aplicaciones no puedan descifrar los datos. Finalice la rotación mediante la eliminación de los valores cifrados de la base de datos que se crearon con la clave maestra de columna anterior. Esto elimina la asociación entre la antigua clave maestra de columna y las claves de cifrado de columna que protege.
Complete-SqlColumnMasterKeyRotation No
Paso 11. Elimine los metadatos de la antigua clave maestra de columna. Remove-SqlColumnMasterKey No

Nota:

Es muy recomendable no eliminar permanentemente la antigua clave maestra de columna después de la rotación. Debería conservarla en su almacén de claves actual o archivarla en otra ubicación segura. Si restaura la base de datos desde un archivo de copia de seguridad a un punto en el tiempo anterior a la configuración de la nueva clave maestra de columna, necesitará la clave antigua para acceder a los datos.

Rotación de una clave maestra de columna sin separación de roles (ejemplo de certificado de Windows)

El siguiente script es un ejemplo completo que reemplaza una clave maestra de columna existente (CMK1) por una nueva clave maestra de columna (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.'

Rotación de claves maestras de columna con separación de roles

El flujo de trabajo de rotación de claves maestras de columna que se describe en esta sección garantiza la separación entre un Administrador de seguridad y un DBA.

Importante

Antes de ejecutar cualquiera de los pasos donde Accede a claves de texto no cifrado o a almacén de claves= en la tabla siguiente (pasos que acceden a claves de texto no cifrado o a almacén de claves), asegúrese de que el entorno de PowerShell se ejecuta en un equipo seguro distinto al que hospeda la base de datos. Para obtener más información, consulte Security Considerations for Key Management (Consideraciones de seguridad para la administración de claves).

Parte 1: DBA

Un DBA recupera metadatos sobre la clave maestra de columna que se va a rotar y sobre las claves de cifrado de columna afectadas asociadas a la clave maestra de columna actual. El DBA comparte toda esta información con un Administrador de seguridad.

Tarea Artículo Accede a claves de texto no cifrado o a almacén de claves Accede a base de datos
Paso 1. Inicie un entorno de PowerShell e importe el módulo SqlServer. Importar el módulo SqlServer No Ninguno
Paso 2. Conecte con el servidor y una base de datos. Conexión a una base de datos No
Paso 3. Recupere los metadatos sobre la clave maestra de columna antigua. Get-SqlColumnMasterKey No
Paso 4. Recupere los metadatos sobre las claves de cifrado de columna protegidas por la antigua clave maestra de columna, incluidos sus valores cifrados. Get-SqlColumnEncryptionKey No
Paso 5. Comparta la ubicación de la clave maestra de columna (el nombre del proveedor y una ruta de acceso de la clave maestra de columna) y los valores cifrados de las claves de cifrado de columna correspondientes protegidos con la clave maestra de columna anterior. Consulte los ejemplos más abajo. No No

Parte 2: Administrador de seguridad

El Administrador de seguridad genera una nueva clave maestra de columna, vuelve a cifrar las claves de cifrado de columna afectadas con la nueva clave maestra de columna y comparte la información sobre la nueva clave maestra de columna, así como el conjunto de nuevos valores cifrados de las claves de cifrado de columna afectadas, con el DBA.

Tarea Artículo Acceso a claves de texto no cifrado o a almacén de claves Accede a base de datos
Paso 1. Obtenga de su DBA la ubicación de la clave maestra de columna antigua y los valores cifrados de las claves de cifrado de columna correspondientes, que están protegidos con la clave maestra de columna antigua. No disponible
Consulte los ejemplos más abajo.
No No
Paso 2. Cree una nueva clave maestra de columna en un almacén de claves.

Nota: El módulo SqlServer no admite este paso. Para realizar esta tarea desde una línea de comandos, debe usar herramientas específicas del tipo del almacén de claves. Al usar Azure Key Vault como almacén de claves, no se admite la rotación de claves administradas por el cliente multiinquilino. Asegúrate de que la nueva clave administrada por el cliente esté en la misma entidad que la existente.
Creación y almacenamiento de claves maestras de columna para Always Encrypted No
Paso 3. Inicie un entorno de PowerShell e importe el módulo SqlServer. Importar el módulo SqlServer No No
Paso 4. Cree un objeto SqlColumnMasterKeySettings que contenga información sobre la ubicación de la antigua clave maestra de columna. SqlColumnMasterKeySettings es un objeto que existe en memoria (en PowerShell). New-SqlColumnMasterKeySettings No No
Paso 5. Cree un objeto SqlColumnMasterKeySettings que contenga información sobre la ubicación de la nueva clave maestra de columna. SqlColumnMasterKeySettings es un objeto que existe en memoria (en PowerShell). Para crearlo, debe usar el cmdlet específico del almacén de claves. New-SqlAzureKeyVaultColumnMasterKeySettings

New-SqlCertificateStoreColumnMasterKeySettings

New-SqlCngColumnMasterKeySettings

New-SqlCspColumnMasterKeySettings
No No
Paso 6. Autentíquese en Azure si la clave maestra de columna antigua (actual) o la nueva se almacena en un almacén de claves o un HSM administrado en Azure Key Vault. Connect-AzAccount No
Paso 7. Obtenga un token de acceso para las instancias de Azure Key Vault, si la clave maestra de columna está almacenada en Azure Key Vault. Get-AzAccessToken No No
Paso 8. Vuelva a re-cifrar cada valor de la clave de cifrado de columna, que actualmente está protegida con la antigua clave maestra de columna, utilizando la nueva clave maestra de columna. New-SqlColumnEncryptionKeyEncryptedValue

Nota: Al llamar a este cmdlet, pase los objetos SqlColumnMasterKeySettings de la antigua y la nueva clave maestra de columna, junto con un valor de la clave de cifrado de columna, para que se vuelvan a cifrar.
No
Paso 9. Comparta la ubicación de la nueva clave maestra de columna (el nombre del proveedor y la ruta de acceso de la clave maestra de columna) y el conjunto de valores cifrados nuevos de las claves de cifrado de columna con el DBA. Consulte los ejemplos más abajo. No No

Nota:

Es muy recomendable no eliminar permanentemente la antigua clave maestra de columna después de la rotación. Debería conservarla en su almacén de claves actual o archivarla en otra ubicación segura. Si restaura la base de datos desde un archivo de copia de seguridad a un punto en el tiempo anterior a la configuración de la nueva clave maestra de columna, necesitará la clave antigua para acceder a los datos.

Parte 3: DBA

El DBA crea metadatos para la nueva clave maestra de columna y actualiza los metadatos de las claves de cifrado de columna afectadas para agregar el nuevo conjunto de valores cifrados. En este paso, el DBA también se coordina con los administradores de las aplicaciones que consultan las columnas de cifrado para asegurarse de que la aplicación pueda acceder a la nueva clave maestra de columna. Una vez que todas las aplicaciones están configuradas para usar la nueva clave maestra de columna, el DBA quita el conjunto anterior de valores cifrados y los metadatos antiguos de claves maestras de columna.

Tarea Artículo Acceso a claves de texto no cifrado o a almacén de claves Accede a base de datos
Paso 1. Obtenga la ubicación de la nueva clave maestra de columna y el nuevo conjunto de valores cifrados de las claves de cifrado de columna correspondientes protegidos con la clave maestra de columna antigua del Administrador de seguridad. Consulte los ejemplos más abajo. No No
Paso 2. Inicie un entorno de PowerShell e importe el módulo SqlServer. Importar el módulo SqlServer No No
Paso 3. Conecte con el servidor y una base de datos. Conexión a una base de datos No
Paso 4. Cree un objeto SqlColumnMasterKeySettings que contenga información sobre la ubicación de la nueva clave maestra de columna. SqlColumnMasterKeySettings es un objeto que existe en memoria (en PowerShell). New-SqlColumnMasterKeySettings No No
Paso 5. Cree los metadatos sobre la nueva clave maestra de columna en la base de datos. New-SqlColumnMasterKey

Nota: En segundo plano, este cmdlet emite la CREATE COLUMN MASTER KEY instrucción (Transact-SQL) para crear metadatos de clave.
No
Paso 6. Recupere los metadatos sobre las claves de cifrado de columna protegidas por la antigua clave maestra de columna. Get-SqlColumnEncryptionKey No
Paso 7. Agregue un nuevo valor cifrado (generado con la nueva clave maestra de columna) a los metadatos de cada clave de cifrado de columna afectada. Add-SqlColumnEncryptionKeyValue No
Paso 8. Coordínese con los administradores de todas las aplicaciones que consultan columnas cifradas de la base de datos (y protegidas con la clave maestra de columna anterior) para que puedan garantizar el acceso de las aplicaciones a la nueva clave maestra de columna. Creación y almacenamiento de claves maestras de columna (Always Encrypted) No No
Paso 9. Finalice la rotación mediante la eliminación de los valores cifrados asociados a la clave maestra de columna anterior de la base de datos.

Nota: Antes de ejecutar este paso, asegúrese de que todas las aplicaciones que consultan columnas cifradas protegidas con la clave maestra de columna anterior se hayan configurado para usar la nueva clave maestra de columna. Si realiza este paso antes de tiempo, es posible que algunas de esas aplicaciones no puedan descifrar los datos.

Este paso quita una asociación entre la antigua clave maestra de columna y las claves de cifrado de columna que protege.
Complete-SqlColumnMasterKeyRotation

También puede usar Remove-SqlColumnEncryptionKeyValue
No
Paso 10. Quite de la base de datos los metadatos antiguos de la clave maestra de columna. Remove-SqlColumnMasterKey No

Rotación de una clave maestra de columna con separación de roles (ejemplo de certificado de Windows)

El siguiente script es un ejemplo completo para generar una nueva clave maestra de columna que está certificada en el almacén de certificados de Windows y rotar una clave maestra de columna actualmente existente para reemplazarla por la nueva clave maestra de columna. El script da por hecho que la base de datos de destino contiene la clave maestra de columnas, denominada CMK1 (que se va a rotar), y que cifra algunas claves de cifrado de columnas.

Parte 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"

Parte 2: Administrador de seguridad

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

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

Rotación de una clave de cifrado de columna

La rotación de una clave de cifrado de columna implica descifrar los datos de todas las columnas cifrados con la clave que se va a rotar y volver a cifrarlos con la nueva clave de cifrado de columna. Este flujo de trabajo de rotación exige acceso a las claves y a la base de datos y, por tanto, no se puede realizar con separación de roles. La rotación de una clave de cifrado de columna puede tardar mucho tiempo si las tablas que contienen columnas cifradas con la clave que se va a rotar son grandes. Por lo tanto, la organización tiene que planear cuidadosamente cualquier rotación de claves de cifrado de columna.

Puede girar una clave de cifrado de columna con un enfoque sin conexión o un enfoque en línea. El primer método probablemente sea más rápido, pero las aplicaciones no pueden escribir en las tablas afectadas. El último enfoque probablemente demore más, pero puede limitar el intervalo de tiempo durante el cual las tablas afectadas no estarán disponibles para las aplicaciones. Para más información, consulte Configuración del cifrado de columna mediante Always Encrypted con PowerShell y Set-SqlColumnEncryption.

Tarea Artículo Accede a claves de texto no cifrado o a almacén de claves Accede a base de datos
Paso 1. Inicie un entorno de PowerShell e importe el módulo SqlServer. Importar el módulo SqlServer No No
Paso 2. Conecte con el servidor y una base de datos. Conexión a una base de datos No
Paso 3. Autentíquese en Azure si su clave maestra de columna, que protege la clave de cifrado de columna que se va a rotar, está almacenada en un almacén de claves o en un HSM gestionado dentro de Azure Key Vault. Connect-AzAccount No
Paso 4. Obtenga un token de acceso para las instancias de Azure Key Vault, si la clave maestra de columna está almacenada en Azure Key Vault. Get-AzAccessToken No No
Paso 5. Genere una nueva clave de cifrado de columna, cífrela con la clave maestra de columna y cree los metadatos de clave de cifrado de columna en la base de datos. New-SqlColumnEncryptionKey

Nota: Use una variación del cmdlet que genera internamente y cifra una clave de cifrado de columna.
En segundo plano, este cmdlet emite la CREATE COLUMN ENCRYPTION KEY instrucción (Transact-SQL) para crear los metadatos de clave.
Paso 6. Busque todas las columnas cifradas con la clave de cifrado de columna anterior. Guía de programación para objetos de administración de SQL Server (SMO) No
Paso 7. Cree un objeto SqlColumnEncryptionSettings para cada columna afectada. SqlColumnEncryptionSettings es un objeto que existe en memoria (en PowerShell). Especifica el esquema de cifrado de destino de una columna. En este caso, el objeto debe especificar que la columna afectada debe cifrarse con la nueva clave de cifrado de columna. ConfiguraciónNuevaDeCifradoDeColumnasSql No No
Paso 8. Vuelva a cifrar las columnas identificadas en el paso 5 con la nueva clave de cifrado de columna. Set-SqlColumnEncryption

Nota: Este paso puede llevar mucho tiempo. Las aplicaciones no podrán tener acceso a las tablas durante toda la operación o parte de esta, dependiendo del enfoque (en línea o sin conexión) que seleccione.
Paso 9. Quite los metadatos de la clave de cifrado de columna anterior. Eliminar-SqlColumnEncryptionKey No

Ejemplo: rotación de una clave de cifrado de columna

El siguiente script muestra la rotación de una clave de cifrado de columna. El script da por hecho que la base de datos de destino contiene algunas columnas cifradas con una clave de cifrado de columna denominada CEK1 (que se va a rotar), que está protegida con una clave maestra de columna denominada CMK1 (la clave maestra de columna no se almacena en 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'

Pasos siguientes

Consulte también