Public/Test-RsCertBinding.ps1
|
# ============================================================================= # Script : Public/Test-RsCertBinding.ps1 # Author : Keith Ramsey # Created : 2026-06-08 # ============================================================================= # Change Log # ----------------------------------------------------------------------------- # 2026-06-08 Keith Ramsey Initial: Phase 5 RS cert binding verification. # Calls Get-SqlRsInstance (private helper), reads # bound thumbprint via ListSSLCertificateBindings WMI # method (DR-013), looks up cert expiry in # Cert:\LocalMachine\My. Never throws (DR-005). # Standalone function per DR-015. # 2026-06-08 Keith Ramsey Route Invoke-CimMethod dispatch through private # Invoke-SqlRsCimBinding wrapper for Pester mockability # (Invoke-CimMethod -InputObject enforces [CimInstance] # binding before mock intercept). # ============================================================================= # Decision Contract: # - Never throws (DR-005). All errors caught and returned as Failed. # - No hostnames, IPs, service accounts in source (air-gap discipline). # - WMI API is ListSSLCertificateBindings on MSReportServer_ConfigurationSetting # (DR-013). Property read is filtered by -Application to match only the # requested RS endpoint. # - dbatools must not be re-introduced (commit c2b23a4). # - Standalone: no change to Install-SqlConnectionCertificate (DR-015). # ============================================================================= function Test-RsCertBinding { <# .SYNOPSIS Reports the TLS certificate binding for a Reporting Services endpoint. .DESCRIPTION Calls Get-SqlRsInstance to resolve the MSReportServer_ConfigurationSetting WMI object, then invokes ListSSLCertificateBindings to read the bound certificate thumbprint for the specified RS application endpoint. Looks up the certificate in Cert:\LocalMachine\My, checks its expiry, and returns a SqlCert.Result. Returns Status=Success with Thumbprint=null and an informational Detail when RS has no TLS certificate bound (HTTP-only mode). Returns Status=Failed if the RS instance cannot be found, if the bound certificate has expired, or if any WMI call fails. Use -Credential to supply alternate credentials for the WMI connection when the calling account lacks RS admin rights on the target machine. Never throws. .PARAMETER RsInstance Reporting Services instance name. Use 'MSSQLSERVER' for the default SSRS 2016 instance, 'SSRS' for SSRS 2017+ standalone, or 'PBIRS' for Power BI Report Server. Defaults to 'MSSQLSERVER'. .PARAMETER Application RS application endpoint to inspect. Must be one of: ReportServerWebService -- the Report Server web service (default) ReportServerWebApp -- the web portal These are the application name strings used by CreateSSLCertificateBinding and ListSSLCertificateBindings (DR-013; SSRS 2016+ naming convention). .PARAMETER ComputerName Target machine. Defaults to the local computer. Used by Get-SqlRsInstance to construct the CIM connection. Air-gap rule: pass the value; never hard-code a hostname. .PARAMETER Credential Credential for the CIM/WMI session when the calling account does not have RS admin rights on the target machine. .OUTPUTS SqlCert.Result Data keys: RsInstance, Application, Thumbprint, ExpiresOn, IsExpired. .EXAMPLE Test-RsCertBinding # Checks the default SSRS instance (MSSQLSERVER), ReportServerWebService # application, on the local machine. .EXAMPLE Test-RsCertBinding -RsInstance 'PBIRS' -Application 'ReportServerWebApp' # Checks the web portal endpoint on a PBIRS installation. .EXAMPLE Test-RsCertBinding -RsInstance 'SSRS' -ComputerName $node -Credential $cred # Checks a remote SSRS 2017+ node with explicit credentials. .NOTES Steps: 1. Call Get-SqlRsInstance to resolve the RS WMI namespace and config object. 2. If RS not found, return Failed. 3. Read bound cert thumbprint from the WMI config object via ListSSLCertificateBindings; filter results by Application name. 4. If no thumbprint (HTTP only): return Success with Thumbprint=null and informational Detail. 5. Look up cert in Cert:\LocalMachine\My by thumbprint; check NotAfter. 6. Build per-binding data row: RsInstance, Application, Thumbprint, ExpiresOn, IsExpired. 7. If IsExpired=true, return Failed with expiry detail. 8. Else return Success. #> [CmdletBinding()] [OutputType('SqlCert.Result')] param( [string] $RsInstance = 'MSSQLSERVER', [ValidateSet('ReportServerWebService', 'ReportServerWebApp')] [string] $Application = 'ReportServerWebService', [string] $ComputerName = $env:COMPUTERNAME, [PSCredential] $Credential ) try { # ------------------------------------------------------------------ # Step 1 — Resolve the RS WMI config object via the private helper. # ------------------------------------------------------------------ $getRsArgs = @{ InstanceName = $RsInstance ComputerName = $ComputerName } if ($Credential) { $getRsArgs['Credential'] = $Credential } $configObj = Get-SqlRsInstance @getRsArgs # ------------------------------------------------------------------ # Step 2 — Return Failed if the RS instance was not found. # ------------------------------------------------------------------ if (-not $configObj) { return New-SqlCertResult -Stage Verify -Status Failed ` -Detail "RS instance '$RsInstance' not found on '$ComputerName'. Verify that Reporting Services is installed and the instance name is correct." ` -Data $null } # ------------------------------------------------------------------ # Step 3 — Call ListSSLCertificateBindings to read the bound cert # thumbprint for the requested Application endpoint (DR-013). # The method returns parallel arrays: Application[], CertificateHash[], # IPAddress[], Port[], HRESULT. # ------------------------------------------------------------------ $thumbprint = $null try { $bindResult = Invoke-SqlRsCimBinding -ConfigObject $configObj if ($bindResult.HRESULT -eq 0 -and $bindResult.Application) { $apps = @($bindResult.Application) $hashes = @($bindResult.CertificateHash) for ($i = 0; $i -lt $apps.Count; $i++) { if ($apps[$i] -ieq $Application) { $thumbprint = $hashes[$i] break } } } } catch { return New-SqlCertResult -Stage Verify -Status Failed ` -Detail "ListSSLCertificateBindings WMI call failed for RS instance '$RsInstance' on '$ComputerName': $($_.Exception.Message)" ` -Data $null } # ------------------------------------------------------------------ # Step 4 — No thumbprint: RS is running HTTP-only. This is not an # error condition; return Success with an informational Detail. # ------------------------------------------------------------------ if ([string]::IsNullOrEmpty($thumbprint)) { $data = [pscustomobject]@{ RsInstance = $RsInstance Application = $Application Thumbprint = $null ExpiresOn = $null IsExpired = $false } return New-SqlCertResult -Stage Verify -Status Success ` -Detail "No certificate is bound to RS instance '$RsInstance' application '$Application'. RS is using HTTP only." ` -Data $data } # ------------------------------------------------------------------ # Step 5 — Look up the bound cert in Cert:\LocalMachine\My on the # TARGET (that is the store RS binds from), and check NotAfter # vs current time. When the target cannot be read over # remoting the expiry is reported as unknown ($null) rather # than guessed from the caller's own store. # ------------------------------------------------------------------ $certArgs = @{ Thumbprint = $thumbprint; ComputerName = $ComputerName } if ($Credential) { $certArgs['Credential'] = $Credential } $certLookup = Get-SqlCertFromTargetStore @certArgs $expiresOn = if ($certLookup.Checked -and $certLookup.Found) { $certLookup.NotAfter } else { $null } $isExpired = [bool]($null -ne $expiresOn -and $expiresOn -lt [datetime]::Now) # ------------------------------------------------------------------ # Step 6 — Build the data row. # ------------------------------------------------------------------ $data = [pscustomobject]@{ RsInstance = $RsInstance Application = $Application Thumbprint = $thumbprint ExpiresOn = $expiresOn IsExpired = $isExpired } # ------------------------------------------------------------------ # Step 7 — Return Failed if the bound certificate is expired. # ------------------------------------------------------------------ if ($isExpired) { $expDetail = if ($expiresOn) { $expiresOn.ToString('yyyy-MM-dd') } else { 'unknown expiry' } return New-SqlCertResult -Stage Verify -Status Failed ` -Detail "Bound RS certificate is expired ($expDetail) -- RS will reject HTTPS connections for '$Application' on instance '$RsInstance'. Renew or replace the certificate and re-run Set-RsCertBinding." ` -Data $data } # ------------------------------------------------------------------ # Step 8 — Return Success. # ------------------------------------------------------------------ New-SqlCertResult -Stage Verify -Status Success ` -Detail "RS instance '$RsInstance' application '$Application' has a valid TLS certificate bound (thumbprint: $thumbprint)." ` -Data $data } catch { New-SqlCertResult -Stage Verify -Status Failed ` -Detail "Test-RsCertBinding encountered an unexpected error: $($_.Exception.Message)" ` -Data $null } } |