Public/New-sqmSqlCertificate.ps1

<#
.SYNOPSIS
    Creates a new self-signed SQL Server certificate as a renewal of an existing one.
 
.DESCRIPTION
    Reads all relevant properties of the existing certificate (Subject, purpose,
    endpoint binding, TDE binding) and creates a new self-signed certificate directly
    in SQL Server using CREATE CERTIFICATE.
 
    Process:
      1. Read existing certificate and determine its purpose
      2. Back up old certificate as .cer + private key as .pvk (BackupPath)
      3. Create new certificate with same subject and new expiry date
      4. Back up the NEW certificate as .cer + .pvk into the same folder (the .cer is the
         public key for AlwaysOn/Broker partners, .cer + .pvk are needed for TDE on secondaries
         and for any restore)
      5. Only if requested, bind based on purpose:
           AlwaysOn -> ALTER ENDPOINT ... FOR DATABASE_MIRRORING (AUTHENTICATION = CERTIFICATE <new>)
           Broker -> ALTER ENDPOINT ... FOR SERVICE_BROKER (AUTHENTICATION = CERTIFICATE <new>)
           TDE -> ALTER DATABASE ENCRYPTION KEY ENCRYPTION BY SERVER CERTIFICATE <new>
      6. Write a renewal protocol as TXT (Subject, thumbprint old/new, bindings, next steps)
 
    The old certificate is neither renamed nor dropped: SQL Server has no way to rename a
    certificate, and it is still needed until all bindings are switched (and, for TDE, to
    restore older backups). Drop it manually after verification.
 
    ORDER FOR ALWAYSON: the partner replicas must know the NEW public key before this replica's
    endpoint is switched, otherwise they reject the connection and the AG disconnects. Run
    without -BindEndpoint, import the new .cer on every partner
    (Install-sqmCertificate -Purpose AlwaysOn -AuthorizationUser ...), then switch the endpoint.
    TDE IN AN AG: the new certificate (.cer + .pvk) must exist on every secondary before -BindTde.
 
    BackupPath is a path on the SQL Server host: BACKUP CERTIFICATE is executed by the service.
 
.PARAMETER SqlInstance
    SQL Server instance (default: current computer name).
 
.PARAMETER SqlCredential
    PSCredential for the connection.
 
.PARAMETER CertificateName
    Name of the certificate to renew (exact name from sys.certificates).
 
.PARAMETER Database
    Database where the certificate resides. Default: master.
 
.PARAMETER NewCertificateName
    Name of the new certificate. Default: <OldName>_<Year> (e.g. AG_CERT_2027).
 
.PARAMETER ValidityYears
    Validity period of the new certificate in years. Default: 5.
 
.PARAMETER BackupPath
    Path for backing up the old certificate (.cer and .pvk).
    Default: from module configuration (OutputPath).
 
.PARAMETER BackupEncryptionPassword
    Password for encrypting the exported private keys (.pvk) of the old and the new certificate.
    Required when the old certificate has a private key.
 
.PARAMETER RenameOldCertificate
    Obsolete, has no effect. SQL Server cannot rename certificates; the old certificate
    always stays under its name until you drop it. Kept for compatibility of existing scripts.
 
.PARAMETER BindEndpoint
    Switch the endpoint (AlwaysOn: DATABASE_MIRRORING, Broker: SERVICE_BROKER) to the new
    certificate immediately. Default: $false. For AlwaysOn only after the partners have the
    new public key.
 
.PARAMETER BindTde
    Re-encrypt the database encryption keys of the TDE databases with the new certificate.
    Default: $false. In an availability group only after the new certificate exists on all
    secondaries.
 
.PARAMETER EnableException
    Throw exceptions immediately.
 
.EXAMPLE
    # Simple renewal without automatic binding
    New-sqmSqlCertificate -SqlInstance "SQL01" -CertificateName "AG_CERT" -BackupEncryptionPassword (Read-Host -AsSecureString)
 
