From 9b18dc9613d812ee5854a284d8918d1edcc4383a Mon Sep 17 00:00:00 2001 From: Andreas Jordan Date: Mon, 31 Aug 2026 16:57:16 +0200 Subject: [PATCH 1/2] Set-DbaNetworkCertificate - Say why a private key is unsuitable A certificate whose private key lives in a Key Storage Provider - what New-SelfSignedCertificate creates by default - is refused with "Failed checks: PrivateKeyInvalid", which sends people looking at key permissions. The real reason is documented by Microsoft: SQL Server can only use a legacy CSP key created with KeySpec AT_KEYEXCHANGE, CNG keys are not supported at all. Test-DbaNetworkCertificate already knows the key type and key number, but the message did not use them. The failed check now names the requirement and describes the key it found (type and KeyNumber when the key is accessible, otherwise "not accessible or a CNG (Key Storage Provider) key"). The check itself is unchanged - it was right. The PrivateKeyValid description of Test-DbaNetworkCertificate says the same. Verified in the lab on PowerShell 7.6 and 5.1 with a default New-SelfSignedCertificate on the target host: the command refuses with the new message and configures nothing. The new test does the same against InstanceRestart and asserts the message. (do *NetworkCertificate*) Co-Authored-By: Claude Fable 5 --- public/Set-DbaNetworkCertificate.ps1 | 15 ++++++++++++- public/Test-DbaNetworkCertificate.ps1 | 2 +- tests/Set-DbaNetworkCertificate.Tests.ps1 | 26 +++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/public/Set-DbaNetworkCertificate.ps1 b/public/Set-DbaNetworkCertificate.ps1 index c84dfdcfa71..8a37c4764e0 100644 --- a/public/Set-DbaNetworkCertificate.ps1 +++ b/public/Set-DbaNetworkCertificate.ps1 @@ -316,7 +316,20 @@ function Set-DbaNetworkCertificate { if (-not $detailedCertTest.CertificateFound) { $failedChecks += "CertificateNotFound" } if ($detailedCertTest.CertificateFound -and -not $detailedCertTest.KeyUsagesValid) { $failedChecks += "KeyUsagesInvalid" } if ($detailedCertTest.CertificateFound -and -not $detailedCertTest.DnsNamesValid) { $failedChecks += "DnsNamesInvalid" } - if ($detailedCertTest.CertificateFound -and -not $detailedCertTest.PrivateKeyValid) { $failedChecks += "PrivateKeyInvalid" } + if ($detailedCertTest.CertificateFound -and -not $detailedCertTest.PrivateKeyValid) { + # SQL Server can only use a private key from a legacy Cryptographic Service Provider with KeySpec + # AT_KEYEXCHANGE; a Key Storage Provider (CNG) key - what New-SelfSignedCertificate creates by + # default - is refused by SQL Server itself. Say so, because "invalid" alone sends people looking + # at permissions. + $privateKeyDescription = "not accessible or a CNG (Key Storage Provider) key" + if ($detailedCertTest.PrivateKeyType) { + $privateKeyDescription = $detailedCertTest.PrivateKeyType + if ($null -ne $detailedCertTest.PrivateKeyNumber) { + $privateKeyDescription += " with KeyNumber $($detailedCertTest.PrivateKeyNumber)" + } + } + $failedChecks += "PrivateKeyInvalid (SQL Server needs a legacy CSP key with KeySpec AT_KEYEXCHANGE; this private key is $privateKeyDescription)" + } if ($detailedCertTest.CertificateFound -and -not $detailedCertTest.PublicKeyValid) { $failedChecks += "PublicKeyInvalid" } if ($detailedCertTest.CertificateFound -and -not $detailedCertTest.SignatureAlgorithmValid) { $failedChecks += "SignatureAlgorithmInvalid" } if ($detailedCertTest.CertificateFound -and -not $detailedCertTest.EnhancedKeyUsageValid) { $failedChecks += "EnhancedKeyUsageInvalid" } diff --git a/public/Test-DbaNetworkCertificate.ps1 b/public/Test-DbaNetworkCertificate.ps1 index 91f0e51b1ad..d0bca4f465a 100644 --- a/public/Test-DbaNetworkCertificate.ps1 +++ b/public/Test-DbaNetworkCertificate.ps1 @@ -78,7 +78,7 @@ function Test-DbaNetworkCertificate { - CertificateFound: Boolean indicating if the certificate was found in LocalMachine\My - KeyUsagesValid: Boolean indicating if the certificate has the required key usages (DigitalSignature and KeyEncipherment) - DnsNamesValid: Boolean indicating if the certificate's DNS names include the server's network name - - PrivateKeyValid: Boolean indicating if the private key is RSACryptoServiceProvider with KeyNumber Exchange + - PrivateKeyValid: Boolean indicating if the private key is RSACryptoServiceProvider with KeyNumber Exchange (a legacy CSP key with KeySpec AT_KEYEXCHANGE, which SQL Server requires; CNG / Key Storage Provider keys are not supported by SQL Server) - PublicKeyValid: Boolean indicating if the public key is RSA with at least 2048 bits - SignatureAlgorithmValid: Boolean indicating if the signature algorithm is SHA-256, SHA-384, or SHA-512 - EnhancedKeyUsageValid: Boolean indicating if the certificate has the Server Authentication enhanced key usage diff --git a/tests/Set-DbaNetworkCertificate.Tests.ps1 b/tests/Set-DbaNetworkCertificate.Tests.ps1 index dadb32e2d32..6ca90ed6628 100644 --- a/tests/Set-DbaNetworkCertificate.Tests.ps1 +++ b/tests/Set-DbaNetworkCertificate.Tests.ps1 @@ -65,6 +65,32 @@ Describe $CommandName -Tag IntegrationTests { $WarnVar | Should -Match "No suitable certificate found" } + It "Says why a certificate with a CNG key is unsuitable" { + # New-SelfSignedCertificate creates a Key Storage Provider (CNG) key by default. SQL Server cannot use + # such a key, and the refusal has to name that reason instead of a bare PrivateKeyInvalid. + $newCngCertificate = { + param ($DnsName) + $splatCertificate = @{ + DnsName = $DnsName + CertStoreLocation = "Cert:\LocalMachine\My" + FriendlyName = "dbatoolsci_cng_key" + } + (New-SelfSignedCertificate @splatCertificate).Thumbprint + } + $splatCreateCng = @{ + ComputerName = $computerName + ScriptBlock = $newCngCertificate + ArgumentList = $computerName + } + $cngThumbprint = Invoke-Command2 @splatCreateCng + $script:createdNetworkCertificateThumbprints += $cngThumbprint + + $result = Set-DbaNetworkCertificate -SqlInstance $TestConfig.InstanceRestart -Thumbprint $cngThumbprint -WarningAction SilentlyContinue + $result | Should -BeNullOrEmpty + $WarnVar | Should -Match "PrivateKeyInvalid" + $WarnVar | Should -Match "legacy CSP key with KeySpec AT_KEYEXCHANGE" + } + It "applies an unsuitable certificate when Force is used" { $splatNewUnsuitableCertificate = @{ ComputerName = $computerName From 8ec76632237344767cc02270108a00dd3d643abd Mon Sep 17 00:00:00 2001 From: Andreas Jordan Date: Tue, 1 Sep 2026 09:42:36 +0200 Subject: [PATCH 2/2] Set-DbaNetworkCertificate - Take the thumbprint raw from the remote scriptblock in the test Invoke-Command2 wraps plain output in a PSCustomObject unless -Raw is used, so the CNG test handed the command an object with a Length property instead of the thumbprint and failed on the thumbprint format check. (do *NetworkCertificate*) --- tests/Set-DbaNetworkCertificate.Tests.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Set-DbaNetworkCertificate.Tests.ps1 b/tests/Set-DbaNetworkCertificate.Tests.ps1 index 6ca90ed6628..58e937a2998 100644 --- a/tests/Set-DbaNetworkCertificate.Tests.ps1 +++ b/tests/Set-DbaNetworkCertificate.Tests.ps1 @@ -81,6 +81,8 @@ Describe $CommandName -Tag IntegrationTests { ComputerName = $computerName ScriptBlock = $newCngCertificate ArgumentList = $computerName + # Raw, because Invoke-Command2 otherwise wraps the string in an object that only has a Length. + Raw = $true } $cngThumbprint = Invoke-Command2 @splatCreateCng $script:createdNetworkCertificateThumbprints += $cngThumbprint