Mindig titkosított kulcsok elforgatása a PowerShell használatával

A következőkre vonatkozik:SQL ServerAzure SQL DatabaseFelügyelt Azure SQL-példány

Ez a cikk az Always Encrypted kulcsainak az SqlServer PowerShell-modullal történő forgatásának lépéseit ismerteti. Az Always Encryptedhez készült SqlServer PowerShell-modul használatának megkezdéséről további információt az Always Encrypted konfigurálása a PowerShell használatával című témakörben talál.

Jegyzet

Microsoft a PowerShell 7 vagy újabb verzióját javasolja Always Encrypted PowerShell-szkriptek futtatásakor. A PowerShell 7 továbbfejlesztett platformfüggetlen támogatást, jobb teljesítményt és a legújabb kompatibilitást biztosít az SqlServer modullal (v22+), amely számos Always Encrypted-forgatókönyvhöz szükséges.

Az Always Encrypted kulcsok elforgatása a meglévő kulcsok újra cserélésének folyamata. Előfordulhat, hogy el kell forgatnia egy kulcsot, ha az sérült, vagy meg kell felelnie a szervezet szabályzatainak vagy megfelelőségi előírásainak, amelyek rendszeres titkosítási kulcsváltást tesznek szükségessé.

Az Always Encrypted kétféle kulcstípust használ, ezért két magas szintű kulcsforgatási munkafolyamat létezik; az oszlop főkulcsának elforgatása és az oszloptitkosítási kulcsok elforgatása.

  • Oszloptitkosítási kulcs rotálása – az aktuális kulccsal titkosított adatok visszafejtésével és az új oszloptitkosítási kulccsal történő újratitkosításával jár. Mivel az oszloptitkosítási kulcsok elforgatásához mind a kulcsokhoz, mind az adatbázishoz hozzá kell férni, az oszloptitkosítási kulcs elforgatása csak szerepkör-elkülönítés nélkül végezhető el.
  • Oszlop főkulcsának elforgatása – az aktuális oszlop főkulcsával védett oszloptitkosítási kulcsok visszafejtésével, az új oszlop főkulcsával történő újratitkosításával, valamint a metaadatok mindkét típusú kulcs esetében történő frissítésével jár. Az oszlop főkulcs-elforgatása szerepkör-elkülönítéssel vagy anélkül is elvégezhető (az SqlServer PowerShell-modul használatakor).

Oszlop főkulcsának elforgatása szerepkör-elkülönítés nélkül

Az oszlop főkulcsának elforgatásához ebben a szakaszban ismertetett módszer nem támogatja a biztonsági rendszergazda és a DBA közötti szerepkör-elkülönítést. Az alábbi lépések némelyike egyesíti a fizikai kulcsok műveleteit a kulcs metaadatain végzett műveletekkel. Ezt a munkafolyamatot akkor használja, ha a szervezet a DevOps-modellt használja, vagy ha az adatbázis a felhőben van üzemeltetve, és az elsődleges cél az, hogy korlátozza a felhőgazdák (de nem a helyszíni adatbázis-üzemeltetők) hozzáférését a bizalmas adatokhoz. Ne használja ezt a módszert, ha a lehetséges támadók adatbázis-kezelőket is tartalmazhatnak, vagy ha a dbA-k nem férhetnek hozzá bizalmas adatokhoz.

Feladat Cikk Szövegformátumú kulcsok/keystore-ok elérése Adatbázis elérése
1. lépés. Hozzon létre egy új oszlop főkulcsát egy kulcstárolóban.