.EXAMPLE
    # With automatic endpoint binding and 10-year validity
    New-sqmSqlCertificate -SqlInstance "SQL01" -CertificateName "AG_CERT" `
        -ValidityYears 10 -BindEndpoint `
        -BackupEncryptionPassword (Read-Host -AsSecureString "Backup-Passwort")
 
.EXAMPLE
    # Renew TDE certificate
    New-sqmSqlCertificate -SqlInstance "SQL01" -CertificateName "TDE_PROD" `
        -BindTde -BackupEncryptionPassword (Read-Host -AsSecureString "Backup-Passwort")
 
.NOTES
    Requires: dbatools, Invoke-sqmLogging, Get-sqmDefaultOutputPath, Copy-sqmToCentralPath
    Requires: sysadmin on the instance.
    AlwaysOn: After renewal the new certificate (.cer) must be imported on all partner replicas
    with the endpoint login's user as owner (Install-sqmCertificate -AuthorizationUser).
    TDE: Key rotation runs online, the database remains available.
#>

function New-sqmSqlCertificate
{
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [string]$SqlInstance,
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$SqlCredential,
        [Parameter(Mandatory = $true)]
        [string]$CertificateName,
        [Parameter(Mandatory = $false)]
        [string]$Database = 'master',
        [Parameter(Mandatory = $false)]
        [string]$NewCertificateName,
        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 20)]
        [int]$ValidityYears = 5,
        [Parameter(Mandatory = $false)]
        [string]$BackupPath,
        [Parameter(Mandatory = $false)]
        [System.Security.SecureString]$BackupEncryptionPassword,
        [Parameter(Mandatory = $false)]
        [bool]$RenameOldCertificate = $true,
        [Parameter(Mandatory = $false)]
        [switch]$BindEndpoint,
        [Parameter(Mandatory = $false)]
        [switch]$BindTde,
        [Parameter(Mandatory = $false)]
        [switch]$EnableException
    )
    
    begin
    {
        $functionName = $MyInvocation.MyCommand.Name
        
        if (-not $PSBoundParameters.ContainsKey('SqlInstance') -or [string]::IsNullOrWhiteSpace($SqlInstance))
        {
            $SqlInstance = $env:COMPUTERNAME
        }
        
        if (-not $script:dbatoolsAvailable)
        {
            $msg = "dbatools-Modul nicht gefunden. Bitte installieren: Install-Module dbatools"
            Invoke-sqmLogging -Message $msg -FunctionName $functionName -Level "ERROR"
            throw $msg
        }
        
        if (-not $BackupPath) { $BackupPath = Get-sqmDefaultOutputPath }
        
        Invoke-sqmLogging -Message ("Starte " + $functionName + ": Zertifikat '$CertificateName' auf $SqlInstance") -FunctionName $functionName -Level "INFO"
    }
    
    process
    {
        try
        {
            $connParams = @{
                SqlInstance   = $SqlInstance
                SqlCredential = $SqlCredential
                Database      = $Database
            }
            # Abfragen in master: eigene Kopie - @connParams enthaelt bereits -Database, ein zusaetzliches
            # -Database bricht unter Windows PowerShell 5.1 mit 'Parameter mehrfach angegeben' ab
            $masterParams = $connParams.Clone()
            $masterParams['Database'] = 'master'
            
            # -------------------------------------------------------------------
            # 1. Bestehendes Zertifikat lesen
            # -------------------------------------------------------------------
            $existingCertQuery = @"
SELECT
    c.name AS CertificateName,
    c.certificate_id,
    c.subject,
    c.start_date,
    c.expiry_date,
    c.issuer_name,
    c.thumbprint,
    c.pvt_key_encryption_type_desc AS PrivateKeyEncryption,
    CASE WHEN c.pvt_key_encryption_type_desc <> 'NO_PRIVATE_KEY' THEN 1 ELSE 0 END AS HasPrivateKey
FROM sys.certificates c
WHERE c.name = '$($CertificateName -replace "'", "''")'
"@

            $existingCert = Invoke-DbaQuery @connParams -Query $existingCertQuery -ErrorAction Stop
            
            if (-not $existingCert)
            {
                throw "Zertifikat '$CertificateName' nicht gefunden in Datenbank '$Database' auf '$SqlInstance'."
            }
            
            # -------------------------------------------------------------------
            # 2. Endpoint-Bindung ermitteln
            # -------------------------------------------------------------------
            $endpointQuery = @"
SELECT
    e.name AS EndpointName,
    e.endpoint_id,
    e.type_desc AS EndpointType,
    e.protocol_desc AS Protocol
FROM sys.endpoints e
INNER JOIN sys.database_mirroring_endpoints dme ON e.endpoint_id = dme.endpoint_id
INNER JOIN sys.certificates c ON dme.certificate_id = c.certificate_id
WHERE c.name = '$($CertificateName -replace "'", "''")'
UNION ALL
SELECT e.name, e.endpoint_id, 'SERVICE_BROKER', e.protocol_desc
FROM sys.endpoints e
INNER JOIN sys.service_broker_endpoints sbe ON e.endpoint_id = sbe.endpoint_id
INNER JOIN sys.certificates c ON sbe.certificate_id = c.certificate_id
WHERE c.name = '$($CertificateName -replace "'", "''")'
"@

            $boundEndpoint = Invoke-DbaQuery @masterParams -Query $endpointQuery -ErrorAction SilentlyContinue
            
            # -------------------------------------------------------------------
            # 3. TDE-Bindung ermitteln
            # -------------------------------------------------------------------
            $tdeQuery = @"
SELECT d.name AS DatabaseName, dek.encryption_state
FROM sys.dm_database_encryption_keys dek
INNER JOIN sys.databases d ON dek.database_id = d.database_id
INNER JOIN sys.certificates c ON dek.encryptor_thumbprint = c.thumbprint
WHERE c.name = '$($CertificateName -replace "'", "''")'
"@

            $tdeDatabases = Invoke-DbaQuery @masterParams -Query $tdeQuery -ErrorAction SilentlyContinue
            
            # -------------------------------------------------------------------
            # 4. Neuen Zertifikatsnamen festlegen
            # -------------------------------------------------------------------
            if (-not $NewCertificateName)
            {
                $NewCertificateName = "$($CertificateName)_$((Get-Date).AddYears($ValidityYears).Year)"
            }
            
            $expiryDate = (Get-Date).AddYears($ValidityYears).ToString('yyyyMMdd')
            $subject = if ($existingCert.subject) { $existingCert.subject }
            else { "CN=$NewCertificateName" }
            $datestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
            $oldRename = "$($CertificateName)_OLD_$(Get-Date -Format 'yyyyMMdd')"
            
            # Passwort-Pruefung: Private Key vorhanden ? Passwort fuer Backup Pflicht
            if ($existingCert.HasPrivateKey -and -not $BackupEncryptionPassword)
            {
                throw "Das Zertifikat '$CertificateName' hat einen Private Key. Bitte -BackupEncryptionPassword angeben fuer die Backup-Verschluesselung."
            }
            
            # ShouldProcess-Bestaetigung
            $action = "Zertifikat '$CertificateName' erneuern ? '$NewCertificateName' (gueltig bis $expiryDate)"
            if (-not $PSCmdlet.ShouldProcess($SqlInstance, $action))
            {
                Invoke-sqmLogging -Message "Abgebrochen durch ShouldProcess." -FunctionName $functionName -Level "INFO"
                return $null
            }
            
            # -------------------------------------------------------------------
            # 5. Backup-Verzeichnis vorbereiten
            # -------------------------------------------------------------------
            $certBackupDir = Join-Path $BackupPath "CertBackup_$(($SqlInstance -replace '\\', '_'))_$datestamp"
            if (-not (Test-Path $certBackupDir)) { New-Item -ItemType Directory -Path $certBackupDir -Force | Out-Null }
            
            # -------------------------------------------------------------------
            # 6. Altes Zertifikat sichern (.cer + optional .pvk)
            # -------------------------------------------------------------------
            # BACKUP CERTIFICATE schreibt der SQL Server-Dienst - Verzeichnis auch serverseitig anlegen
            $certBackupDirSql = $certBackupDir -replace "'", "''"
            Invoke-DbaQuery @masterParams -Query "EXEC master.dbo.xp_create_subdir N'$certBackupDirSql';" -ErrorAction Stop

            # Passwort als Klartext fuer T-SQL (nur im Speicher, nicht geloggt)
            $plainPwd = $null
            if ($BackupEncryptionPassword)
            {
                $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($BackupEncryptionPassword)
                $plainPwd = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) -replace "'", "''"
                [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
            }

            try
            {
                $cerFile = Join-Path $certBackupDir "$($CertificateName)_OLD.cer"
                $pvkFile = $null
                $backupSql = "BACKUP CERTIFICATE [$CertificateName] TO FILE = N'$($cerFile -replace "'", "''")'"
                if ($existingCert.HasPrivateKey)
                {
                    $pvkFile = Join-Path $certBackupDir "$($CertificateName)_OLD.pvk"
                    $backupSql += " WITH PRIVATE KEY (FILE = N'$($pvkFile -replace "'", "''")', ENCRYPTION BY PASSWORD = N'$plainPwd')"
                }
                Invoke-DbaQuery @connParams -Query "$backupSql;" -ErrorAction Stop
                Invoke-sqmLogging -Message "Backup des alten Zertifikats: $cerFile" -FunctionName $functionName -Level "INFO"

                # -------------------------------------------------------------------
                # 7. Neues Zertifikat erstellen
                # -------------------------------------------------------------------
                $createSql = @"
CREATE CERTIFICATE [$NewCertificateName]
WITH SUBJECT = N'$($subject -replace "'", "''")',
     EXPIRY_DATE = N'$expiryDate';
"@

                Invoke-DbaQuery @connParams -Query $createSql -ErrorAction Stop
                Invoke-sqmLogging -Message "Neues Zertifikat '$NewCertificateName' erstellt (gueltig bis $expiryDate)." -FunctionName $functionName -Level "INFO"

                # Neues Zertifikat sofort sichern: .cer = Public Key fuer AG-/Broker-Partner,
                # .cer + .pvk fuer TDE auf Secondaries und fuer jedes spaetere Restore
                $newCerFile = Join-Path $certBackupDir "$($NewCertificateName).cer"
                $newPvkFile = $null
                $newBackupSql = "BACKUP CERTIFICATE [$NewCertificateName] TO FILE = N'$($newCerFile -replace "'", "''")'"
                if ($plainPwd)
                {
                    $newPvkFile = Join-Path $certBackupDir "$($NewCertificateName).pvk"
                    $newBackupSql += " WITH PRIVATE KEY (FILE = N'$($newPvkFile -replace "'", "''")', ENCRYPTION BY PASSWORD = N'$plainPwd')"
                }
                Invoke-DbaQuery @connParams -Query "$newBackupSql;" -ErrorAction Stop
                Invoke-sqmLogging -Message "Backup des neuen Zertifikats: $newCerFile" -FunctionName $functionName -Level "INFO"
                if (-not $newPvkFile)
                {
                    Write-Warning "Private Key des neuen Zertifikats '$NewCertificateName' NICHT gesichert (kein -BackupEncryptionPassword). Sofort nachholen: BACKUP CERTIFICATE ... WITH PRIVATE KEY."
                }
            }
            finally
            {
                $plainPwd = $null # Sofort aus Speicher entfernen
            }

            # Neues Zertifikat lesen fuer Rueckgabeobjekt
            $newCert = Invoke-DbaQuery @connParams -Query ($existingCertQuery -replace [regex]::Escape("'$($CertificateName -replace "'", "''")'"), "'$($NewCertificateName -replace "'", "''")'") -ErrorAction SilentlyContinue

            # -------------------------------------------------------------------
            # 8. Optional: Endpoint-Bindung aktualisieren
            # -------------------------------------------------------------------
            $endpointBound = $false
            if ($BindEndpoint -and $boundEndpoint)
            {
                foreach ($ep in $boundEndpoint)
                {
                    $epClause = if ($ep.EndpointType -eq 'SERVICE_BROKER') { 'SERVICE_BROKER' }
                    else { 'DATABASE_MIRRORING' }
                    $alterEndpointSql = @"
ALTER ENDPOINT [$($ep.EndpointName)]
    FOR $epClause (AUTHENTICATION = CERTIFICATE [$NewCertificateName]);
"@

                    Invoke-DbaQuery @masterParams -Query $alterEndpointSql -ErrorAction Stop
                    Invoke-sqmLogging -Message "Endpoint '$($ep.EndpointName)' auf neues Zertifikat '$NewCertificateName' umgestellt." -FunctionName $functionName -Level "INFO"
                    $endpointBound = $true
                }
            }

            # -------------------------------------------------------------------
            # 9. Optional: TDE-Bindung aktualisieren
            # -------------------------------------------------------------------
            $tdeBound = $false
            if ($BindTde -and $tdeDatabases)
            {
                foreach ($tdeDb in $tdeDatabases)
                {
                    $alterTdeSql = @"
USE [$($tdeDb.DatabaseName)];
ALTER DATABASE ENCRYPTION KEY
    ENCRYPTION BY SERVER CERTIFICATE [$NewCertificateName];
"@

                    Invoke-DbaQuery @masterParams -Query $alterTdeSql -ErrorAction Stop
                    Invoke-sqmLogging -Message "TDE fuer '$($tdeDb.DatabaseName)' auf '$NewCertificateName' umgestellt." -FunctionName $functionName -Level "INFO"
                    $tdeBound = $true
                }
            }

            # -------------------------------------------------------------------
            # 10. Altes Zertifikat bleibt bestehen
            # -------------------------------------------------------------------
            # SQL Server kennt kein Umbenennen von Zertifikaten (-RenameOldCertificate ist wirkungslos).
            # Das alte Zertifikat wird erst nach Verifikation manuell geloescht (siehe Protokoll).
            Invoke-sqmLogging -Message "Altes Zertifikat '$CertificateName' bleibt bestehen. Nach Verifikation manuell loeschen." -FunctionName $functionName -Level "INFO"

            # -------------------------------------------------------------------
            # 11. Bestelldatenblatt schreiben
            # -------------------------------------------------------------------
            $sheetFile = Join-Path $certBackupDir "Erneuerungsprotokoll_${CertificateName}_${datestamp}.txt"
            $lines = [System.Collections.Generic.List[string]]::new()
            
            $lines.Add("=" * 70)
            $lines.Add(" ZERTIFIKAT-ERNEUERUNGSPROTOKOLL")
            $lines.Add(" Erstellt : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
            $lines.Add(" Instanz : $SqlInstance")
            $lines.Add(" Datenbank : $Database")
            $lines.Add("=" * 70)
            $lines.Add("")
            $lines.Add("ALTES ZERTIFIKAT")
            $lines.Add("-" * 40)
            $lines.Add(" Name : $CertificateName")
            $lines.Add(" Subject : $($existingCert.subject)")
            $lines.Add(" Aussteller : $($existingCert.issuer_name)")
            $lines.Add(" Gueltig von : $(if ($existingCert.start_date) { $existingCert.start_date.ToString('yyyy-MM-dd') }
                    else { 'n/a' })"
)
            $lines.Add(" Abgelaufen : $(if ($existingCert.expiry_date) { $existingCert.expiry_date.ToString('yyyy-MM-dd') }
                    else { 'n/a' })"
)
            $lines.Add(" Thumbprint : $([System.BitConverter]::ToString($existingCert.thumbprint).Replace('-', ''))")
            $lines.Add(" Private Key : $(if ($existingCert.HasPrivateKey) { $existingCert.PrivateKeyEncryption }
                    else { 'Kein Private Key' })"
)
            $lines.Add(" Backup .cer : $cerFile")
            if ($pvkFile) { $lines.Add(" Backup .pvk : $pvkFile") }
            $lines.Add("")
            $lines.Add("NEUES ZERTIFIKAT")
            $lines.Add("-" * 40)
            $lines.Add(" Name : $NewCertificateName")
            $lines.Add(" Subject : $subject")
            $lines.Add(" Gueltig bis : $expiryDate")
            if ($newCert)
            {
                $lines.Add(" Thumbprint : $([System.BitConverter]::ToString($newCert.thumbprint).Replace('-', ''))")
            }
            $lines.Add(" Backup .cer : $newCerFile")
            if ($newPvkFile) { $lines.Add(" Backup .pvk : $newPvkFile") }
            $lines.Add("")
            
            if ($boundEndpoint)
            {
                $lines.Add("ENDPOINT-BINDUNG")
                $lines.Add("-" * 40)
                foreach ($ep in $boundEndpoint)
                {
                    $lines.Add(" Endpoint : $($ep.EndpointName) [$($ep.EndpointType)]")
                    $lines.Add(" Umgestellt : $(if ($endpointBound) { 'JA - automatisch' }
                            else { 'NEIN - manuell erforderlich' })"
)
                    if (-not $endpointBound)
                    {
                        $lines.Add(" T-SQL : ALTER ENDPOINT [$($ep.EndpointName)] FOR $(if ($ep.EndpointType -eq 'SERVICE_BROKER') { 'SERVICE_BROKER' } else { 'DATABASE_MIRRORING' })")
                        $lines.Add(" (AUTHENTICATION = CERTIFICATE [$NewCertificateName]);")
                    }
                }
                $lines.Add("")
                $lines.Add(" *** WICHTIG FueR ALWAYSON / SERVICE BROKER ***")
                $lines.Add(" Jeder Partner muss den neuen Public Key kennen, BEVOR der Endpoint umgestellt wird:")
                $lines.Add(" 1. Gesicherter Public Key: $newCerFile")
                $lines.Add(" 2. Auf jedem Partner importieren, Besitzer = Benutzer des Endpoint-Logins:")
                $lines.Add(" Install-sqmCertificate -SqlInstance <Partner> -CertFile <Pfad>\$NewCertificateName.cer \")
                $lines.Add(" -CertificateName '$NewCertificateName' -Purpose AlwaysOn -AuthorizationUser <Benutzer>")
                $lines.Add(" 3. Erst dann hier den Endpoint umstellen (T-SQL oben) und den AG-Status pruefen.")
                $lines.Add("")
            }
            
            if ($tdeDatabases)
            {
                $lines.Add("TDE-BINDUNG")
                $lines.Add("-" * 40)
                foreach ($tdeDb in $tdeDatabases)
                {
                    $lines.Add(" Datenbank : $($tdeDb.DatabaseName)")
                    $lines.Add(" Umgestellt : $(if ($tdeBound) { 'JA - automatisch (online, kein Downtime)' }
                            else { 'NEIN - manuell erforderlich' })"
)
                    if (-not $tdeBound)
                    {
                        $lines.Add(" T-SQL : USE [$($tdeDb.DatabaseName)];")
                        $lines.Add(" ALTER DATABASE ENCRYPTION KEY")
                        $lines.Add(" ENCRYPTION BY SERVER CERTIFICATE [$NewCertificateName];")
                    }
                }
                $lines.Add("")
                $lines.Add(" *** WICHTIG FueR TDE ***")
                if ($newPvkFile)
                {
                    $lines.Add(" Neues Zertifikat inkl. Private Key gesichert: $newPvkFile")
                    $lines.Add(" Sicherung getrennt vom Server aufbewahren, Passwort separat ablegen.")
                }
                else
                {
                    $lines.Add(" Private Key des neuen Zertifikats ist NICHT gesichert - sofort nachholen:")
                    $lines.Add(" BACKUP CERTIFICATE [$NewCertificateName] TO FILE = N'<Pfad>.cer'")
                    $lines.Add(" WITH PRIVATE KEY (FILE = N'<Pfad>.pvk', ENCRYPTION BY PASSWORD = N'<Passwort>');")
                }
                $lines.Add(" AlwaysOn: .cer + .pvk VOR dem Umstellen auf allen Secondaries importieren")
                $lines.Add(" (Install-sqmCertificate -CertFile ... -PrivateKeyFile ... -CertPassword ...).")
                $lines.Add(" Altes Zertifikat behalten, solange Backups existieren, die damit verschluesselt sind.")
                $lines.Add("")
            }
            
            $lines.Add("NaeCHSTE SCHRITTE")
            $lines.Add("-" * 40)
            $lines.Add(" 1. Funktionalitaet des neuen Zertifikats verifizieren")
            $lines.Add(" 2. AlwaysOn-Replikation / TDE-Status pruefen")
            $lines.Add(" 3. Altes Zertifikat '$CertificateName' nach Verifikation loeschen:")
            $lines.Add(" DROP CERTIFICATE [$CertificateName];")
            $lines.Add(" 4. Backup-Dateien sicher archivieren: $certBackupDir")
            $lines.Add(" 5. Zertifikat-Ablaufdatum im Monitoring aktualisieren")
            $lines.Add("")
            $lines.Add("GESICHERTE DATEIEN")
            $lines.Add("-" * 40)
            $lines.Add(" Verzeichnis : $certBackupDir")
            $lines.Add(" Zertifikat : $cerFile")
            if ($pvkFile) { $lines.Add(" Private Key : $pvkFile (mit Passwort verschluesselt)") }
            $lines.Add(" Neu .cer : $newCerFile")
            if ($newPvkFile) { $lines.Add(" Neu .pvk : $newPvkFile (mit Passwort verschluesselt)") }
            $lines.Add(" Protokoll : $sheetFile")
            
            $lines | Out-File -FilePath $sheetFile -Encoding UTF8 -Force
            Invoke-sqmLogging -Message "Erneuerungsprotokoll: $sheetFile" -FunctionName $functionName -Level "INFO"
            
            Copy-sqmToCentralPath -Path @($sheetFile, $cerFile)
            
            # -------------------------------------------------------------------
            # 12. Rueckgabeobjekt
            # -------------------------------------------------------------------
            $result = [PSCustomObject]@{
                SqlInstance           = $SqlInstance
                Database           = $Database
                OldCertificateName = $CertificateName
                NewCertificateName = $NewCertificateName
                NewExpiryDate       = (Get-Date).AddYears($ValidityYears)
                NewThumbprint       = if ($newCert) { [System.BitConverter]::ToString($newCert.thumbprint).Replace('-', '') } else { $null }
                EndpointBound       = $endpointBound
                TdeBound           = $tdeBound
                BackupDirectory    = $certBackupDir
                CerBackupFile       = $cerFile
                PvkBackupFile       = $pvkFile
                NewCerBackupFile   = $newCerFile
                NewPvkBackupFile   = $newPvkFile
                ProtocolFile       = $sheetFile
                Success               = $true
            }
            
            Write-Host "Zertifikat '$NewCertificateName' erfolgreich erstellt." -ForegroundColor Green
            Write-Host "Protokoll : $sheetFile" -ForegroundColor Cyan
            Write-Host "Backup-Dir : $certBackupDir" -ForegroundColor Cyan
            
            return $result
        }
        catch
        {
            $errMsg = "Fehler bei Zertifikat-Erneuerung: $($_.Exception.Message)"
            Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
            if ($EnableException) { throw }
            Write-Error $errMsg
            return [PSCustomObject]@{ Success = $false; ErrorMessage = $errMsg }
        }
    }
    
    end
    {
        Invoke-sqmLogging -Message "$functionName abgeschlossen." -FunctionName $functionName -Level "INFO"
    }
}