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