Jegyzet: Az SqlServer PowerShell-modul nem támogatja ezt a lépést. A feladat parancssorból történő végrehajtásához a kulcstárolóhoz tartozó eszközöket kell használnia. Ha az Azure Key Vaultot használja kulcstárolóként, a több-bérlős ügyfél által felügyelt kulcsváltás nem támogatott. Győződjön meg arról, hogy az új ügyfél által kezelt kulcs ugyanabban a bérlőben van, mint a meglévő.
Oszlop főkulcsainak létrehozása és tárolása az Always Encryptedhez Igen Nem
2. lépés. PowerShell-környezet indítása és az SqlServer modul importálása Az SqlServer modul importálása Nem Nem
3. lépés. Csatlakozzon a kiszolgálóhoz és az adatbázishoz. Csatlakozás adatbázishoz Nem Igen
4. lépés. Hozzon létre egy SqlColumnMasterKeySettings objektumot, amely információkat tartalmaz az új oszlop főkulcsának helyéről. Az SqlColumnMasterKeySettings egy olyan objektum, amely a memóriában (a PowerShellben) létezik. A létrehozásához a kulcstárolóra jellemző parancsmagot kell használnia. Új-SqlAzureKeyVaultOszlopFőKulcsBeállítások

Új-SqlTanúsítványTárolóOszlopFőkulcsBeállítások

Új-SqlCngOszlopMesterKulcsBeállítások

Új-SqlCspOszlopMesterKulcsBeállítások
Nem Nem
5. lépés. Hozza létre az új oszlop főkulcsának metaadatait az adatbázisban. New-SqlColumnMasterKey

Megjegyzés: A borítók alatt ez a parancsmag kiadja a CREATE COLUMN MASTER KEY (Transact-SQL) utasítást a kulcs metaadatainak létrehozásához.
Nem Igen
6. lépés. Hitelesítés az Azure-ban, ha az aktuális oszlop főkulcsa vagy az új oszlop főkulcsa kulcstartóban vagy felügyelt HSM-ben van tárolva az Azure Key Vaultban Connect-AzAccount Igen Nem
7. lépés. Szerezze be az Azure Key Vaultok hozzáférési jogkivonatát, ha az oszlop főkulcsa az Azure Key Vaultban van tárolva. Get-AzAccessToken Nem Nem
8. lépés. Indítsa el a forgatási folyamatot azzal, hogy az egyes oszloptitkosítási kulcsokat titkosítja, amelyek a régi oszlop főkulccsal vannak védve, az új oszlop főkulcs használatával. A lépés után az egyes érintett oszloptitkosítási kulcsok (amelyek a forgatás alatt álló régi oszlop főkulcshoz tartoznak) mind a régi, mind az új oszlop főkulccsal titkosítva vannak, és két titkosított értékkel rendelkeznek az adatbázis metaadataiban. Invoke-SqlColumnMasterKeyRotation Igen Igen
9. lépés. Az adatbázis titkosított oszlopait lekérdező (és a régi oszlop főkulcsával védett) alkalmazások rendszergazdáival együttműködve biztosíthatják, hogy az alkalmazások hozzáférhessenek az új oszlop főkulcsához. Oszlop főkulcsának létrehozása és tárolása (Mindig titkosított) Igen Nem
10. lépés. A forgatás befejezése

Megjegyzés: A lépés végrehajtása előtt győződjön meg arról, hogy a régi oszlop főkulcsával védett titkosított oszlopokat lekérdező összes alkalmazás konfigurálva lett az új oszlopmintakulcs használatára. Ha ezt a lépést idő előtt hajtja végre, előfordulhat, hogy néhány alkalmazás nem tudja visszafejteni az adatokat. Végezze el a forgatást úgy, hogy eltávolítja a titkosított értékeket a régi oszlop főkulcsával létrehozott adatbázisból. Ez eltávolítja a régi oszlop főkulcsa és az általa védett oszloptitkosítási kulcsok közötti társításokat.
Complete-SqlColumnMasterKeyRotation Nem Igen
11. lépés Távolítsa el a metaadatokat a régi oszlop főkulcsából. Távolítsa el az SqlColumnMasterKey kulcsát Nem Igen

Jegyzet

Erősen ajánlott, hogy a forgatás után ne törölje véglegesen a régi oszlop főkulcsát. Ehelyett a régi oszlop főkulcsát a jelenlegi kulcstárolójában kell tartania, vagy egy másik biztonságos helyen kell archiválnia. Ha egy biztonságimásolat-fájlból visszaállítja az adatbázist az új oszlop főkulcsának konfigurálása előtti időpontra, az adatok eléréséhez szüksége lesz a régi kulcsra.

