Skip to content

Commit 0930946

Browse files
authored
Implement DNS SAN validation for certificate checks
Added validation for the first DNS Subject Alternative Name (SAN) entry to match the machine's FQDN. Updated documentation and improved error handling for certificate checks.
1 parent 3494535 commit 0930946

File tree

1 file changed

+103
-12
lines changed

1 file changed

+103
-12
lines changed

Powershell/Test-SCOMCertificate.ps1

Lines changed: 103 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
Check all certificates on the local machine:
2929
PS C:\> .\Test-SCOMCertificates.ps1 -All
3030
.NOTES
31+
Update 02/2026 (Blake Drumm, https://blakedrumm.com/)
32+
Added SAN-first-entry validation: if the certificate contains DNS Subject Alternative Names,
33+
the FIRST DNS SAN entry must match the machine FQDN. This prevents cert auth failures where
34+
SAN[0] does not match the expected FQDN.
3135
Update 05/2024 (Blake Drumm, https://blakedrumm.com/)
3236
Updated the way the subject name is parsed against the DNS resolved name of the machine.
3337
Update 03/2024 (Blake Drumm, https://blakedrumm.com/)
@@ -147,6 +151,51 @@ begin
147151
$TimeStamp = Get-Date -Format "MM/dd/yyyy hh:mm:ss tt"
148152
return $TimeStamp
149153
}
154+
155+
# Helper: Extract DNS SANs in order, then return the first DNS entry (if any)
156+
function Get-FirstDnsSanEntry
157+
{
158+
param(
159+
[Parameter(Mandatory = $true)]
160+
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
161+
)
162+
163+
try
164+
{
165+
$sanExt = $Certificate.Extensions | Where-Object { $_.Oid -and $_.Oid.Value -eq '2.5.29.17' } | Select-Object -First 1
166+
if ($null -eq $sanExt) { return $null }
167+
168+
# .Format($false) preserves the extension display order from the certificate
169+
$sanText = $sanExt.Format($false)
170+
if ([string]::IsNullOrWhiteSpace($sanText)) { return $null }
171+
172+
# Split on commas and newlines while maintaining order
173+
$parts = $sanText -split "(\r\n|\n|,)"
174+
foreach ($p in $parts)
175+
{
176+
$t = ($p -as [string]).Trim()
177+
if ([string]::IsNullOrWhiteSpace($t)) { continue }
178+
179+
# Common formats: "DNS Name=host.contoso.com" or sometimes "DNS=host.contoso.com"
180+
if ($t -match '^(DNS Name|DNS)\s*=\s*(.+)$')
181+
{
182+
$dns = $Matches[2].Trim()
183+
if ($dns.EndsWith('.')) { $dns = $dns.TrimEnd('.') }
184+
if (-not [string]::IsNullOrWhiteSpace($dns))
185+
{
186+
return $dns
187+
}
188+
}
189+
}
190+
191+
return $null
192+
}
193+
catch
194+
{
195+
return $null
196+
}
197+
}
198+
150199
$out = @()
151200
$out += "`n" + @"
152201
$(Invoke-TimeStamp) : Starting Script
@@ -256,11 +305,8 @@ $(Invoke-TimeStamp) : Starting Script
256305
$Subject = $Certificate.Subject
257306
#Build chain
258307
$chain.Build($Certificate)
259-
# List the chain elements
260-
# Write-Host $chain.ChainElements.Certificate.IssuerName.Name
261308
# List the chain elements verbose
262309
$ChainCerts = ($chain.ChainElements).certificate | select Subject, SerialNumber
263-
#$ChainCerts
264310
$chainCertFormatter = New-Object System.Text.StringBuilder
265311
foreach ($C1 IN $ChainCerts)
266312
{
@@ -270,9 +316,7 @@ $(Invoke-TimeStamp) : Starting Script
270316
$chainCertFormatter.AppendLine("($($C1.serialnumber))") | Out-Null
271317
}
272318
$ChainCertsOutput = $chainCertFormatter.ToString()
273-
#write-host $ChainCertsOutput
274-
# ^^ needs to be justified. I suspect creating an object array and then exporting that to a string may
275-
# keep the justification and still allow it to be displayed.
319+
276320
$text4 = @"
277321
=====================================================================================================================
278322
$(if (!$SerialNumber -and $All) { "($x`/$($certs.Count)) " })Examining Certificate
@@ -287,19 +331,25 @@ $($ChainCertsOutput)
287331
Write-Host $text4
288332
$out += "`n" + "`n" + $text4
289333
$pass = $true
334+
290335
# Check subjectname
291-
$fqdn = (Resolve-DnsName $env:COMPUTERNAME -Type A | Select-Object -ExpandProperty Name -Unique) -join " "
336+
$fqdnList = @((Resolve-DnsName $env:COMPUTERNAME -Type A | Select-Object -ExpandProperty Name -Unique))
337+
$fqdn = ($fqdnList) -join " "
338+
$fqdnPrimary = $null
339+
if ($fqdnList -and $fqdnList.Count -gt 0) { $fqdnPrimary = ($fqdnList | Select-Object -First 1) }
340+
if ([string]::IsNullOrWhiteSpace($fqdnPrimary)) { $fqdnPrimary = $fqdn }
341+
292342
trap [DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException]
293343
{
294344
# Not part of a domain
295345
continue;
296346
}
297-
347+
298348
$subjectProblem = $false
299349
$fqdnRegexPattern = "CN=" + ($fqdn.Replace(".", "\.")).Replace(" ", "|CN=")
300350
try { $CheckForDuplicateSubjectCNs = ((($cert).Subject).Split(",") | %{ $_.Trim() } | Where { $_ -match "CN=" }).Trim("CN=") | % { $_.Split(".") | Select-Object -First 1 } | Group-Object | Where-Object { $_.Count -gt 1 } | Select -ExpandProperty Name }
301351
catch { $CheckForDuplicateSubjectCNs = $null }
302-
352+
303353
if (-NOT $cert.Subject)
304354
{
305355
$text5 = "Certificate Subject Common Name Missing"
@@ -350,6 +400,39 @@ $($ChainCertsOutput)
350400
$pass = $true;
351401
$text7 = "Certificate Subjectname is Good"; $out += "`n" + $text7; Write-Host $text7 -BackgroundColor Green -ForegroundColor Black
352402
}
403+
404+
# NEW: Validate first DNS SAN entry matches the machine FQDN
405+
# If the certificate has DNS SANs, SCOM/cert auth can fail when the first SAN does not match the expected FQDN.
406+
$sanProblem = $false
407+
$firstDnsSan = Get-FirstDnsSanEntry -Certificate $cert
408+
if ($firstDnsSan)
409+
{
410+
if ($firstDnsSan.ToUpper() -ne $fqdnPrimary.ToUpper())
411+
{
412+
$textSAN1 = "Certificate SAN First DNS Entry Mismatch"
413+
$out += "`n" + $textSAN1
414+
Write-Host $textSAN1 -BackgroundColor Red -ForegroundColor Black
415+
416+
$textSAN2 = @"
417+
The first DNS Subject Alternative Name (SAN) entry does not match the expected FQDN.
418+
This can cause certificate authentication failures if SCOM uses SAN[0] during validation.
419+
First DNS SAN entry: $firstDnsSan
420+
Expected FQDN: $fqdnPrimary
421+
"@
422+
$out += "`n" + $textSAN2
423+
Write-Host $textSAN2
424+
$pass = $false
425+
$sanProblem = $true
426+
}
427+
else
428+
{
429+
$textSAN3 = "Certificate SAN first DNS entry is Good"
430+
$out += "`n" + $textSAN3
431+
Write-Host $textSAN3 -BackgroundColor Green -ForegroundColor Black
432+
}
433+
}
434+
# If no DNS SAN is present, do not fail (existing CN validation remains the primary check).
435+
353436
# Verify private key
354437
if (!($cert.HasPrivateKey))
355438
{
@@ -382,6 +465,7 @@ $($ChainCertsOutput)
382465
$pass = $false
383466
}
384467
else { $text12 = "Private Key is Good"; $out += "`n" + $text12; Write-Host $text12 -BackgroundColor Green -ForegroundColor Black }
468+
385469
# Check expiration dates
386470
if (($cert.NotBefore -gt [DateTime]::Now) -or ($cert.NotAfter -lt [DateTime]::Now))
387471
{
@@ -405,6 +489,7 @@ Expiration
405489
$out += "`n" + $text15
406490
Write-Host $text15 -BackgroundColor Green -ForegroundColor Black
407491
}
492+
408493
# Enhanced key usage extension
409494
$enhancedKeyUsageExtension = $cert.Extensions | Where-Object { $_.ToString() -match "X509EnhancedKeyUsageExtension" }
410495
if ($null -eq $enhancedKeyUsageExtension)
@@ -462,6 +547,7 @@ Enhanced Key Usage Extension is Good
462547
}
463548
}
464549
}
550+
465551
# KeyUsage extension
466552
$keyUsageExtension = $cert.Extensions | Where-Object { $_.ToString() -match "X509KeyUsageExtension" }
467553
if ($null -eq $keyUsageExtension)
@@ -516,6 +602,7 @@ Enhanced Key Usage Extension is Good
516602
else { $text30 = "Key Usage Extensions are Good"; $out += "`n" + $text30; Write-Host $text30 -BackgroundColor Green -ForegroundColor Black }
517603
}
518604
}
605+
519606
# KeySpec
520607
$keySpec = $cert.PrivateKey.CspKeyContainerInfo.KeyNumber
521608
if ($null -eq $keySpec)
@@ -543,6 +630,7 @@ Enhanced Key Usage Extension is Good
543630
$pass = $false
544631
}
545632
else { $text35 = "KeySpec is Good"; $out += "`n" + $text35; Write-Host $text35 -BackgroundColor Green -ForegroundColor Black }
633+
546634
# Check that serial is written to proper reg
547635
$certSerial = $cert.SerialNumber
548636
$certSerialReversed = [System.String]("")
@@ -598,7 +686,8 @@ Enhanced Key Usage Extension is Good
598686
else { $text42 = "Serial Number is written to the registry"; $out += "`n" + $text42; Write-Host $text42 -BackgroundColor Green -ForegroundColor Black }
599687
}
600688
}
601-
#Check that the cert's issuing CA is trusted (This is not technically required as it is the remote machine cert's CA that must be trusted. Most users leverage the same CA for all machines, though, so it's worth checking
689+
690+
#Check that the cert's issuing CA is trusted
602691
$chain = new-object Security.Cryptography.X509Certificates.X509Chain
603692
$chain.ChainPolicy.RevocationMode = 0
604693
if ($chain.Build($cert) -eq $false)
@@ -654,6 +743,7 @@ Enhanced Key Usage Extension is Good
654743
Write-Host $text48
655744
}
656745
}
746+
657747
if ($pass)
658748
{
659749
$text49 = "`n*** This certificate is properly configured and imported for System Center Operations Manager ***"; $out += "`n" + $text49; Write-Host $text49 -ForegroundColor Green
@@ -664,6 +754,7 @@ Enhanced Key Usage Extension is Good
664754
}
665755
$out += "`n" + " " # This is so there is white space between each Cert. Makes it less of a jumbled mess.
666756
}
757+
667758
if ($certs.Count -eq $NotPresentCount)
668759
{
669760
$text49 = " Unable to locate any certificates on this server that match the criteria specified OR the serial number in the registry does not match any certificates present."; $out += "`n" + $text49; Write-Host $text49 -ForegroundColor Red
@@ -754,15 +845,15 @@ Certificate Checker
754845
continue
755846
}
756847
#endregion Function
757-
848+
758849
#region DefaultActions
759850
if ($Servers -or $OutputFile -or $All -or $SerialNumber)
760851
{
761852
Test-SCOMCertificate -Servers $Servers -OutputFile $OutputFile -All:$All -SerialNumber:$SerialNumber
762853
}
763854
else
764855
{
765-
# Modify line 772 if you want to change the default behavior when running this script through Powershell ISE
856+
# Modify line 863 if you want to change the default behavior when running this script through Powershell ISE
766857
#
767858
# Examples:
768859
# Test-SCOMCertificate -SerialNumber 1f00000008c694dac94bcfdc4a000000000008

0 commit comments

Comments
 (0)