Egy oszlop főkulcsának forgatása szerepkör különválasztása nélkül (Windows-tanúsítvány példa)

Az alábbi szkript egy teljes példa, amely lecserél egy meglévő oszlop mesterkulcsot (CMK1) egy új oszlop mesterkulcsra (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.'

Oszlop főkulcsának cseréje szerepkör-elkülönítéssel

Az ebben a szakaszban ismertetett főkulcs-rotációs munkafolyamat biztosítja a biztonsági rendszergazda és a DBA elkülönítését.

Fontos

Mielőtt végrehajtaná az alábbi táblázatban szereplő azon lépéseket, amelyek hozzáférnek a szövegesen tárolt kulcsokhoz vagy a kulcstárolóhoz=Yes értéket kap, győződjön meg róla, hogy a PowerShell környezet egy olyan biztonságos gépen fut, amely különbözik az adatbázist futtató számítógéptől. További információ: A kulcskezelés biztonsági szempontjai.

1. rész: DBA

A DBA lekéri az elforgatni kívánt oszlop főkulcsának metaadatait, valamint az aktuális oszlop főkulcsához társított érintett oszloptitkosítási kulcsokat. A DBA ezeket az információkat egy biztonsági rendszergazdával osztja meg.

Feladat Cikk Szövegformátumú kulcsok/keystore-ok elérése Adatbázis elérése
1. lépés. Indítsa el a PowerShell-környezetet, és importálja az SqlServer modult. Az SqlServer modul importálása Nem Egyik sem
2. lépés. Csatlakozzon a kiszolgálóhoz és egy adatbázishoz. Csatlakozás adatbázishoz Nem Igen
3. lépés. Kérje le a régi oszlop főkulcsának metaadatait. Get-SqlColumnMasterKey Nem Igen
4. lépés. Kérje le az oszloptitkosítási kulcsok metaadatait, amelyeket a régi oszlop főkulcsa véd, beleértve azok titkosított értékeit is. Get-SqlColumnEncryptionKey Nem Igen
5. lépés. Ossza meg az oszlop főkulcsának helyét (az oszlop főkulcsának szolgáltatónevét és kulcsútvonalát) és a megfelelő oszloptitkosítási kulcsok titkosított értékeit, amelyek a régi oszlop főkulcsával védettek. Lásd az alábbi példákat. Nem Nem

2. rész: Biztonsági rendszergazda

A biztonsági rendszergazda létrehoz egy új oszlop-főkulcsot, újra titkosítja az érintett oszloptitkosítási kulcsokat az új oszloptitkosítási kulccsal, és megosztja az új oszlop főkulcsával, valamint az érintett oszloptitkosítási kulcsok új titkosított értékeinek készletével kapcsolatos információkat a DBA-val.

Feladat Cikk Egyszerű szöveges kulcsok/keystore elérése Adatbázis elérése
1. lépés. Szerezze be a régi oszlop főkulcsának helyét és a megfelelő oszloptitkosítási kulcsok titkosított értékeit, amelyek a régi oszlop főkulcsával védettek a DBA-ból. Nincs adat.
Lásd az alábbi példákat.
Nem Nem
2. lépés. Hozzon létre egy új oszlop főkulcsát egy kulcstárolóban.

Jegyzet: Az SqlServer modul nem támogatja ezt a lépést. Ha parancssorból szeretné elvégezni ezt a feladatot, a kulcstároló típusának megfelelő eszközöket kell használnia. Ha az Azure Key Vaultot használja kulcstárolóként, a több-bérlős ügyfél által felügyelt kulcsváltás nem támogatott. Győződjön meg arról, hogy az új ügyfél által kezelt kulcs ugyanabban a bérlőben van, mint a meglévő.
Oszlop főkulcsainak létrehozása és tárolása az Always Encryptedhez Igen Nem
3. lépés. Indítsa el a PowerShell-környezetet, és importálja az SqlServer modult. Az SqlServer modul importálása Nem Nem
4. lépés. Hozzon létre egy SqlColumnMasterKeySettings objektumot, amely információkat tartalmaz a régi oszlop főkulcsának helyéről. Az SqlColumnMasterKeySettings egy olyan objektum, amely a memóriában (a PowerShellben) létezik. Új-SqlOszlopfőkulcsBeállítások Nem Nem
5. lépés. Hozzon létre egy SqlColumnMasterKeySettings objektumot, amely információkat tartalmaz az új oszlop főkulcsának helyéről. Az SqlColumnMasterKeySettings egy olyan objektum, amely a memóriában (a PowerShellben) létezik. A létrehozásához a kulcstárolóra jellemző parancsmagot kell használnia. Új-SqlAzureKeyVaultOszlopFőKulcsBeállítások

Új-SqlTanúsítványTárolóOszlopFőkulcsBeállítások

Új-SqlCngOszlopMesterKulcsBeállítások

Új-SqlCspOszlopMesterKulcsBeállítások
Nem Nem
6. lépés. Hitelesítés az Azure-ban, ha a régi (jelenlegi) oszlop főkulcsa vagy az új oszlop főkulcsa kulcstartóban vagy felügyelt HSM-ben van tárolva az Azure Key Vaultban. Connect-AzAccount Igen Nem
7. lépés. Szerezze be az Azure Key Vaultok hozzáférési jogkivonatát, ha az oszlop főkulcsa az Azure Key Vaultban van tárolva. Get-AzAccessToken Nem Nem
8. lépés. Az új oszloptitkosítási kulccsal újratitkosíthatja az oszloptitkosítási kulcs minden értékét, amely jelenleg a régi oszlop főkulcsával van védve. New-SqlColumnEncryptionKeyEncryptedValue

Jegyzet: A parancsmag meghívásakor adja át az SqlColumnMasterKeySettings objektumokat a régi és az új oszlop főkulcsához, valamint az oszloptitkosítási kulcs értékét, hogy újra titkosítva legyen.
Igen Nem
9. lépés. Ossza meg az új oszlop főkulcsának helyét (az oszlop főkulcsának szolgáltatónevét és kulcsútvonalát) és az oszloptitkosítási kulcsok új titkosított értékeinek készletét a DBA-val. Lásd az alábbi példákat. Nem Nem

Jegyzet

Erősen ajánlott, hogy a forgatás után ne törölje véglegesen a régi oszlop főkulcsát. Ehelyett a régi oszlop főkulcsát a jelenlegi kulcstárolójában kell tartania, vagy egy másik biztonságos helyen kell archiválnia. Ha egy biztonságimásolat-fájlból visszaállítja az adatbázist az új oszlop főkulcsának konfigurálása előtti időpontra, az adatok eléréséhez szüksége lesz a régi kulcsra.

3. rész: DBA

A DBA metaadatokat hoz létre az új oszlop főkulcsához, és frissíti az érintett oszloptitkosítási kulcsok metaadatait az új titkosított értékek hozzáadásához. Ebben a lépésben a DBA egyeztet a titkosítási oszlopokat lekérdező alkalmazások rendszergazdáival is, akik biztosítják, hogy az alkalmazás hozzáférhessen az új oszlop főkulcsához. Miután minden alkalmazás be van állítva az új oszlop főkulcsának használatára, a DBA eltávolítja a régi titkosított értékeket és a régi oszlop főkulcs-metaadatait.

Feladat Cikk Egyszerű szöveges kulcsok/keystore elérése Adatbázis elérése
1. lépés. Szerezze be az új oszlop főkulcsának helyét és a megfelelő oszloptitkosítási kulcsok új, a régi oszlop főkulcsával védett titkosított értékeit a biztonsági rendszergazdától. Lásd az alábbi példákat. Nem Nem
2. lépés. Indítsa el a PowerShell-környezetet, és importálja az SqlServer modult. Az SqlServer modul importálása Nem Nem
3. lépés. Csatlakozzon a kiszolgálóhoz és egy adatbázishoz. Csatlakozás adatbázishoz Nem Igen
4. lépés. Hozzon létre egy SqlColumnMasterKeySettings objektumot, amely információkat tartalmaz az új oszlop főkulcsának helyéről. Az SqlColumnMasterKeySettings egy olyan objektum, amely a memóriában (a PowerShellben) létezik. Új-SqlOszlopfőkulcsBeállítások Nem Nem
5. lépés. Hozza létre az új oszlop főkulcsának metaadatait az adatbázisban. New-SqlColumnMasterKey

Megjegyzés: A háttérben ez a parancsmag a kulcsmetaadatok létrehozásához kiadja a CREATE COLUMN MASTER KEY (Transact-SQL) utasítást.
Nem Igen
6. lépés. Kérje le a metaadatokat az oszloptitkosítási kulcsokról, amelyeket a régi oszlop főkulcsa véd. Get-SqlColumnEncryptionKey Nem Igen
7. lépés. Adjon hozzá egy új (az új oszlop főkulcsával előállított) titkosított értéket az egyes érintett oszloptitkosítási kulcsok metaadataihoz. Add-SqlOszlopTitkosításKulcsÉrték Nem Igen
8. lépés. Az adatbázis titkosított oszlopait lekérdező (és a régi oszlop főkulcsával védett) alkalmazások rendszergazdáival együttműködve biztosíthatják, hogy az alkalmazások hozzáférhessenek az új oszlop főkulcsához. Oszlop főkulcsainak létrehozása és tárolása (Mindig titkosított) Nem Nem
9. lépés. A forgatás befejezéséhez távolítsa el a régi oszlop főkulcsához társított titkosított értékeket az adatbázisból.

Jegyzet: A lépés végrehajtása előtt győződjön meg arról, hogy minden olyan alkalmazás, amely a régi oszlop főkulcsával védett titkosított oszlopokat kérdez le, konfigurálva legyen az új oszlopmintakulcs használatára. Ha ezt a lépést idő előtt hajtja végre, előfordulhat, hogy néhány alkalmazás nem tudja visszafejteni az adatokat.

Ez a lépés eltávolítja a régi oszlop főkulcsa és az általa védett oszloptitkosítási kulcsok közötti társításokat.
Complete-SqlColumnMasterKeyRotation

Másik lehetőségként a Remove-SqlColumnEncryptionKeyValue parancsot is használhatja
Nem Igen
10. lépés. A régi oszlop főkulcs-metaadatainak eltávolítása az adatbázisból Távolítsa el az SqlColumnMasterKey kulcsát Nem Igen

Oszlop főkulcsa forgatása szerepkörök szétválasztásával (a Windows-tanúsítvány példáján keresztül)

Az alábbi szkript egy átfogó példa arra, hogyan lehet egy új oszlop főkulcsot generálni, amely tanúsítványként szerepel a Windows Tanúsítványtárolóban. Ez magában foglalja egy meglévő (jelenlegi) oszlop főkulcsának kicserélését az új oszlop főkulcsra. A szkript feltételezi, hogy a céladatbázis tartalmazza a CMK1 (forgatandó) nevű oszlop főkulcsát, amely titkosít néhány oszloptitkosítási kulcsot.

1. rész: 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. rész: Biztonsági rendszergazda

[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. rész: 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'

Oszloptitkosítási kulcs elforgatása

Az oszloptitkosítási kulcs elforgatása során az összes oszlop adatainak visszafejtése történik az elforgatandó kulccsal, majd az adatok újratitkosítása az új oszloptitkosítási kulccsal. Ez a rotációs munkafolyamat a kulcsokhoz és az adatbázishoz is hozzáférést igényel, ezért szerepkör-elkülönítéssel nem hajtható végre. Az oszloptitkosítási kulcsok elforgatása hosszú időt vehet igénybe, ha a kulccsal titkosított oszlopokat tartalmazó táblák nagyok. Ezért a szervezetnek körültekintően kell megterveznie az oszloptitkosítási kulcs rotálását.

Az oszloptitkosítási kulcsot offline vagy online módon is elforgathatja. A korábbi módszer valószínűleg gyorsabb lesz, de az alkalmazások nem tudnak írni az érintett táblákba. Az utóbbi megközelítés valószínűleg hosszabb időt vesz igénybe, de korlátozhatja az időintervallumot, amely alatt az érintett táblák nem érhetők el az alkalmazások számára. További információért lásd: Oszloptitkosítás konfigurálása a PowerShell Always Encrypted használatával és a Set-SqlColumnEncryption használatával.

Feladat Cikk Szövegformátumú kulcsok/keystore-ok elérése Adatbázis elérése
1. lépés. Indítsa el a PowerShell-környezetet, és importálja az SqlServer modult. Az SqlServer modul importálása Nem Nem
2. lépés. Csatlakozzon a kiszolgálóhoz és egy adatbázishoz. Csatlakozás adatbázishoz Nem Igen
3. lépés. Jelentkezzen be az Azure-ba, ha az oszlop főkulcsa, amely az elforgatandó oszloptitkosítási kulcsot védi, az Azure Key Vault kulcstartójában vagy egy felügyelt HSM-ben van tárolva. Connect-AzAccount Igen Nem
4. lépés. Szerezze be az Azure Key Vaultok hozzáférési jogkivonatát, ha az oszlop főkulcsa az Azure Key Vaultban van tárolva. Get-AzAccessToken Nem Nem
5. lépés. Hozzon létre egy új oszloptitkosítási kulcsot, titkosítsa az oszlop főkulcsával, és hozzon létre oszloptitkosítási kulcs metaadatait az adatbázisban. New-SqlColumnEncryptionKey

Jegyzet: Használja a parancsmag egy olyan változatát, amely belsőleg létrehoz és titkosít egy oszloptitkosítási kulcsot.
A háttérben ez a parancsmag a CREATE COLUMN ENCRYPTION KEY (Transact-SQL) utasítást hajtja végre a kulcs metaadatainak létrehozásához.
Igen Igen
6. lépés. Keresse meg a régi oszloptitkosítási kulccsal titkosított összes oszlopot. SQL Server Management Objects (SMO) programozási útmutató Nem Igen
7. lépés. Hozzon létre egy SqlColumnEncryptionSettings objektumot minden érintett oszlophoz. Az SqlColumnEncryptionSettings egy olyan objektum, amely a memóriában (a PowerShellben) létezik. Egy oszlop céltitkosítási sémájának megadása. Ebben az esetben az objektumnak meg kell adnia, hogy az érintett oszlopot az új oszloptitkosítási kulcs használatával kell titkosítani. New-SqlColumnEncryptionSettings Nem Nem
8. lépés. Az 5. lépésben azonosított oszlopok újratitkosítása az új oszloptitkosítási kulccsal. Set-SqlColumnEncryption

Jegyzet: Ez a lépés hosszú időt vehet igénybe. Az alkalmazások a választott megközelítéstől (online vagy offline) függően nem tudják elérni a táblákat a teljes művelettel vagy annak egy részével.
Igen Igen
9. lépés. Távolítsa el a régi oszloptitkosítási kulcs metaadatait. Remove-SqlColumnEncryptionKey Nem Igen

Példa – Oszloptitkosítási kulcs elforgatása

Az alábbi szkript egy oszloptitkosítási kulcs elforgatását mutatja be. A szkript feltételezi, hogy a céladatbázis tartalmaz néhány oszloptitkosítási kulccsal titkosított, CEK1 (forgatandó) nevű oszloptitkosítási kulcsot, amelyet cmK1 nevű oszlop-főkulcs véd (az oszlop főkulcsa nem az Azure Key Vaultban van tárolva).

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

Következő lépések

Lásd még: