<# MIT License Copyright (c) Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE #> # Version 23.08.10.1807 <# .NOTES Name: MonitorExchangeAuthCertificate.ps1 Requires: Exchange Management Shell and Organization Management permissions. Major Release History: 01/10/2023 - Initial Public Release on CSS-Exchange .SYNOPSIS Validates the Auth Certificate configuration of the Exchange organization where the script runs. It can be run in mode to automatically replace an invalid Auth Certificate or prepare a new next Auth Certificate to ensure a smooth Auth Certificate rollover. .DESCRIPTION This script checks the status of the Auth Certificate which is set to the Auth Configuration of the Exchange organization. If the script is executed without any parameter, it will only perform tests if any Auth Certificate renewal action is required. The script can be executed in action mode which will then perform the appropriate Auth Certificate renewal actions (if required). The script can also be configured to run via Scheduled Task on a daily base. It will then perform the required renewal actions without admin interaction needed (except in Exchange Hybrid scenarios where a run of the Hybrid Configuration Wizard or HCW is required after a new Auth Certificate becomes active). .PARAMETER ValidateAndRenewAuthCertificate You can use this parameter to let the script perform the required Auth Certificate renewal actions. If the script runs with this parameter set to $false, no action will be made to the current Auth Configuration. .PARAMETER IgnoreUnreachableServers This optional parameter can be used to ignore if some of the Exchange servers within the organization cannot be reached. If this parameter is used, the script only validates the servers that can be reached and will perform Auth Certificate renewal actions based on the result. .PARAMETER IgnoreHybridConfig This optional parameter allows you to explicitly perform Auth Certificate renewal actions (if required) even if an Exchange hybrid configuration was detected. You need to run the Hybrid Configuration Wizard (HCW) after the renewed Auth Certificate becomes the one in use. .PARAMETER PrepareADForAutomationOnly This optional parameter can be used in AD Split Permission scenarios. It allows you to create the AD account which can then be used to run the Exchange Auth Certificate Monitoring script automatically via Scheduled Task. .PARAMETER ADAccountDomain This optional parameter allows you to specify the domain which is then used by the script to generate the AD account used for automation. .PARAMETER ConfigureScriptToRunViaScheduledTask This optional parameter can be used to automatically prepare the requirements in AD (user account), Exchange (email enable the account, hide the account from address book, create a new role group with limited permissions) and finally it creates the scheduled task on the computer on which the script was executed (it has to be an Exchange server running the mailbox role). .PARAMETER AutomationAccountCredential This optional parameter can be used to provide a different user under whose context the script is then executed via scheduled task. .PARAMETER SendEmailNotificationTo This optional parameter can be used to specify recipients which will then be notified in case that an Exchange Auth Certificate renewal action was performed. .PARAMETER TrustAllCertificates This optional parameter can be used to trust all certificates when connecting to the EWS service to send out email notifications. .PARAMETER TestEmailNotification This optional parameter can be used to test the email notification feature of the script. .PARAMETER Password Parameter to provide a password to the script which is required in some scenarios. This parameter is required if you use one of the following parameters: - If you use the PrepareADForAutomationOnly parameter - If you use the ExportAuthCertificatesAsPfx parameter It is an optional parameter if you use the ConfigureScriptToRunViaScheduledTask parameter. .PARAMETER ExportAuthCertificatesAsPfx This optional parameter can be used to export all on the system available Auth Certificates as password protected .pfx file. .PARAMETER ScriptUpdateOnly This optional parameter allows you to only update the script without performing any other actions. .PARAMETER SkipVersionCheck This optional parameter allows you to skip the automatic version check and script update. .EXAMPLE .\MonitorExchangeAuthCertificate.ps1 Runs the script in validation mode and will show you the Auth Certificate renewal action which will be performed when executed in renew mode. .EXAMPLE .\MonitorExchangeAuthCertificate.ps1 -ValidateAndRenewAuthCertificate $true -Confirm:$false Runs the script in renewal mode without user interaction. The Auth Certificate renewal action will be performed (if required). In unattended mode the internal SMTP certificate will be replaced with the new Auth Certificate and is then set back to the previous one. .EXAMPLE .\MonitorExchangeAuthCertificate.ps1 -ValidateAndRenewAuthCertificate $true -IgnoreUnreachableServers $true -Confirm:$false Runs the script in renewal mode without user interaction. We only take the Exchange server into account which are reachable and will perform the renewal action if required. .EXAMPLE .\MonitorExchangeAuthCertificate.ps1 -ValidateAndRenewAuthCertificate $true -IgnoreHybridConfig $true -Confirm:$false Runs the script in renewal mode without user interaction. The renewal action will be performed even if a Exchange hybrid configuration was detected. Please note that you have to run the Hybrid Configuration Wizard (HCW) after the active Auth Certificate was replaced. .EXAMPLE .\MonitorExchangeAuthCertificate.ps1 -ConfigureScriptToRunViaScheduledTask -Password (Get-Credential).Password If you run the script using this parameter, the script will then create a new AD user which is then assigned to a newly created Exchange Role Group. The script will also create a scheduled task that runs on a hourly base. The '-ConfigureScriptToRunViaScheduledTask' parameter can be combined with the '-IgnoreHybridConfig $true' and '-IgnoreUnreachableServers $true' parameter. #> [CmdletBinding(DefaultParameterSetName = "MonitorExchangeAuthCertificateManually", SupportsShouldProcess = $true, ConfirmImpact = "High")] param( [Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")] [bool]$ValidateAndRenewAuthCertificate = $false, [Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")] [Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")] [bool]$IgnoreUnreachableServers = $false, [Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")] [Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")] [bool]$IgnoreHybridConfig = $false, [Parameter(Mandatory = $false, ParameterSetName = "SetupAutomaticExecutionADRequirements")] [switch]$PrepareADForAutomationOnly, [Parameter(Mandatory = $false, ParameterSetName = "SetupAutomaticExecutionADRequirements")] [string]$ADAccountDomain = $env:USERDNSDOMAIN, [Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")] [switch]$ConfigureScriptToRunViaScheduledTask, [Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")] [PSCredential]$AutomationAccountCredential, [Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")] [Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")] [Parameter(Mandatory = $true, ParameterSetName = "TestEmailNotification")] [ValidatePattern("^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")] [string[]]$SendEmailNotificationTo, [Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")] [Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")] [Parameter(Mandatory = $false, ParameterSetName = "TestEmailNotification")] [switch]$TrustAllCertificates, [Parameter(Mandatory = $false, ParameterSetName = "TestEmailNotification")] [switch]$TestEmailNotification, [Parameter(Mandatory = $true, ParameterSetName = "SetupAutomaticExecutionADRequirements")] [Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")] [Parameter(Mandatory = $true, ParameterSetName = "ExportExchangeAuthCertificatesAsPfx")] [SecureString]$Password, [Parameter(Mandatory = $false, ParameterSetName = "ExportExchangeAuthCertificatesAsPfx")] [switch]$ExportAuthCertificatesAsPfx, [Parameter(Mandatory = $false, ParameterSetName = "ScriptUpdateOnly")] [switch]$ScriptUpdateOnly, [Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")] [Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")] [Parameter(Mandatory = $false, ParameterSetName = "SetupAutomaticExecutionADRequirements")] [switch]$SkipVersionCheck ) $BuildVersion = "23.08.10.1807" function Confirm-Administrator { $currentPrincipal = New-Object Security.Principal.WindowsPrincipal( [Security.Principal.WindowsIdentity]::GetCurrent() ) return $currentPrincipal.IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator ) } function Invoke-CatchActionError { [CmdletBinding()] param( [ScriptBlock]$CatchActionFunction ) if ($null -ne $CatchActionFunction) { & $CatchActionFunction } } function Invoke-CatchActionErrorLoop { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [int]$CurrentErrors, [Parameter(Mandatory = $false, Position = 1)] [ScriptBlock]$CatchActionFunction ) process { if ($null -ne $CatchActionFunction -and $Error.Count -ne $CurrentErrors) { $i = 0 while ($i -lt ($Error.Count - $currentErrors)) { & $CatchActionFunction $Error[$i] $i++ } } } } # Confirm that either Remote Shell or EMS is loaded from an Edge Server, Exchange Server, or a Tools box. # It does this by also initializing the session and running Get-EventLogLevel. (Server Management RBAC right) # All script that require Confirm-ExchangeShell should be at least using Server Management RBAC right for the user running the script. function Confirm-ExchangeShell { [CmdletBinding()] param( [Parameter(Mandatory = $false)] [bool]$LoadExchangeShell = $true, [Parameter(Mandatory = $false)] [ScriptBlock]$CatchActionFunction ) begin { Write-Verbose "Calling: $($MyInvocation.MyCommand)" Write-Verbose "Passed: LoadExchangeShell: $LoadExchangeShell" $currentErrors = $Error.Count $edgeTransportKey = 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\EdgeTransportRole' $setupKey = 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup' $remoteShell = (-not(Test-Path $setupKey)) $toolsServer = (Test-Path $setupKey) -and (-not(Test-Path $edgeTransportKey)) -and ($null -eq (Get-ItemProperty -Path $setupKey -Name "Services" -ErrorAction SilentlyContinue)) Invoke-CatchActionErrorLoop $currentErrors $CatchActionFunction function IsExchangeManagementSession { [OutputType("System.Boolean")] param( [ScriptBlock]$CatchActionFunction ) $getEventLogLevelCallSuccessful = $false $isExchangeManagementShell = $false try { $currentErrors = $Error.Count $attempts = 0 do { $eventLogLevel = Get-EventLogLevel -ErrorAction Stop | Select-Object -First 1 $attempts++ if ($attempts -ge 5) { throw "Failed to run Get-EventLogLevel too many times." } } while ($null -eq $eventLogLevel) $getEventLogLevelCallSuccessful = $true foreach ($e in $eventLogLevel) { Write-Verbose "Type is: $($e.GetType().Name) BaseType is: $($e.GetType().BaseType)" if (($e.GetType().Name -eq "EventCategoryObject") -or (($e.GetType().Name -eq "PSObject") -and ($null -ne $e.SerializationData))) { $isExchangeManagementShell = $true } } Invoke-CatchActionErrorLoop $currentErrors $CatchActionFunction } catch { Write-Verbose "Failed to run Get-EventLogLevel" Invoke-CatchActionError $CatchActionFunction } return [PSCustomObject]@{ CallWasSuccessful = $getEventLogLevelCallSuccessful IsManagementShell = $isExchangeManagementShell } } } process { $isEMS = IsExchangeManagementSession $CatchActionFunction if ($isEMS.CallWasSuccessful) { Write-Verbose "Exchange PowerShell Module already loaded." } else { if (-not ($LoadExchangeShell)) { return } #Test 32 bit process, as we can't see the registry if that is the case. if (-not ([System.Environment]::Is64BitProcess)) { Write-Warning "Open a 64 bit PowerShell process to continue" return } if (Test-Path "$setupKey") { Write-Verbose "We are on Exchange 2013 or newer" try { $currentErrors = $Error.Count if (Test-Path $edgeTransportKey) { Write-Verbose "We are on Exchange Edge Transport Server" [xml]$PSSnapIns = Get-Content -Path "$env:ExchangeInstallPath\Bin\exShell.psc1" -ErrorAction Stop foreach ($PSSnapIn in $PSSnapIns.PSConsoleFile.PSSnapIns.PSSnapIn) { Write-Verbose ("Trying to add PSSnapIn: {0}" -f $PSSnapIn.Name) Add-PSSnapin -Name $PSSnapIn.Name -ErrorAction Stop } Import-Module $env:ExchangeInstallPath\bin\Exchange.ps1 -ErrorAction Stop } else { Import-Module $env:ExchangeInstallPath\bin\RemoteExchange.ps1 -ErrorAction Stop Connect-ExchangeServer -Auto -ClientApplication:ManagementShell } Invoke-CatchActionErrorLoop $currentErrors $CatchActionFunction Write-Verbose "Imported Module. Trying Get-EventLogLevel Again" $isEMS = IsExchangeManagementSession $CatchActionFunction if (($isEMS.CallWasSuccessful) -and ($isEMS.IsManagementShell)) { Write-Verbose "Successfully loaded Exchange Management Shell" } else { Write-Warning "Something went wrong while loading the Exchange Management Shell" } } catch { Write-Warning "Failed to Load Exchange PowerShell Module..." Invoke-CatchActionError $CatchActionFunction } } else { Write-Verbose "Not on an Exchange or Tools server" } } } end { $returnObject = [PSCustomObject]@{ ShellLoaded = $isEMS.CallWasSuccessful Major = ((Get-ItemProperty -Path $setupKey -Name "MsiProductMajor" -ErrorAction SilentlyContinue).MsiProductMajor) Minor = ((Get-ItemProperty -Path $setupKey -Name "MsiProductMinor" -ErrorAction SilentlyContinue).MsiProductMinor) Build = ((Get-ItemProperty -Path $setupKey -Name "MsiBuildMajor" -ErrorAction SilentlyContinue).MsiBuildMajor) Revision = ((Get-ItemProperty -Path $setupKey -Name "MsiBuildMinor" -ErrorAction SilentlyContinue).MsiBuildMinor) EdgeServer = $isEMS.CallWasSuccessful -and (Test-Path $setupKey) -and (Test-Path $edgeTransportKey) ToolsOnly = $isEMS.CallWasSuccessful -and $toolsServer RemoteShell = $isEMS.CallWasSuccessful -and $remoteShell EMS = $isEMS.IsManagementShell } return $returnObject } } function WriteErrorInformationBase { [CmdletBinding()] param( [object]$CurrentError = $Error[0], [ValidateSet("Write-Host", "Write-Verbose")] [string]$Cmdlet ) if ($null -ne $CurrentError.OriginInfo) { & $Cmdlet "Error Origin Info: $($CurrentError.OriginInfo.ToString())" } & $Cmdlet "$($CurrentError.CategoryInfo.Activity) : $($CurrentError.ToString())" if ($null -ne $CurrentError.Exception -and $null -ne $CurrentError.Exception.StackTrace) { & $Cmdlet "Inner Exception: $($CurrentError.Exception.StackTrace)" } elseif ($null -ne $CurrentError.Exception) { & $Cmdlet "Inner Exception: $($CurrentError.Exception)" } if ($null -ne $CurrentError.InvocationInfo.PositionMessage) { & $Cmdlet "Position Message: $($CurrentError.InvocationInfo.PositionMessage)" } if ($null -ne $CurrentError.Exception.SerializedRemoteInvocationInfo.PositionMessage) { & $Cmdlet "Remote Position Message: $($CurrentError.Exception.SerializedRemoteInvocationInfo.PositionMessage)" } if ($null -ne $CurrentError.ScriptStackTrace) { & $Cmdlet "Script Stack: $($CurrentError.ScriptStackTrace)" } } function Write-VerboseErrorInformation { [CmdletBinding()] param( [object]$CurrentError = $Error[0] ) WriteErrorInformationBase $CurrentError "Write-Verbose" } function Write-HostErrorInformation { [CmdletBinding()] param( [object]$CurrentError = $Error[0] ) WriteErrorInformationBase $CurrentError "Write-Host" } function Invoke-CatchActions { [CmdletBinding()] param( [object]$CurrentError = $Error[0] ) Write-Verbose "Calling: $($MyInvocation.MyCommand)" $script:ErrorsExcluded += $CurrentError Write-Verbose "Error Excluded Count: $($Script:ErrorsExcluded.Count)" Write-Verbose "Error Count: $($Error.Count)" Write-VerboseErrorInformation $CurrentError } function Get-UnhandledErrors { [CmdletBinding()] param () $index = 0 return $Error | ForEach-Object { $currentError = $_ $handledError = $Script:ErrorsExcluded | Where-Object { $_.Equals($currentError) } if ($null -eq $handledError) { [PSCustomObject]@{ ErrorInformation = $currentError Index = $index } } $index++ } } function Get-HandledErrors { [CmdletBinding()] param () $index = 0 return $Error | ForEach-Object { $currentError = $_ $handledError = $Script:ErrorsExcluded | Where-Object { $_.Equals($currentError) } if ($null -ne $handledError) { [PSCustomObject]@{ ErrorInformation = $currentError Index = $index } } $index++ } } function Test-UnhandledErrorsOccurred { return $Error.Count -ne $Script:ErrorsExcluded.Count } function Invoke-ErrorCatchActionLoopFromIndex { [CmdletBinding()] param( [int]$StartIndex ) Write-Verbose "Calling: $($MyInvocation.MyCommand)" Write-Verbose "Start Index: $StartIndex Error Count: $($Error.Count)" if ($StartIndex -ne $Error.Count) { # Write the errors out in reverse in the order that they came in. $index = $Error.Count - $StartIndex - 1 do { Invoke-CatchActions $Error[$index] $index-- } while ($index -ge 0) } } function Invoke-ErrorMonitoring { # Always clear out the errors # setup variable to monitor errors that occurred $Error.Clear() $Script:ErrorsExcluded = @() } function Invoke-WriteDebugErrorsThatOccurred { function WriteErrorInformation { [CmdletBinding()] param( [object]$CurrentError ) Write-VerboseErrorInformation $CurrentError Write-Verbose "-----------------------------------`r`n`r`n" } if ($Error.Count -gt 0) { Write-Verbose "`r`n`r`nErrors that occurred that wasn't handled" Get-UnhandledErrors | ForEach-Object { Write-Verbose "Error Index: $($_.Index)" WriteErrorInformation $_.ErrorInformation } Write-Verbose "`r`n`r`nErrors that were handled" Get-HandledErrors | ForEach-Object { Write-Verbose "Error Index: $($_.Index)" WriteErrorInformation $_.ErrorInformation } } else { Write-Verbose "No errors occurred in the script." } } function Get-NewLoggerInstance { [CmdletBinding()] param( [string]$LogDirectory = (Get-Location).Path, [ValidateNotNullOrEmpty()] [string]$LogName = "Script_Logging", [bool]$AppendDateTime = $true, [bool]$AppendDateTimeToFileName = $true, [int]$MaxFileSizeMB = 10, [int]$CheckSizeIntervalMinutes = 10, [int]$NumberOfLogsToKeep = 10 ) $fileName = if ($AppendDateTimeToFileName) { "{0}_{1}.txt" -f $LogName, ((Get-Date).ToString('yyyyMMddHHmmss')) } else { "$LogName.txt" } $fullFilePath = [System.IO.Path]::Combine($LogDirectory, $fileName) if (-not (Test-Path $LogDirectory)) { try { New-Item -ItemType Directory -Path $LogDirectory -ErrorAction Stop | Out-Null } catch { throw "Failed to create Log Directory: $LogDirectory. Inner Exception: $_" } } return [PSCustomObject]@{ FullPath = $fullFilePath AppendDateTime = $AppendDateTime MaxFileSizeMB = $MaxFileSizeMB CheckSizeIntervalMinutes = $CheckSizeIntervalMinutes NumberOfLogsToKeep = $NumberOfLogsToKeep BaseInstanceFileName = $fileName.Replace(".txt", "") Instance = 1 NextFileCheckTime = ((Get-Date).AddMinutes($CheckSizeIntervalMinutes)) PreventLogCleanup = $false LoggerDisabled = $false } | Write-LoggerInstance -Object "Starting Logger Instance $(Get-Date)" } function Write-LoggerInstance { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object]$LoggerInstance, [Parameter(Mandatory = $true, Position = 1)] [object]$Object ) process { if ($LoggerInstance.LoggerDisabled) { return } if ($LoggerInstance.AppendDateTime -and $Object.GetType().Name -eq "string") { $Object = "[$([System.DateTime]::Now)] : $Object" } # Doing WhatIf:$false to support -WhatIf in main scripts but still log the information $Object | Out-File $LoggerInstance.FullPath -Append -WhatIf:$false #Upkeep of the logger information if ($LoggerInstance.NextFileCheckTime -gt [System.DateTime]::Now) { return } #Set next update time to avoid issues so we can log things $LoggerInstance.NextFileCheckTime = ([System.DateTime]::Now).AddMinutes($LoggerInstance.CheckSizeIntervalMinutes) $item = Get-ChildItem $LoggerInstance.FullPath if (($item.Length / 1MB) -gt $LoggerInstance.MaxFileSizeMB) { $LoggerInstance | Write-LoggerInstance -Object "Max file size reached rolling over" | Out-Null $directory = [System.IO.Path]::GetDirectoryName($LoggerInstance.FullPath) $fileName = "$($LoggerInstance.BaseInstanceFileName)-$($LoggerInstance.Instance).txt" $LoggerInstance.Instance++ $LoggerInstance.FullPath = [System.IO.Path]::Combine($directory, $fileName) $items = Get-ChildItem -Path ([System.IO.Path]::GetDirectoryName($LoggerInstance.FullPath)) -Filter "*$($LoggerInstance.BaseInstanceFileName)*" if ($items.Count -gt $LoggerInstance.NumberOfLogsToKeep) { $item = $items | Sort-Object LastWriteTime | Select-Object -First 1 $LoggerInstance | Write-LoggerInstance "Removing Log File $($item.FullName)" | Out-Null $item | Remove-Item -Force } } } end { return $LoggerInstance } } function Invoke-LoggerInstanceCleanup { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object]$LoggerInstance ) process { if ($LoggerInstance.LoggerDisabled -or $LoggerInstance.PreventLogCleanup) { return } Get-ChildItem -Path ([System.IO.Path]::GetDirectoryName($LoggerInstance.FullPath)) -Filter "*$($LoggerInstance.BaseInstanceFileName)*" | Remove-Item -Force } } function Get-GlobalCatalogServer { [CmdletBinding()] param( [Parameter(Mandatory = $false)] [string]$SiteName = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite().Name, [Parameter(Mandatory = $false)] [ScriptBlock]$CatchActionFunction ) <# This function returns a Global Catalog server for the Active Directory Site of the computer. #> try { Write-Verbose "Calling: $($MyInvocation.MyCommand)" Write-Verbose ("Trying to query a Global Catalog for the current forest for site: $($SiteName)") return ([System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Forest.FindGlobalCatalog($SiteName)).Name } catch { Write-Verbose ("Error while querying a Global Catalog for current forest - Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction return } } function Enable-TrustAnyCertificateCallback { param() <# This helper function can be used to ignore certificate errors. It works by setting the ServerCertificateValidationCallback to a callback that always returns true. This is useful when you are using self-signed certificates or certificates that are not trusted by the system. #> Add-Type -TypeDefinition @" namespace Microsoft.CSSExchange { public class CertificateValidator { public static bool TrustAnyCertificateCallback( object sender, System.Security.Cryptography.X509Certificates.X509Certificate cert, System.Security.Cryptography.X509Certificates.X509Chain chain, System.Net.Security.SslPolicyErrors sslPolicyErrors) { return true; } public static void IgnoreCertificateErrors() { System.Net.ServicePointManager.ServerCertificateValidationCallback = TrustAnyCertificateCallback; } } } "@ [Microsoft.CSSExchange.CertificateValidator]::IgnoreCertificateErrors() } function Send-EwsMailMessage { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory = $false)] [ValidatePattern("^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")] [string]$From = $null, [Parameter(Mandatory = $true)] [ValidatePattern("^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")] [string[]]$To, [Parameter(Mandatory = $false)] [ValidatePattern("^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")] [string[]]$Cc = $null, [Parameter(Mandatory = $false)] [ValidatePattern("^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")] [string[]]$Bcc = $null, [Parameter(Mandatory = $true)] [string]$Subject, [Parameter(Mandatory = $true)] [string]$Body, [Parameter(Mandatory = $false)] [switch]$BodyAsHtml, [Parameter(Mandatory = $false)] [ValidateSet("Low", "Normal", "High")] [string]$Importance = "Normal", [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential]$Credential, [Parameter(Mandatory = $false)] [string]$EwsManagedAPIAssemblyPath = "$($env:ExchangeInstallPath)bin\Microsoft.Exchange.WebServices.dll", [Parameter(Mandatory = $true)] [ValidatePattern("\/ews\/exchange.asmx$")] [string]$EwsServiceUrl, [Parameter(Mandatory = $false)] [switch]$IgnoreCertificateMismatch, [Parameter(Mandatory = $false)] [ScriptBlock]$CatchActionFunction ) begin { Write-Verbose "Calling: $($MyInvocation.MyCommand)" [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 if (Test-Path $EwsManagedAPIAssemblyPath) { Write-Verbose ("EWS Managed API Assembly was found under: $($EwsManagedAPIAssemblyPath)") Add-Type -Path $EwsManagedAPIAssemblyPath } else { Write-Verbose ("EWS Managed API Assembly was not found under: $($EwsManagedAPIAssemblyPath)") Write-Verbose ("Please download it from: 'https://aka.ms/ews-managed-api-readme' and provide the correct path") return $false } } process { if ($IgnoreCertificateMismatch) { Write-Verbose ("IgnoreCertificateMismatch was used - policy will be set to: TrustAnyCertificate") Enable-TrustAnyCertificateCallback } try { $ewsService = New-Object "Microsoft.Exchange.WebServices.Data.ExchangeService" -ArgumentList Exchange2013_SP1 $ewsService.Url = $EwsServiceUrl $ewsService.Credentials = New-Object "Microsoft.Exchange.WebServices.Data.WebCredentials" if ($null -ne $Credential) { Write-Verbose ("Credentials were provided - will try to use them") Write-Verbose ("Username: $($Credential.UserName)") $ewsService.UseDefaultCredentials = $false $ewsService.Credentials.Credentials.UserName = $Credential.UserName $ewsService.Credentials.Credentials.Password = $Credential.GetNetworkCredential().Password } else { Write-Verbose ("We will try to send the email from user: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)") $ewsService.UseDefaultCredentials = $true } $newMessage = New-Object "Microsoft.Exchange.WebServices.Data.EmailMessage" -ArgumentList $ewsService $newMessage.Subject = $Subject $newMessage.Importance = $Importance $newMessage.Body = $Body if (-not($BodyAsHtml)) { Write-Verbose ("Message will be send in plain text") $newMessage.Body.BodyType = "Text" } if ($null -ne $From) { Write-Verbose ("We will try to send the message by using the following 'From' address: $($From)") $newMessage.From = $From } foreach ($toRecipient in $To) { Write-Verbose ("Recipient: $($toRecipient) will be added to 'To' line") [void]$newMessage.ToRecipients.Add($toRecipient) } if ($null -ne $Cc) { foreach ($ccRecipient in $Cc) { Write-Verbose ("Recipient: $($ccRecipient) will be added to 'Cc' line") [void]$newMessage.CcRecipients.Add($ccRecipient) } } if ($null -ne $Bcc) { foreach ($bccRecipient in $Bcc) { Write-Verbose ("Recipient: $($bccRecipient) will be added to 'Bcc' line") [void]$newMessage.BccRecipients.Add($bccRecipient) } } } catch { Write-Verbose ("Something went wrong while preparing to send an email with the subject '$($newMessage.Subject)'") Invoke-CatchActionError $CatchActionFunction return $false } } end { try { $newMessage.SendAndSaveCopy() } catch { Write-Verbose ("Something went wrong while trying to send an email with the subject '$($newMessage.Subject)'") Invoke-CatchActionError $CatchActionFunction return $false } Write-Verbose ("An email with the subject '$($newMessage.Subject)' was sent and saved in the SendItems folder") return $true } } function Write-Host { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets', '', Justification = 'Proper handling of write host with colors')] [CmdletBinding()] param( [Parameter(Position = 1, ValueFromPipeline)] [object]$Object, [switch]$NoNewLine, [string]$ForegroundColor ) process { $consoleHost = $host.Name -eq "ConsoleHost" if ($null -ne $Script:WriteHostManipulateObjectAction) { $Object = & $Script:WriteHostManipulateObjectAction $Object } $params = @{ Object = $Object NoNewLine = $NoNewLine } if ([string]::IsNullOrEmpty($ForegroundColor)) { if ($null -ne $host.UI.RawUI.ForegroundColor -and $consoleHost) { $params.Add("ForegroundColor", $host.UI.RawUI.ForegroundColor) } } elseif ($ForegroundColor -eq "Yellow" -and $consoleHost -and $null -ne $host.PrivateData.WarningForegroundColor) { $params.Add("ForegroundColor", $host.PrivateData.WarningForegroundColor) } elseif ($ForegroundColor -eq "Red" -and $consoleHost -and $null -ne $host.PrivateData.ErrorForegroundColor) { $params.Add("ForegroundColor", $host.PrivateData.ErrorForegroundColor) } else { $params.Add("ForegroundColor", $ForegroundColor) } Microsoft.PowerShell.Utility\Write-Host @params if ($null -ne $Script:WriteHostDebugAction -and $null -ne $Object) { &$Script:WriteHostDebugAction $Object } } } function SetProperForegroundColor { $Script:OriginalConsoleForegroundColor = $host.UI.RawUI.ForegroundColor if ($Host.UI.RawUI.ForegroundColor -eq $Host.PrivateData.WarningForegroundColor) { Write-Verbose "Foreground Color matches warning's color" if ($Host.UI.RawUI.ForegroundColor -ne "Gray") { $Host.UI.RawUI.ForegroundColor = "Gray" } } if ($Host.UI.RawUI.ForegroundColor -eq $Host.PrivateData.ErrorForegroundColor) { Write-Verbose "Foreground Color matches error's color" if ($Host.UI.RawUI.ForegroundColor -ne "Gray") { $Host.UI.RawUI.ForegroundColor = "Gray" } } } function RevertProperForegroundColor { $Host.UI.RawUI.ForegroundColor = $Script:OriginalConsoleForegroundColor } function SetWriteHostAction ($DebugAction) { $Script:WriteHostDebugAction = $DebugAction } function SetWriteHostManipulateObjectAction ($ManipulateObject) { $Script:WriteHostManipulateObjectAction = $ManipulateObject } function Write-Verbose { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets', '', Justification = 'In order to log Write-Verbose from Shared functions')] [CmdletBinding()] param( [Parameter(Position = 1, ValueFromPipeline)] [string]$Message ) process { if ($null -ne $Script:WriteVerboseManipulateMessageAction) { $Message = & $Script:WriteVerboseManipulateMessageAction $Message } Microsoft.PowerShell.Utility\Write-Verbose $Message if ($null -ne $Script:WriteVerboseDebugAction) { & $Script:WriteVerboseDebugAction $Message } # $PSSenderInfo is set when in a remote context if ($PSSenderInfo -and $null -ne $Script:WriteRemoteVerboseDebugAction) { & $Script:WriteRemoteVerboseDebugAction $Message } } } function SetWriteVerboseAction ($DebugAction) { $Script:WriteVerboseDebugAction = $DebugAction } function SetWriteRemoteVerboseAction ($DebugAction) { $Script:WriteRemoteVerboseDebugAction = $DebugAction } function SetWriteVerboseManipulateMessageAction ($DebugAction) { $Script:WriteVerboseManipulateMessageAction = $DebugAction } function Confirm-ProxyServer { [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $true)] [string] $TargetUri ) Write-Verbose "Calling $($MyInvocation.MyCommand)" try { $proxyObject = ([System.Net.WebRequest]::GetSystemWebProxy()).GetProxy($TargetUri) if ($TargetUri -ne $proxyObject.OriginalString) { Write-Verbose "Proxy server configuration detected" Write-Verbose $proxyObject.OriginalString return $true } else { Write-Verbose "No proxy server configuration detected" return $false } } catch { Write-Verbose "Unable to check for proxy server configuration" return $false } } function Invoke-WebRequestWithProxyDetection { [CmdletBinding(DefaultParameterSetName = "Default")] param ( [Parameter(Mandatory = $true, ParameterSetName = "Default")] [string] $Uri, [Parameter(Mandatory = $false, ParameterSetName = "Default")] [switch] $UseBasicParsing, [Parameter(Mandatory = $true, ParameterSetName = "ParametersObject")] [hashtable] $ParametersObject, [Parameter(Mandatory = $false, ParameterSetName = "Default")] [string] $OutFile ) Write-Verbose "Calling $($MyInvocation.MyCommand)" if ([System.String]::IsNullOrEmpty($Uri)) { $Uri = $ParametersObject.Uri } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 if (Confirm-ProxyServer -TargetUri $Uri) { $webClient = New-Object System.Net.WebClient $webClient.Headers.Add("User-Agent", "PowerShell") $webClient.Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials } if ($null -eq $ParametersObject) { $params = @{ Uri = $Uri OutFile = $OutFile } if ($UseBasicParsing) { $params.UseBasicParsing = $true } } else { $params = $ParametersObject } try { Invoke-WebRequest @params } catch { Write-VerboseErrorInformation } } <# Determines if the script has an update available. #> function Get-ScriptUpdateAvailable { [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $false)] [string] $VersionsUrl = "https://github.com/microsoft/CSS-Exchange/releases/latest/download/ScriptVersions.csv" ) $BuildVersion = "23.08.10.1807" $scriptName = $script:MyInvocation.MyCommand.Name $scriptPath = [IO.Path]::GetDirectoryName($script:MyInvocation.MyCommand.Path) $scriptFullName = (Join-Path $scriptPath $scriptName) $result = [PSCustomObject]@{ ScriptName = $scriptName CurrentVersion = $BuildVersion LatestVersion = "" UpdateFound = $false Error = $null } if ((Get-AuthenticodeSignature -FilePath $scriptFullName).Status -eq "NotSigned") { Write-Warning "This script appears to be an unsigned test build. Skipping version check." } else { try { $versionData = [Text.Encoding]::UTF8.GetString((Invoke-WebRequestWithProxyDetection -Uri $VersionsUrl -UseBasicParsing).Content) | ConvertFrom-Csv $latestVersion = ($versionData | Where-Object { $_.File -eq $scriptName }).Version $result.LatestVersion = $latestVersion if ($null -ne $latestVersion) { $result.UpdateFound = ($latestVersion -ne $BuildVersion) } else { Write-Warning ("Unable to check for a script update as no script with the same name was found." + "`r`nThis can happen if the script has been renamed. Please check manually if there is a newer version of the script.") } Write-Verbose "Current version: $($result.CurrentVersion) Latest version: $($result.LatestVersion) Update found: $($result.UpdateFound)" } catch { Write-Verbose "Unable to check for updates: $($_.Exception)" $result.Error = $_ } } return $result } function Confirm-Signature { [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $true)] [string] $File ) $IsValid = $false $MicrosoftSigningRoot2010 = 'CN=Microsoft Root Certificate Authority 2010, O=Microsoft Corporation, L=Redmond, S=Washington, C=US' $MicrosoftSigningRoot2011 = 'CN=Microsoft Root Certificate Authority 2011, O=Microsoft Corporation, L=Redmond, S=Washington, C=US' try { $sig = Get-AuthenticodeSignature -FilePath $File if ($sig.Status -ne 'Valid') { Write-Warning "Signature is not trusted by machine as Valid, status: $($sig.Status)." throw } $chain = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Chain $chain.ChainPolicy.VerificationFlags = "IgnoreNotTimeValid" if (-not $chain.Build($sig.SignerCertificate)) { Write-Warning "Signer certificate doesn't chain correctly." throw } if ($chain.ChainElements.Count -le 1) { Write-Warning "Certificate Chain shorter than expected." throw } $rootCert = $chain.ChainElements[$chain.ChainElements.Count - 1] if ($rootCert.Certificate.Subject -ne $rootCert.Certificate.Issuer) { Write-Warning "Top-level certificate in chain is not a root certificate." throw } if ($rootCert.Certificate.Subject -ne $MicrosoftSigningRoot2010 -and $rootCert.Certificate.Subject -ne $MicrosoftSigningRoot2011) { Write-Warning "Unexpected root cert. Expected $MicrosoftSigningRoot2010 or $MicrosoftSigningRoot2011, but found $($rootCert.Certificate.Subject)." throw } Write-Host "File signed by $($sig.SignerCertificate.Subject)" $IsValid = $true } catch { $IsValid = $false } $IsValid } <# .SYNOPSIS Overwrites the current running script file with the latest version from the repository. .NOTES This function always overwrites the current file with the latest file, which might be the same. Get-ScriptUpdateAvailable should be called first to determine if an update is needed. In many situations, updates are expected to fail, because the server running the script does not have internet access. This function writes out failures as warnings, because we expect that Get-ScriptUpdateAvailable was already called and it successfully reached out to the internet. #> function Invoke-ScriptUpdate { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([boolean])] param () $scriptName = $script:MyInvocation.MyCommand.Name $scriptPath = [IO.Path]::GetDirectoryName($script:MyInvocation.MyCommand.Path) $scriptFullName = (Join-Path $scriptPath $scriptName) $oldName = [IO.Path]::GetFileNameWithoutExtension($scriptName) + ".old" $oldFullName = (Join-Path $scriptPath $oldName) $tempFullName = (Join-Path $env:TEMP $scriptName) if ($PSCmdlet.ShouldProcess("$scriptName", "Update script to latest version")) { try { Invoke-WebRequestWithProxyDetection -Uri "https://github.com/microsoft/CSS-Exchange/releases/latest/download/$scriptName" -OutFile $tempFullName } catch { Write-Warning "AutoUpdate: Failed to download update: $($_.Exception.Message)" return $false } try { if (Confirm-Signature -File $tempFullName) { Write-Host "AutoUpdate: Signature validated." if (Test-Path $oldFullName) { Remove-Item $oldFullName -Force -Confirm:$false -ErrorAction Stop } Move-Item $scriptFullName $oldFullName Move-Item $tempFullName $scriptFullName Remove-Item $oldFullName -Force -Confirm:$false -ErrorAction Stop Write-Host "AutoUpdate: Succeeded." return $true } else { Write-Warning "AutoUpdate: Signature could not be verified: $tempFullName." Write-Warning "AutoUpdate: Update was not applied." } } catch { Write-Warning "AutoUpdate: Failed to apply update: $($_.Exception.Message)" } } return $false } <# Determines if the script has an update available. Use the optional -AutoUpdate switch to make it update itself. Pass -Confirm:$false to update without prompting the user. Pass -Verbose for additional diagnostic output. Returns $true if an update was downloaded, $false otherwise. The result will always be $false if the -AutoUpdate switch is not used. #> function Test-ScriptVersion { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'Need to pass through ShouldProcess settings to Invoke-ScriptUpdate')] [CmdletBinding(SupportsShouldProcess)] [OutputType([bool])] param ( [Parameter(Mandatory = $false)] [switch] $AutoUpdate, [Parameter(Mandatory = $false)] [string] $VersionsUrl = "https://github.com/microsoft/CSS-Exchange/releases/latest/download/ScriptVersions.csv" ) $updateInfo = Get-ScriptUpdateAvailable $VersionsUrl if ($updateInfo.UpdateFound) { if ($AutoUpdate) { return Invoke-ScriptUpdate } else { Write-Warning "$($updateInfo.ScriptName) $BuildVersion is outdated. Please download the latest, version $($updateInfo.LatestVersion)." } } return $false } function Add-ADUserToLocalGroup { [CmdletBinding(SupportsShouldProcess)] [OutputType([bool])] param( [string]$MemberUPN, [string]$Group, [ScriptBlock]$CatchActionFunction ) <# This function adds an Active Directory user to a local group. #> try { Write-Verbose "Calling: $($MyInvocation.MyCommand)" Add-Type -AssemblyName "System.DirectoryServices.AccountManagement" -ErrorAction Stop $localContext = [System.DirectoryServices.AccountManagement.ContextType]::Machine $domainContext = [System.DirectoryServices.AccountManagement.ContextType]::Domain $localMachine = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext($localContext) $localGroup = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($localMachine, $Group) if (-not($localGroup.Members.Contains($domainContext, [System.DirectoryServices.AccountManagement.IdentityType]::UserPrincipalName, $MemberUPN))) { if ($PSCmdlet.ShouldProcess($Group, "Add user $($MemberUPN) to local group")) { $localGroup.Members.Add($domainContext, [System.DirectoryServices.AccountManagement.IdentityType]::UserPrincipalName, $MemberUPN) $localGroup.Save() } } else { Write-Verbose ("User: $($MemberUPN) is already a member of group: $($Group)") } } catch [System.DirectoryServices.AccountManagement.PrincipalOperationException] { throw ("There are users in the local administrators group which cannot be resolved - please remove them and run the script again") Invoke-CatchActionError $CatchActionFunction return } catch { Write-Verbose ("Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction return } finally { if ($null -ne $localGroup) { $localGroup.Dispose() } } return $true } function New-AuthCertificateManagementAccount { [CmdletBinding(SupportsShouldProcess)] [OutputType([bool])] param( [SecureString]$Password, [string]$DomainToUse = $env:USERDNSDOMAIN, [string]$DomainController = $env:USERDNSDOMAIN, [ScriptBlock]$CatchActionFunction ) Write-Verbose "Calling: $($MyInvocation.MyCommand)" $systemMailboxGuid = "b963af59-3975-4f92-9d58-ad0b1fe3a1a3" $samAccountName = "SM_ad0b1fe3a1a3" $userPrincipalName = "SystemMailbox{$($systemMailboxGuid)}@$($DomainToUse)" Write-Verbose ("Domain passed to the function is: $($DomainToUse)") Write-Verbose ("Domain or Domain Controller to be used with 'New-ADUser' call is: $($DomainController)") try { $adAccount = Get-ADUser -Identity $samAccountName -Server $DomainController -ErrorAction Stop } catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] { Write-Verbose ("AD user account wasn't found using Domain Controller: $($DomainController)") Invoke-CatchActionError $CatchActionFunction } catch { Write-Verbose ("We hit an unhandled exception and cannot continue - Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction return $false } if ($null -eq $adAccount) { try { $newADUserParams = @{ Name = "SystemMailbox{$($systemMailboxGuid)}" DisplayName = "Microsoft Exchange Auth Certificate Manager" SamAccountName = $samAccountName UserPrincipalName = $userPrincipalName AccountPassword = $Password Enabled = $true PasswordNeverExpires = $true Server = $DomainController ErrorAction = "Stop" } if ($PSCmdlet.ShouldProcess($samAccountName, "New-ADUser")) { New-ADUser @newADUserParams | Out-Null } Write-Verbose ("User: 'Microsoft Exchange Auth Certificate Manager' was successfully created") return $true } catch [System.UnauthorizedAccessException] { Write-Verbose ("You don't have the permissions to create a new AD user account") Invoke-CatchActionError $CatchActionFunction } catch { Write-Verbose ("Something went wrong while creating the 'Microsoft Exchange Auth Certificate Manager' account - Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction } } else { Write-Verbose ("The AD account: $($userPrincipalName) already exists") Write-Verbose ("Trying to reset the password for the account") try { if ($PSCmdlet.ShouldProcess($adAccount, "Set-ADAccountPassword")) { Set-ADAccountPassword -Identity $adAccount -NewPassword $Password -Reset -Server $DomainController -Confirm:$false -ErrorAction Stop } if ($PSCmdlet.ShouldProcess($adAccount, "Set-ADUser")) { Set-ADUser -Identity $adAccount -ChangePasswordAtLogon $false -Server $DomainController -Confirm:$false -ErrorAction Stop } return $true } catch [System.UnauthorizedAccessException] { Write-Verbose ("You don't have the permissions to reset the password of an AD account") Invoke-CatchActionError $CatchActionFunction } catch { Write-Verbose ("Unable to reset the password for the already existing AD user account - Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction } } return $false } function Build-ExchangeAuthCertificateManagementAccount { [CmdletBinding(DefaultParameterSetName = "CreateNewAccount", SupportsShouldProcess)] [OutputType([System.Object])] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "UseExistingAccount")] [bool]$UseExistingAccount = $false, [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "UseExistingAccount")] [PSCredential]$AccountCredentialObject, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "CreateNewAccount")] [SecureString]$PasswordToSet, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "CreateNewAccount")] [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "UseExistingAccount")] [string]$DomainController, [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "CreateNewAccount")] [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "UseExistingAccount")] [ScriptBlock]$CatchActionFunction ) begin { Write-Verbose "Calling: $($MyInvocation.MyCommand)" $systemMailboxIdentity = "SM_ad0b1fe3a1a3" $domainToUse = (Get-Mailbox -Arbitration -ErrorAction SilentlyContinue | Where-Object { ($null -ne $_.UserPrincipalName) } | Select-Object -First 1).UserPrincipalName.Split("@")[-1] if (($UseExistingAccount) -and ($null -ne $AccountCredentialObject)) { Write-Verbose ("Account information passed - we will use account: $($AccountCredentialObject.UserName)") if ($AccountCredentialObject.UserName.IndexOf("\") -ne -1) { Write-Verbose ("Username passed in \ format") $systemMailboxIdentity = ($AccountCredentialObject.UserName).Split("\")[-1] } else { Write-Verbose ("Username passed in UPN or plain format") $systemMailboxIdentity = $AccountCredentialObject.UserName } $PasswordToSet = $AccountCredentialObject.Password } function NewAuthCertificateManagementRole { [CmdletBinding()] [OutputType([bool])] param( [string]$DomainController ) Write-Verbose "Calling: $($MyInvocation.MyCommand)" try { Write-Verbose ("Trying to create 'Auth Certificate Management' role group by using Domain Controller: $($DomainController)") if ($PSCmdlet.ShouldProcess("View-Only Configuration, View-Only Recipients, Exchange Server Certificates, Organization Client Access", "New-RoleGroup")) { $roleGroupParams = @{ Name = "Auth Certificate Management" Roles = "View-Only Configuration", "View-Only Recipients" , "Exchange Server Certificates", "Organization Client Access" Description = "Members of this management group can create and manage Auth Certificates" DomainController = $DomainController ErrorAction = "Stop" WhatIf = $WhatIfPreference } New-RoleGroup @roleGroupParams | Out-Null Write-Verbose ("Validate that the role group was created successful") $roleGroup = Get-RoleGroup -Identity "Auth Certificate Management" -DomainController $DomainController -ErrorAction SilentlyContinue if ($null -ne $roleGroup) { Write-Verbose ("Role group 'Auth Certificate Management' found by using Domain Controller: $($DomainController)") return $true } else { throw ("Role group 'Auth Certificate Management' not found by using Domain Controller: $($DomainController)") } } else { return $true } } catch { Write-Verbose ("Unable to create 'Auth Certificate Management' role group - Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction } return $false } } process { if ($null -eq $domainToUse) { Write-Verbose ("Unable to figure out the domain used by the arbitration mailbox - we can't continue without this information") return } $authCertificateRoleGroup = Get-RoleGroup -Identity "Auth Certificate Management" -ErrorAction SilentlyContinue if ($null -eq $authCertificateRoleGroup) { Write-Verbose ("Role group for Auth Certificate management doesn't exist. Group 'Auth Certificate Management' will be created now") $newRoleGroupStatus = NewAuthCertificateManagementRole -DomainController $DomainController } if (($null -ne $authCertificateRoleGroup) -or ($newRoleGroupStatus)) { Write-Verbose ("Role group exists or was created successfully - searching for Auth Certificate management account") if ($UseExistingAccount -eq $false) { Write-Verbose ("System mailbox doesn't exist and will be created now") $newAuthCertificateManagementAccountParams = @{ Password = $PasswordToSet DomainToUse = $domainToUse DomainController = $DomainController WhatIf = $WhatIfPreference } if ($null -ne $CatchActionFunction) { $newAuthCertificateManagementAccountParams.Add("CatchActionFunction", ${Function:Invoke-CatchActions}) } $adUserExistsOrCreated = New-AuthCertificateManagementAccount @newAuthCertificateManagementAccountParams Write-Verbose ("Waiting 10 seconds for replication - please be patient") Start-Sleep -Seconds 10 } else { Write-Verbose ("Trying to find the user which was passed to the function") $adUserExistsOrCreated = ((Get-User -Identity $systemMailboxIdentity -DomainController $DomainController -ErrorAction SilentlyContinue).Count -eq 1) Write-Verbose ("Does the account exists? $($adUserExistsOrCreated)") } } else { Write-Verbose ("Something went wrong while preparing the Auth Certificate management role group") return } if ($adUserExistsOrCreated) { Write-Verbose ("Auth Certificate management AD account is now ready to use - going to email enable it now") $systemMailboxRecipientInfo = Get-Recipient -Identity $systemMailboxIdentity -ErrorAction SilentlyContinue if ($null -eq $systemMailboxRecipientInfo) { Write-Verbose ("Recipient has not yet been email enabled") try { if ($PSCmdlet.ShouldProcess($systemMailboxIdentity, "Enable-Mailbox")) { Enable-Mailbox -Identity $systemMailboxIdentity -DomainController $DomainController -ErrorAction Stop | Out-Null Write-Verbose ("Wait another 5 seconds and give Exchange time to process") Start-Sleep -Seconds 5 } } catch { Write-Verbose ("Something went wrong while email activating the Auth Certificate management account") Invoke-CatchActionError $CatchActionFunction return } } } else { Write-Verbose ("Something went wrong while preparing the Auth Certificate management account") return } $systemMailboxMailboxInfo = Get-Mailbox -Identity $systemMailboxIdentity -DomainController $DomainController -ErrorAction SilentlyContinue if (($WhatIfPreference) -and ($null -eq $systemMailboxMailboxInfo)) { $systemMailboxMailboxInfo = @{ HiddenFromAddressListsEnabled = $false } } if ($null -ne $systemMailboxMailboxInfo) { Write-Verbose ("Auth Certificate management mailbox found") if ($systemMailboxMailboxInfo.HiddenFromAddressListsEnabled -eq $false) { Write-Verbose ("Auth Certificate management mailbox is not hidden from AddressList - going to hide the mailbox now") try { if ($PSCmdlet.ShouldProcess($systemMailboxIdentity, "Set-Mailbox")) { Set-Mailbox -Identity $systemMailboxIdentity -HiddenFromAddressListsEnabled $true -ErrorAction Stop | Out-Null } } catch { Write-Verbose ("Unable to hide Auth Certificate management account from AddressList") Invoke-CatchActionError $CatchActionFunction return } } } else { Write-Verbose ("Unable to email enable the Auth Certificate management account") return } $roleGroupMembership = Get-RoleGroupMember "Auth Certificate Management" -ErrorAction SilentlyContinue $systemMailboxUserInfo = Get-User -Identity $systemMailboxIdentity -DomainController $DomainController -ErrorAction SilentlyContinue if (($WhatIfPreference) -and ($null -eq $systemMailboxUserInfo)) { $systemMailboxUserInfo = @{ SamAccountName = $systemMailboxIdentity UserPrincipalName = $systemMailboxIdentity } } if (($null -eq $roleGroupMembership) -or (-not($roleGroupMembership.DistinguishedName.ToLower().Contains($systemMailboxUserInfo.DistinguishedName.ToLower())))) { Write-Verbose ("Add Auth Certificate management account to 'Auth Certificate Management' role group") try { if ($PSCmdlet.ShouldProcess($systemMailboxIdentity, "Add-RoleGroupMember")) { Add-RoleGroupMember "Auth Certificate Management" -Member $systemMailboxIdentity -ErrorAction Stop | Out-Null Write-Verbose ("Auth Certificate management account added to 'Auth Certificate Management' role group") } } catch { Write-Verbose ("Unable to add Auth Certificate management account to role group") Invoke-CatchActionError $CatchActionFunction return } } else { Write-Verbose ("Account: $($systemMailboxIdentity) is already a member of the 'Auth Certificate Management' role group") } if ($null -ne $systemMailboxUserInfo) { Write-Verbose ("Account: $($systemMailboxIdentity) must be added to the local administrators group") if (Add-ADUserToLocalGroup -MemberUPN $systemMailboxUserInfo.UserPrincipalName -Group "S-1-5-32-544" -WhatIf:$WhatIfPreference) { Write-Verbose ("Account successfully added to local administrators group") } else { Write-Verbose ("Error while adding the user to the local administrators group - Exception: $($Error[0].Exception.Message)") return } } else { Write-Verbose ("Something went wrong as we can no longer find the Auth Certificate management account") return } } end { return [PSCustomObject]@{ UserPrincipalName = $systemMailboxUserInfo.UserPrincipalName SamAccountName = $systemMailboxUserInfo.SamAccountName Password = $PasswordToSet } } } function Copy-ScriptToExchangeDirectory { [CmdletBinding(SupportsShouldProcess)] [OutputType([System.Object])] param( [Parameter(Mandatory = $false)] [string]$FullPathToScript = $MyInvocation.ScriptName, [Parameter(Mandatory = $false)] [ScriptBlock]$CatchActionFunction ) Write-Verbose "Calling: $($MyInvocation.MyCommand)" $exchangeInstallPath = $env:ExchangeInstallPath $scriptName = $FullPathToScript.Split("\")[-1] if ($null -ne $exchangeInstallPath) { Write-Verbose ("ExchangeInstallPath is: $($exchangeInstallPath)") $localScriptsPath = [System.IO.Path]::Combine($exchangeInstallPath, "Scripts") try { if (-not(Test-Path -Path $localScriptsPath)) { Write-Verbose ("Folder: $($localScriptsPath) doesn't exist - it will be created now") if ($PSCmdlet.ShouldProcess("$exchangeInstallPath\Scripts", "New-Item")) { New-Item -Path $exchangeInstallPath -ItemType Directory -Name "Scripts" -ErrorAction Stop | Out-Null } } if (($WhatIfPreference) -or (Test-Path -Path $localScriptsPath -ErrorAction Stop)) { Write-Verbose ("Path: $($localScriptsPath) was successfully created") if ($PSCmdlet.ShouldProcess("Copy: $FullPathToScript To: $localScriptsPath", "Copy-Item")) { Copy-Item -Path $FullPathToScript -Destination $localScriptsPath -Force -ErrorAction Stop } if (($WhatIfPreference) -or (Test-Path -Path $FullPathToScript)) { Write-Verbose ("Script: $($scriptName) successfully copied over to: $($localScriptsPath)") return [PSCustomObject]@{ WorkingDirectory = $localScriptsPath ScriptName = $scriptName } } } } catch { Write-Verbose ("Something went wrong - Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction } } return } function Export-ExchangeAuthCertificate { [CmdletBinding(SupportsShouldProcess)] [OutputType([System.Object])] param( [Parameter(Mandatory = $true)] [SecureString]$Password, [ScriptBlock]$CatchActionFunction ) <# This function exports the current Auth Certificate and (if configured) the next Auth Certificate. The certificates will be stored as password protected .pfx file. #> try { Write-Verbose "Calling: $($MyInvocation.MyCommand)" $certificatesReadyToExportList = New-Object 'System.Collections.Generic.List[object]' $certificatesUnableToExportList = New-Object 'System.Collections.Generic.List[string]' $currentAuthConfig = Get-AuthConfig -ErrorAction Stop $allExchangeCertificates = Get-ExchangeCertificate -Server $env:COMPUTERNAME -ErrorAction SilentlyContinue if ($null -ne $currentAuthConfig) { $currentAuthCertThumbprint = $currentAuthConfig.CurrentCertificateThumbprint $nextAuthCertThumbprint = $currentAuthConfig.NextCertificateThumbprint if (-not([System.String]::IsNullOrEmpty($currentAuthCertThumbprint))) { Write-Verbose ("CurrentCertificateThumbprint is: $($currentAuthCertThumbprint) - trying to find it on the local computer") $currentAuthCertificate = $allExchangeCertificates | Where-Object { ($_.Thumbprint -eq $currentAuthCertThumbprint) } if (($null -eq $currentAuthCertificate) -or ($currentAuthCertificate.HasPrivateKey -eq $false) -or ($currentAuthCertificate.PrivateKeyExportable -eq $false)) { Write-Verbose ("Current Auth Certificate doesn't fullfil the requirements to be exportable on this machine") $certificatesUnableToExportList.Add($currentAuthCertThumbprint) } else { Write-Verbose ("Current Auth Certificate was detected on the local machine and is ready to be exported") $certificatesReadyToExportList.Add($currentAuthCertificate) } } if (-not([System.String]::IsNullOrEmpty($nextAuthCertThumbprint))) { Write-Verbose ("NextCertificateThumbprint is: $($nextAuthCertThumbprint) - trying to find it on the local computer") $nextAuthCertificate = $allExchangeCertificates | Where-Object { ($_.Thumbprint -eq $nextAuthCertThumbprint) } if (($null -eq $nextAuthCertificate) -or ($nextAuthCertificate.HasPrivateKey -eq $false) -or ($nextAuthCertificate.PrivateKeyExportable -eq $false)) { Write-Verbose ("Next Auth Certificate doesn't fullfil the requirements to be exportable on this machine") $certificatesUnableToExportList.Add($nextAuthCertThumbprint) } else { Write-Verbose ("Next Auth Certificate was detected on the local machine and is ready to be exported") $certificatesReadyToExportList.Add($nextAuthCertificate) } } Write-Verbose ("There are: $($certificatesReadyToExportList.Count) certificates on the list that will be exported now") $dateTimeAppendix = (Get-Date -Format "yyyyMMddhhmmss") foreach ($cert in $certificatesReadyToExportList) { Write-Verbose ("Exporting the certificate with thumbprint: $($cert.Thumbprint) now...") try { if ($PSCmdlet.ShouldProcess($cert.Thumbprint, "Export-ExchangeCertificate")) { $authCert = Export-ExchangeCertificate -Thumbprint $cert.Thumbprint -BinaryEncoded -Password $Password } $certExportPath = "$($PSScriptRoot)\$($cert.Thumbprint)-$($dateTimeAppendix).pfx" if ($PSCmdlet.ShouldProcess("Export certificate: $($cert.Thumbprint) To: $certExportPath", "[System.IO.File]::WriteAllBytes")) { [System.IO.File]::WriteAllBytes($certExportPath, $authCert.FileData) } Write-Verbose ("Certificate exported to: $certExportPath") } catch { Write-Verbose ("We hit an issue during certificate export - Exception $($Error[0].Exception.Message)") $certificatesUnableToExportList.Add($authCert.Thumbprint) Invoke-CatchActionError $CatchActionFunction } } } else { Write-Verbose ("No valid Auth Config returned") return } } catch { Write-Verbose ("Unable to query the Exchange Auth Config - Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction } return [PSCustomObject]@{ CertificatesAvailableToExport = ($certificatesReadyToExportList.Count -gt 0) ExportSuccessful = (($certificatesReadyToExportList.Count -gt 0) -and ($certificatesUnableToExportList.Count -eq 0)) NumberOfCertificatesToExport = $certificatesReadyToExportList.Count NumberOfCertificatesUnableToExport = $certificatesUnableToExportList.Count UnableToExportCertificatesList = $certificatesUnableToExportList } } function Import-ExchangeAuthCertificateToServers { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $false)] [string]$ExportFromServer = $env:COMPUTERNAME, [Parameter(Mandatory = $true)] [string]$Thumbprint, [Parameter(Mandatory = $true)] [System.Collections.Generic.List[string]]$ServersToImportList, [Parameter(Mandatory = $false)] [ScriptBlock]$CatchActionFunction ) <# This function can be used to export an Exchange Certificate as byte array and import it to a list of servers which were passed to this function via ServersToImportList parameter. The function returns a PSCustomObject with the following properties: - ExportSuccessful : Indicator if the certificate was successfully exported on the source server (where the script runs) - ImportToAllServersSuccessful : Indicator if the certificate was successfully imported to all servers - Thumbprint : Thumbprint of the certificate that was imported - ImportedToServersList : List of all servers on which the certificate was successfully imported - ImportToServersFailedList : List of all serves on which the certificate import failed for whatever reason #> begin { Write-Verbose "Calling: $($MyInvocation.MyCommand)" $exportSuccessful = $false $importFailedList = New-Object "System.Collections.Generic.List[string]" $importSuccessfulList = New-Object "System.Collections.Generic.List[string]" } process { try { # Generate a temporary password to protect the exported private key in memory and on transport $bytes = [System.Byte[]]::new(64) ([System.Security.Cryptography.RandomNumberGenerator]::Create()).GetBytes($bytes) $secureString = [System.Security.SecureString]::new() foreach ($b in $bytes) { $secureString.AppendChar([char]$b) } $secureString.MakeReadOnly() $bytes = $null if ($PSCmdlet.ShouldProcess($Thumbprint, "Export-ExchangeCertificate")) { # Export the certificate as byte array as we need to pass this to the Import-ExchangeCertificate cmdlet $exportExchangeCertificateParams = @{ Server = $ExportFromServer Thumbprint = $Thumbprint BinaryEncoded = $true Password = $secureString ErrorAction = "Stop" } $exportedAuthCertificate = Export-ExchangeCertificate @exportExchangeCertificateParams } if (($null -ne $exportedAuthCertificate.FileData) -or ($WhatIfPreference)) { Write-Verbose ("Certificate with thumbprint: $Thumbprint successfully exported") $exportSuccessful = $true # Next step is to import the certificate to all Exchange servers passed via $ServersToImportList parameter foreach ($server in $ServersToImportList) { try { if ($PSCmdlet.ShouldProcess($server, "Import-ExchangeCertificate")) { $importExchangeCertificateParams = @{ Server = $server FileData = $exportedAuthCertificate.FileData Password = $secureString PrivateKeyExportable = $true ErrorAction = "Stop" } Import-ExchangeCertificate @importExchangeCertificateParams } Write-Verbose ("Certificate import to server: $server was successful") $importSuccessfulList.Add($server) } catch { Write-Verbose ("Unable to import the certificate to server: $server - Exception: $($Error[0].Exception.Message)") $importFailedList.Add($server) Invoke-CatchActionError $CatchActionFunction } } } else { Write-Verbose ("Unable to export the certificate with thumbprint: $Thumbprint") } } catch { Write-Verbose ("Something went wrong - Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction } } end { $exportedAuthCertificate = $null $secureString.Dispose() return [PSCustomObject]@{ ExportSuccessful = $exportSuccessful ImportToAllServersSuccessful = (($importFailedList.Count -eq 0) -and ($exportSuccessful)) Thumbprint = $Thumbprint ImportedToServersList = $importSuccessfulList ImportToServersFailedList = $importFailedList } } } function New-AuthCertificateMonitoringLogFolder { [CmdletBinding(SupportsShouldProcess)] [OutputType([System.String])] param() Write-Verbose "Calling: $($MyInvocation.MyCommand)" $exchangeInstallPath = $env:ExchangeInstallPath if ($null -eq $exchangeInstallPath) { Write-Verbose ("ExchangeInstallPath environment variable doesn't exist - fallback to use temp folder to store logs") $exchangeInstallPath = $env:TEMP } if ($null -ne $exchangeInstallPath) { $logFilePath = [System.IO.Path]::Combine($exchangeInstallPath, "Logging") $finalLogPath = [System.IO.Path]::Combine($logFilePath, "AuthCertificateMonitoring") if ((Test-Path -Path $finalLogPath) -eq $false) { if ($PSCmdlet.ShouldProcess("$logFilePath\AuthCertificateMonitoring", "New-Item")) { New-Item -Path $logFilePath -ItemType Directory -Name "AuthCertificateMonitoring" -ErrorAction SilentlyContinue | Out-Null } } return $finalLogPath } return } function Import-ExchangeCertificateFromRawData { [CmdletBinding()] param( [System.Object[]]$ExchangeCertificates ) <# This helper function must be used if Serialization Data Signing is enabled, but the Auth Certificate which is configured has expired or isn't available on the system where the script runs. The 'Get-ExchangeCertificate' cmdlet fails to deserialize and so, only RawData (byte[]) will be returned. To workaround, we initialize the X509Certificate2 class and import the data by using the Import() method. #> begin { Write-Verbose "Calling: $($MyInvocation.MyCommand)" $exchangeCertificatesList = New-Object 'System.Collections.Generic.List[object]' } process { if ($ExchangeCertificates.Count -ne 0) { Write-Verbose ("Going to process '$($ExchangeCertificates.Count )' Exchange certificates") foreach ($c in $ExchangeCertificates) { # Initialize X509Certificate2 class $certObject = New-Object 'System.Security.Cryptography.X509Certificates.X509Certificate2' # Use the Import() method to import byte[] RawData $certObject.Import($c.RawData) if ($null -ne $certObject.Thumbprint) { Write-Verbose ("Certificate with thumbprint: $($certObject.Thumbprint) imported successfully") $exchangeCertificatesList.Add($certObject) } } } } end { return $exchangeCertificatesList } } function Get-ExchangeServerCertificate { [CmdletBinding()] [OutputType([System.Object])] param( [string]$Server = $env:COMPUTERNAME, [string]$Thumbprint = $null ) begin { Write-Verbose "Calling: $($MyInvocation.MyCommand)" $allExchangeCertificates = New-Object 'System.Collections.Generic.List[object]' } process { $getExchangeCertificateParams = @{ Server = $Server ErrorAction = "Stop" } if (-not([System.String]::IsNullOrEmpty($Thumbprint))) { $getExchangeCertificateParams.Add("Thumbprint", $Thumbprint) } $exchangeCertificates = Get-ExchangeCertificate @getExchangeCertificateParams if ($null -ne $exchangeCertificates) { if ($null -ne $exchangeCertificates[0].Thumbprint) { Write-Verbose ("Deserialization of the Exchange certificates was successful") foreach ($c in $exchangeCertificates) { $allExchangeCertificates.Add($c) } } else { Write-Verbose ("Deserialization of the Exchange certificates failed - trying to import from RawData") foreach ($c in $exchangeCertificates) { $allExchangeCertificates.Add($(Import-ExchangeCertificateFromRawData $c)) } } Write-Verbose ("$($allExchangeCertificates.Count) Exchange Server certificates were returned") } } end { return $allExchangeCertificates } } function Get-ExchangeContainer { [CmdletBinding()] [OutputType([System.DirectoryServices.DirectoryEntry])] param () $rootDSE = [ADSI]("LDAP://$([System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Name)/RootDSE") $exchangeContainerPath = ("CN=Microsoft Exchange,CN=Services," + $rootDSE.configurationNamingContext) $exchangeContainer = [ADSI]("LDAP://" + $exchangeContainerPath) Write-Verbose "Exchange Container Path: $($exchangeContainer.path)" return $exchangeContainer } function Get-OrganizationContainer { [CmdletBinding()] [OutputType([System.DirectoryServices.DirectoryEntry])] param () $exchangeContainer = Get-ExchangeContainer $searcher = New-Object System.DirectoryServices.DirectorySearcher($exchangeContainer, "(objectClass=msExchOrganizationContainer)", @("distinguishedName")) return $searcher.FindOne().GetDirectoryEntry() } function Get-InternalTransportCertificateFromServer { [CmdletBinding()] [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])] param ( [string]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] [ScriptBlock]$CatchActionFunction ) <# Reads the certificate set as internal transport certificate (aka default SMTP certificate) from AD. The certificate is specified on a per-server base. Returns the X509Certificate2 object if we were able to query it from AD, otherwise it returns $null. #> try { Write-Verbose "Calling: $($MyInvocation.MyCommand)" $organizationContainer = Get-OrganizationContainer $exchangeServerPath = ("CN=" + $($ComputerName.Split(".")[0]) + ",CN=Servers,CN=Exchange Administrative Group (FYDIBOHF23SPDLT),CN=Administrative Groups," + $organizationContainer.distinguishedName) $exchangeServer = [ADSI]("LDAP://" + $exchangeServerPath) Write-Verbose "Exchange Server path: $($exchangeServerPath)" if ($null -ne $exchangeServer.msExchServerInternalTLSCert) { $certObject = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($exchangeServer.msExchServerInternalTLSCert) Write-Verbose ("Internal transport certificate on server: $($ComputerName) is: $($certObject.Thumbprint)") } } catch { Write-Verbose ("Unable to query the internal transport certificate - Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction } return $certObject } function New-ExchangeAuthCertificate { [CmdletBinding(DefaultParameterSetName = "NewPrimaryAuthCert", SupportsShouldProcess = $true, ConfirmImpact = "High")] [OutputType([System.Object])] param( [Parameter(Mandatory = $false, ParameterSetName = "NewPrimaryAuthCert")] [switch]$ReplaceExpiredAuthCertificate, [Parameter(Mandatory = $false, ParameterSetName = "NewNextAuthCert")] [switch]$ConfigureNextAuthCertificate, [Parameter(Mandatory = $true, ParameterSetName = "NewNextAuthCert")] [int]$CurrentAuthCertificateLifetimeInDays, [Parameter(Mandatory = $false, ParameterSetName = "NewPrimaryAuthCert")] [Parameter(Mandatory = $false, ParameterSetName = "NewNextAuthCert")] [ScriptBlock]$CatchActionFunction ) begin { Write-Verbose "Calling: $($MyInvocation.MyCommand)" function GetCertificateBoundToDefaultWebSiteThumbprints { [CmdletBinding()] param() <# Returns the thumbprint of the certificate which is bound to the 'Default Web Site' in IIS #> Write-Verbose "Calling: $($MyInvocation.MyCommand)" try { # Remove empty elements from array as they could be returned if no certificate is bound to a binding in between, then sort # the array and remove duplicates. $hashes = ((Get-Website -Name "Default Web Site" -ErrorAction Stop).bindings.collection.CertificateHash) | Where-Object { $_ } | Sort-Object -Unique } catch { Write-Verbose ("Unable to query 'Default Web Site' SSL binding information") Invoke-CatchActionError $CatchActionFunction } return $hashes } function GenerateNewAuthCertificate { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] [OutputType([System.Object])] param() <# Generates a new Auth Certificate which can then be configured via 'Set-AuthConfig' Returns a PSCustomObject with the information of the newly generated certificate which can then be consumed by the next function which configures the certificate as new Auth Certificate. #> Write-Verbose "Calling: $($MyInvocation.MyCommand)" $confirmationMessage = "The following actions will be performed without the need to reconfirm:" + "`r`n - The internal transport certificate will be queried" + "`r`n - A new certificate will be generated, it overrides the internal transport certificate" + "`r`n - The internal transport certificate will be set back to the previous one" + "`r`n or" + "`r`n - A new internal transport certificate will be generated if the previous one is invalid" $operationSuccessful = $false $internalTransportCertificateFoundOnServer = $false $errorCount = $Error.Count $authCertificateFriendlyName = ("Microsoft Exchange Server Auth Certificate - $(Get-Date -Format yyyyMMddhhmmss)") try { $newInternalTransportCertificateParams = @{ Server = $env:COMPUTERNAME KeySize = 2048 PrivateKeyExportable = $true FriendlyName = $env:COMPUTERNAME DomainName = $env:COMPUTERNAME IncludeServerFQDN = $true Services = "SMTP" Force = $true ErrorAction = "Stop" } $newAuthCertificateParams = @{ Server = $env:COMPUTERNAME KeySize = 2048 PrivateKeyExportable = $true SubjectName = "cn=Microsoft Exchange Server Auth Certificate" FriendlyName = $authCertificateFriendlyName DomainName = @() ErrorAction = "Stop" } if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, $confirmationMessage, "Unattended Exchange certificate generation")) { Write-Verbose ("Internal transport certificate will be overwritten for a short time and then reset to the previous one") $internalTransportCertificate = Get-InternalTransportCertificateFromServer $env:COMPUTERNAME $defaultWebSiteCertificateThumbprints = GetCertificateBoundToDefaultWebSiteThumbprints [string]$internalTransportCertificateThumbprint = $internalTransportCertificate.Thumbprint if (($null -ne $internalTransportCertificate) -and ($null -ne $defaultWebSiteCertificateThumbprints)) { $newAuthCertificateParams.Add("Force", $true) $servicesToEnable = $null $servicesToEnableList = New-Object 'System.Collections.Generic.List[object]' try { $internalTransportCertificate = Get-ExchangeServerCertificate -Server $env:COMPUTERNAME -Thumbprint $internalTransportCertificateThumbprint -ErrorAction Stop if ($null -ne $internalTransportCertificate) { $internalTransportCertificateFoundOnServer = $true $isInternalTransportBoundToIisFe = $defaultWebSiteCertificateThumbprints.Contains($internalTransportCertificateThumbprint) if (($null -ne $internalTransportCertificate.Services) -and ($internalTransportCertificate.Services -ne 0)) { $servicesToEnableList.AddRange(($internalTransportCertificate.Services).ToString().ToUpper().Split(",").Trim()) # Make sure to remove IIS from list if the certificate was not bound to Front End Website before if (($isInternalTransportBoundToIisFe -eq $false) -and ($servicesToEnableList.Contains("IIS"))) { Write-Verbose ("Internal transport certificate is bound to Back End Website - avoid to enable it for IIS to prevent it being bound to Front End") $servicesToEnableList.Remove("IIS") } } elseif ($null -eq $internalTransportCertificate.Services) { Write-Verbose ("No service information returned for internal transport certificate") if ($isInternalTransportBoundToIisFe) { Write-Verbose ("Internal transport certificate was bound to Front-End Website and will be rebound to it again") $servicesToEnableList.Add("IIS") } $servicesToEnableList.Add("SMTP") } $servicesToEnable = $([string]::Join(", ", $servicesToEnableList)) } } catch { Invoke-CatchActionError $CatchActionFunction Write-Verbose ("Internal transport certificate wasn't detected on server: $($env:COMPUTERNAME)") Write-Verbose ("We will generate a new internal transport certificate now") try { if ($PSCmdlet.ShouldProcess("New-ExchangeCertificate", "Generate new internal transport certificate")) { $newSelfSignedTransportCertificate = New-ExchangeCertificate @newInternalTransportCertificateParams if ($null -ne $newSelfSignedTransportCertificate) { $internalTransportCertificateFoundOnServer = $true if ($null -ne $newSelfSignedTransportCertificate.Thumbprint) { Write-Verbose ("Certificate object successfully deserialized") [string]$internalTransportCertificateThumbprint = $newSelfSignedTransportCertificate.Thumbprint } else { Write-Verbose ("Looks like deserialization of the certificate object failed - trying to import from RawData") [string]$internalTransportCertificateThumbprint = (Import-ExchangeCertificateFromRawData $newSelfSignedTransportCertificate).Thumbprint if ($null -ne $internalTransportCertificateThumbprint) { Write-Verbose ("Import from RawData was successful") } else { throw ("Import from RawData failed") } } Write-Verbose ("A new internal transport certificate with thumbprint: $($internalTransportCertificateThumbprint) was generated") $servicesToEnable = "SMTP" } } else { $newInternalTransportCertificateParams.GetEnumerator() | ForEach-Object { Write-Host ("What if: Key: $($_.key) - Value: $($_.value)") } } } catch { Write-Verbose ("Hit an exception while trying to generate a new internal transport certificate - Exception: $(Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction } } } } Write-Verbose ("Starting Auth Certificate creation process") try { if ($PSCmdlet.ShouldProcess("New-ExchangeCertificate", "Generate new Auth Certificate")) { $newAuthCertificate = New-ExchangeCertificate @newAuthCertificateParams Start-Sleep -Seconds 5 } else { $newAuthCertificateParams.GetEnumerator() | ForEach-Object { Write-Host ("What if: Key: $($_.key) - Value: $($_.value)") } # Create dummy object to pass the following checks if -WhatIf was used as we don't create a new certificate in this mode $newAuthCertificate = @{ Thumbprint = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234" } } } catch { Write-Verbose ("Hit an exception while trying to generate a new Exchange Server Auth Certificate - Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction } if ($internalTransportCertificateFoundOnServer) { if ($PSCmdlet.ShouldProcess("Certificate: $internalTransportCertificateThumbprint on: $env:COMPUTERNAME for: $servicesToEnable", "Enable-ExchangeCertificate")) { Write-Verbose ("Resetting internal transport certificate back to previous one") Enable-ExchangeCertificate -Server $env:COMPUTERNAME -Thumbprint $internalTransportCertificateThumbprint -Services $servicesToEnable -Force | Out-Null Start-Sleep -Seconds 10 Write-Verbose ("Internal transport certificate was reset back to: $((Get-InternalTransportCertificateFromServer $env:COMPUTERNAME).Thumbprint)") } } if ($null -ne $newAuthCertificate) { $operationSuccessful = $true if ($null -ne $newAuthCertificate.Thumbprint) { Write-Verbose ("Certificate object successfully deserialized") [string]$newAuthCertificateThumbprint = $newAuthCertificate.Thumbprint } else { Write-Verbose ("Looks like deserialization of the certificate object failed - trying to import from RawData") [string]$newAuthCertificateThumbprint = (Import-ExchangeCertificateFromRawData $newAuthCertificate).Thumbprint if ($null -ne $newAuthCertificateThumbprint) { Write-Verbose ("Import from RawData was successful") } else { throw ("Import from RawData failed") } } Write-Verbose ("New Auth Certificate was successfully created. Thumbprint: $($newAuthCertificateThumbprint)") } } catch { Write-Verbose ("We hit an exception during Auth Certificate creation process - Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction } return [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME InternalTransportThumbprint = $internalTransportCertificateThumbprint FriendlyName = $authCertificateFriendlyName Thumbprint = $newAuthCertificateThumbprint Successful = $operationSuccessful ErrorOccurred = if ($Error.Count -gt $errorCount) { $($Error[0].Exception.Message) } } } function ConfigureNextAuthCertificate { [CmdletBinding()] [OutputType([System.Object])] param( [int]$CurrentAuthCertificateLifetimeInDays, [int]$EnableDaysInFuture = 30 ) <# We must generate a new self-signed certificate and set it as new certificate by the help of the -NewCertificateThumbprint parameter. We must also specify a DateTime (via -NewCertificateEffectiveDate) when the new certificate becomes active. Returns $true if renewal was successful, returns $false if it wasn't #> Write-Verbose "Calling: $($MyInvocation.MyCommand)" $renewalSuccessful = $false $newAuthCertificateObject = GenerateNewAuthCertificate $nextAuthCertificateActiveOn = (Get-Date).AddDays($EnableDaysInFuture) if ($null -ne $CurrentAuthCertificateLifetimeInDays) { Write-Verbose ("Current Auth Certificate will expire in: $($CurrentAuthCertificateLifetimeInDays) days") if ($CurrentAuthCertificateLifetimeInDays -lt ($EnableDaysInFuture + 2)) { Write-Verbose ("Need to re-calculate the EnableDaysInFuture value to ensure a smooth Auth Certificate rotation") # Assuming that there is not much time (< 2 days) until the current Auth Certificate expires, # the next Auth Certificate should become active as soon as the AuthAdmin servicelet runs on the server $EnableDaysInFuture = 0 if (($CurrentAuthCertificateLifetimeInDays - 4) -gt 0) { $EnableDaysInFuture = 4 } elseif (($CurrentAuthCertificateLifetimeInDays - 2) -gt 0) { $EnableDaysInFuture = 2 } $nextAuthCertificateActiveOn = (Get-Date).AddDays($EnableDaysInFuture) Write-Verbose ("The new Auth Certificate will become active in: $($EnableDaysInFuture) days") } else { Write-Verbose ("There is enough time to initiate the Auth Certificate rotation - no need to adjust EnableDaysInFuture") } } if (($null -ne $newAuthCertificateObject) -and ($newAuthCertificateObject.Successful)) { [string]$newAuthCertificateThumbprint = $newAuthCertificateObject.Thumbprint Write-Verbose ("New Auth Certificate with thumbprint: $($newAuthCertificateThumbprint) generated - the new one will replace the existing one in: $($EnableDaysInFuture) days") try { Write-Verbose ("[Required] Step 1: Set certificate: $($newAuthCertificateThumbprint) as the next Auth Certificate") if ($PSCmdlet.ShouldProcess("Certificate: $newAuthCertificateThumbprint Date: $nextAuthCertificateActiveOn", "Set-AuthConfig")) { Set-AuthConfig -NewCertificateThumbprint $newAuthCertificateThumbprint -NewCertificateEffectiveDate $nextAuthCertificateActiveOn -Force -ErrorAction Stop } if ($EnableDaysInFuture -eq 0) { # Restart MSExchangeServiceHost service to ensure that the new Auth Certificate is used immediately as don't have time # to wait until the AuthAdmin servicelet runs on the server due to the limited time until the current Auth Certificate expires Write-Verbose ("[Optional] Step 2: Restart service 'MSExchangeServiceHost' on computer: $($env:COMPUTERNAME)") Restart-Service -Name "MSExchangeServiceHost" -ErrorAction Stop } Write-Verbose ("Done - Certificate: $($newAuthCertificateThumbprint) set as the next Auth Certificate") Write-Verbose ("Effective date is: $($nextAuthCertificateActiveOn)") $renewalSuccessful = $true } catch { Write-Verbose ("Error while enabling the next Auth Certificate. Error: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction } } return [PSCustomObject]@{ RenewalSuccessful = $renewalSuccessful NextCertificateActiveOnDate = $nextAuthCertificateActiveOn NewCertificateThumbprint = $newAuthCertificateThumbprint } } function ReplaceExpiredAuthCertificate { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] [OutputType([System.Object])] param() <# We must generate a new self-signed certificate and replace the existing Auth Certificate if it's already expired. We must also set it as active by specifying the current DateTime via -NewCertificateEffectiveDate parameter. To speed things up, restarting 'MSExchangeServiceHost' service is needed as well as 'MSExchangeOWAAppPool' and 'MSExchangeECPAppPool' app pools. However, it shouldn't become a problem if restarting the service or app pools fails. Returns $true if renewal was successful, returns $false if it wasn't #> Write-Verbose "Calling: $($MyInvocation.MyCommand)" $newAuthCertificateActiveOn = (Get-Date) $renewalSuccessful = $false $newAuthCertificateObject = GenerateNewAuthCertificate if (($null -ne $newAuthCertificateObject) -and ($newAuthCertificateObject.Successful)) { [string]$newAuthCertificateThumbprint = $newAuthCertificateObject.Thumbprint Write-Verbose ("New Auth Certificate with thumbprint: $($newAuthCertificateThumbprint) generated - the existing one will be replaced immediately with the new one") try { Write-Verbose ("[Required] Step 1: Set certificate: $($newAuthCertificateThumbprint) as new Auth Certificate") if ($PSCmdlet.ShouldProcess("Certificate: $newAuthCertificateThumbprint Date: $newAuthCertificateActiveOn", "Set-AuthConfig")) { Set-AuthConfig -NewCertificateThumbprint $newAuthCertificateThumbprint -NewCertificateEffectiveDate $newAuthCertificateActiveOn -Force -ErrorAction Stop } Write-Verbose ("[Required] Step 2: Publish the new Auth Certificate") if ($PSCmdlet.ShouldProcess("PublishCertificate", "Set-AuthConfig")) { Set-AuthConfig -PublishCertificate -ErrorAction Stop } Write-Verbose ("[Required] Step 3: Clear previous Auth Certificate") if ($PSCmdlet.ShouldProcess("ClearPreviousCertificate", "Set-AuthConfig")) { Set-AuthConfig -ClearPreviousCertificate -ErrorAction Stop } try { # Run these commands in a separate try / catch as it isn't a terminating issue if they fail Write-Verbose ("[Optional] Step 4: Restart service 'MSExchangeServiceHost' on computer: $($env:COMPUTERNAME)") Restart-Service -Name "MSExchangeServiceHost" -ErrorAction Stop if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Restart-WebAppPool")) { Write-Verbose ("[Optional] Step 5: Restart WebApp Pools 'MSExchangeOWAAppPool' and 'MSExchangeECPAppPool' on computer $($env:COMPUTERNAME)") Restart-WebAppPool -Name "MSExchangeOWAAppPool" -ErrorAction Stop Restart-WebAppPool -Name "MSExchangeECPAppPool" -ErrorAction Stop } } catch { Write-Warning ("Error while restarting service 'MSExchangeServiceHost' or WebApp Pools") Write-Warning ("However, these steps are optional and not required - the Auth Certificate was replaced with a new one") Invoke-CatchActionError $CatchActionFunction } Write-Verbose ("Done - Certificate: $($newAuthCertificateThumbprint) is the new Auth Certificate") $renewalSuccessful = $true } catch { Write-Verbose ("Error while enabling the new Auth Certificate - Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction } } return [PSCustomObject]@{ RenewalSuccessful = $renewalSuccessful NextCertificateActiveOnDate = $newAuthCertificateActiveOn NewCertificateThumbprint = $newAuthCertificateThumbprint } } } process { if ($ReplaceExpiredAuthCertificate) { Write-Verbose ("Calling function to replace an already expired or invalid Auth Certificate") $renewalActionPerformed = ReplaceExpiredAuthCertificate } elseif ($ConfigureNextAuthCertificate) { Write-Verbose ("Calling function to state the next Auth Certificate for rotation") $renewalActionPerformed = ConfigureNextAuthCertificate -CurrentAuthCertificateLifetimeInDays $CurrentAuthCertificateLifetimeInDays } else { Write-Verbose ("No Auth Certificate configuration action was specified") } } end { return [PSCustomObject]@{ RenewalActionPerformed = ($renewalActionPerformed.RenewalSuccessful -eq $true) AuthCertificateActivationDate = ($renewalActionPerformed.NextCertificateActiveOnDate) NewCertificateThumbprint = ($renewalActionPerformed.NewCertificateThumbprint) } } } function Register-AuthCertificateRenewalTask { [CmdletBinding(SupportsShouldProcess)] [OutputType([bool])] param( [string]$TaskName = "Daily Auth Certificate Check", [string]$Username, [SecureString]$Password, [string]$WorkingDirectory, [string]$ScriptName, [bool]$IgnoreOfflineServers = $false, [bool]$IgnoreHybridConfig = $false, [ValidatePattern("^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")] [string[]]$SendEmailNotificationTo, [switch]$TrustAllCertificates, [string]$DailyRuntime = "10am", [string]$TaskDescription = "AutoGeneratedViaMonitorExchangeAuthCertificateScript", [ScriptBlock]$CatchActionFunction ) Write-Verbose "Calling: $($MyInvocation.MyCommand)" $fullPathToScript = [System.IO.Path]::Combine($WorkingDirectory, $ScriptName) try { $existingScheduledTask = Get-ScheduledTask -TaskName $($TaskName) -ErrorAction Stop | Where-Object { ($_.Description -eq $TaskDescription) } } catch { Write-Verbose ("No scheduled task with name: $($TaskName) was found - we don't need to unregister it") Invoke-CatchActionError $CatchActionFunction } if ($null -ne $existingScheduledTask) { Write-Verbose ("Scheduled task already exists - will be deleted now to re-create a new one") try { foreach ($t in $existingScheduledTask) { if ($PSCmdlet.ShouldProcess($t.TaskName, "Unregister-ScheduledTask")) { Unregister-ScheduledTask -TaskPath $($t.TaskPath) -TaskName $($t.TaskName) -Confirm:$false -ErrorAction Stop } Write-Verbose ("Scheduled task: $($t.TaskName) successfully unregistered") } } catch { Write-Verbose ("The scheduled task already exists and we were unable to unregister it - Exception $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction return $false } } if (($WhatIfPreference) -or (Test-Path -Path $fullPathToScript)) { $passwordAsPlaintextString = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)) if ($PSCmdlet.ShouldProcess($DailyRuntime, "New-ScheduledTaskTrigger")) { $schTaskTrigger = New-ScheduledTaskTrigger -Daily -At $DailyRuntime } $newScheduledTaskParams = @{ Execute = "powershell.exe" WorkingDirectory = "$($WorkingDirectory)" } $basicArgumentParameters = "-NonInteractive -NoLogo -NoProfile -Command `".\$($ScriptName) -ValidateAndRenewAuthCertificate `$true -IgnoreUnreachableServers `$$($IgnoreOfflineServers) -IgnoreHybridConfig `$$($IgnoreHybridConfig)" if ($null -ne $SendEmailNotificationTo) { if ($TrustAllCertificates) { $newScheduledTaskParams.Add("Argument", "$($basicArgumentParameters) -SendEmailNotificationTo $([string]::Join(", ", $SendEmailNotificationTo)) -TrustAllCertificates -Confirm:`$false`"") } else { $newScheduledTaskParams.Add("Argument", "$($basicArgumentParameters) -SendEmailNotificationTo $([string]::Join(", ", $SendEmailNotificationTo)) -Confirm:`$false`"") } } else { $newScheduledTaskParams.Add("Argument", "$($basicArgumentParameters) -Confirm:`$false`"") } if ($PSCmdlet.ShouldProcess($TaskName, "New-ScheduledTaskAction")) { $schTaskAction = New-ScheduledTaskAction @newScheduledTaskParams $registerSchTaskParams = @{ TaskName = $TaskName Trigger = $schTaskTrigger Action = $schTaskAction Description = $TaskDescription RunLevel = "Highest" User = $Username Password = $passwordAsPlaintextString Force = $true ErrorAction = "Stop" } } try { Write-Verbose ("Scheduled Task: $($TaskName) successfully created") if ($PScmdlet.ShouldProcess($TaskName, "Register-ScheduledTask")) { Register-ScheduledTask @registerSchTaskParams | Out-Null } return $true } catch { Write-Verbose ("Error while creating Scheduled Task: $($TaskName) - Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction } } else { Write-Verbose ("Script: $($fullPathToScript) doesn't exist") } return $false } function Get-ExchangeAuthCertificateStatus { [CmdletBinding()] [OutputType([System.Object])] param( [bool]$IgnoreUnreachableServers = $false, [bool]$IgnoreHybridSetup = $false, [ScriptBlock]$CatchActionFunction ) <# Returns an object which contains information if the current Auth Certificate and/or the next Auth Certificate must be renewed. The object contains the following properties: - CurrentAuthCertificateLifetimeInDays - ReplaceRequired - ConfigureNextAuthRequired - NumberOfUnreachableServers - UnreachableServerList - HybridSetupDetected - StopProcessingDueToHybrid - MultipleExchangeADSites #> begin { Write-Verbose "Calling: $($MyInvocation.MyCommand)" $replaceRequired = $false $importCurrentAuthCertificateRequired = $false $configureNextAuthRequired = $false $importNextAuthCertificateRequired = $false $currentAuthCertificateValidInDays = 0 $nextAuthCertificateValidInDays = 0 $exchangeServersUnreachableList = New-Object 'System.Collections.Generic.List[string]' $exchangeServersReachableList = New-Object 'System.Collections.Generic.List[string]' $currentAuthCertificateFoundOnServersList = New-Object 'System.Collections.Generic.List[string]' $nextAuthCertificateFoundOnServersList = New-Object 'System.Collections.Generic.List[string]' $currentAuthCertificateMissingOnServersList = New-Object 'System.Collections.Generic.List[string]' $nextAuthCertificateMissingOnServersList = New-Object 'System.Collections.Generic.List[string]' } process { $authConfiguration = Get-AuthConfig -ErrorAction SilentlyContinue $allMailboxServers = Get-ExchangeServer | Where-Object { ((($_.IsMailboxServer) -or ($_.IsClientAccessServer)) -and ($_.AdminDisplayVersion -Match "^Version 15")) } $multipleExchangeSites = (($allMailboxServers.Site.Name | Sort-Object -Unique).Count -gt 1) Write-Verbose ("Exchange deployed to multiple AD sites? $($multipleExchangeSites)") try { $hybridConfiguration = Get-HybridConfiguration -ErrorAction Stop } catch { Write-Verbose ("We hit an exception while querying the Exchange Hybrid configuration state - Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction } if ($null -ne $authConfiguration) { Write-Verbose ("AuthConfig returned via 'Get-AuthConfig' call") if (-not([string]::IsNullOrEmpty($authConfiguration.CurrentCertificateThumbprint))) { Write-Verbose ("CurrentCertificateThumbprint is: $($authConfiguration.CurrentCertificateThumbprint)") foreach ($mbxServer in $allMailboxServers) { try { Write-Verbose ("Trying to query current Auth Certificate on server: $($mbxServer)") $currentAuthCertificate = Get-ExchangeServerCertificate -Server $($mbxServer.Fqdn) -Thumbprint $authConfiguration.CurrentCertificateThumbprint -ErrorAction Stop $exchangeServersReachableList.Add($mbxServer.Fqdn) $currentAuthCertificateFoundOnServersList.Add($mbxServer.Fqdn) } catch { Write-Verbose ("We hit an exception - going to determine the reason") Invoke-CatchActionError $CatchActionFunction if ((($error[0].CategoryInfo).Reason) -eq "InvalidOperationException") { # Auth Certificate must exist on all servers, if it doesn't, generate a new one and replace the existing one Write-Verbose ("Current Auth Certificate not found on server: $($mbxServer)") $exchangeServersReachableList.Add($mbxServer.Fqdn) $currentAuthCertificateMissingOnServersList.Add($mbxServer.Fqdn) } else { Write-Verbose ("Computer: $($mbxServer.Fqdn) is unreachable and cannot take into account") $exchangeServersUnreachableList.Add($mbxServer.Fqdn) } } } } if (-not([string]::IsNullOrEmpty($authConfiguration.NextCertificateThumbprint))) { Write-Verbose ("NextCertificateThumbprint is: $($authConfiguration.NextCertificateThumbprint)") foreach ($mbxServer in $exchangeServersReachableList) { try { Write-Verbose ("Trying to query next Auth Certificate on server: $($mbxServer)") $nextAuthCertificate = Get-ExchangeServerCertificate -Server $mbxServer -Thumbprint $authConfiguration.NextCertificateThumbprint -ErrorAction Stop $nextAuthCertificateFoundOnServersList.Add($mbxServer) } catch { Invoke-CatchActionError $CatchActionFunction if ((($error[0].CategoryInfo).Reason) -eq "InvalidOperationException") { # Next Auth Certificate must exist on all servers, if it doesn't, generate a new one and replace the existing Write-Verbose ("Next Auth Certificate not found on server: $($mbxServer)") $nextAuthCertificateMissingOnServersList.Add($mbxServer) } else { Write-Verbose ("Exception reason is: $(($error[0].CategoryInfo).Reason)") Write-Verbose ("Do nothing as we can't say for sure if the Auth Certificate exists or not") } } } } Write-Verbose ("Number of unreachable servers: $($exchangeServersUnreachableList.Count) - IgnoreUnreachableServers? $($IgnoreUnreachableServers)") if (($exchangeServersUnreachableList.Count -eq 0) -or (($exchangeServersUnreachableList.Count -gt 0) -and ($IgnoreUnreachableServers))) { if ($exchangeServersReachableList.Count -gt $currentAuthCertificateMissingOnServersList.Count) { $currentAuthCertificateValidInDays = (($currentAuthCertificate.NotAfter) - (Get-Date)).Days } if ($exchangeServersReachableList.Count -gt $nextAuthCertificateMissingOnServersList.Count) { $nextAuthCertificateValidInDays = (($nextAuthCertificate.NotAfter) - (Get-Date)).Days } if (($currentAuthCertificateValidInDays -lt 0) -and ($nextAuthCertificateValidInDays -lt 0)) { # Scenario 1: Current Auth Certificate has expired and no next Auth Certificate defined or the next Auth Certificate has expired $replaceRequired = $true } elseif ((($currentAuthCertificateValidInDays -ge 0) -and ($currentAuthCertificateValidInDays -le 60)) -and (($nextAuthCertificateValidInDays -le 0) -or ($nextAuthCertificateValidInDays -le 120)) -and ($currentAuthCertificateMissingOnServersList.Count -eq 0) -and ($nextAuthCertificateMissingOnServersList.Count -eq 0)) { # Scenario 2: Current Auth Certificate is valid but no next Auth Certificate defined or next Auth Certificate will expire in < 120 days $configureNextAuthRequired = $true } elseif (($currentAuthCertificateValidInDays -le 0) -and ($nextAuthCertificateValidInDays -ge 0)) { # Scenario 3: Unlikely but possible - current Auth Certificate has expired and next Auth Certificate is set but not yet active $replaceRequired = $true } else { if ($currentAuthCertificateMissingOnServersList.Count -gt 0) { # Scenario 4: Current Auth Certificate is missing on at least one (1) mailbox or CAS server $importCurrentAuthCertificateRequired = $true } if ($nextAuthCertificateMissingOnServersList.Count -gt 0) { # Scenario 5: Next Auth Certificate is missing on at least one (1) mailbox or CAS server $importNextAuthCertificateRequired = $true } } $stopProcessingDueToHybrid = ((($null -ne $hybridConfiguration) -and ($IgnoreHybridSetup -eq $false)) -and (($replaceRequired) -or ($configureNextAuthRequired))) Write-Verbose ("Replace of the primary Auth Certificate required? $($replaceRequired)") Write-Verbose ("Import of the primary Auth Certificate required? $($importCurrentAuthCertificateRequired)") Write-Verbose ("Replace of the next Auth Certificate required? $($configureNextAuthRequired)") Write-Verbose ("Import of the next Auth Certificate required? $($importNextAuthCertificateRequired)") Write-Verbose ("Hybrid Configuration detected? $($null -ne $hybridConfiguration)") Write-Verbose ("Stop processing due to hybrid? $($stopProcessingDueToHybrid)") } else { Write-Verbose ("Unable to reach the following Exchange Servers: $([string]::Join(", ", $exchangeServersUnreachableList))") Write-Verbose ("No renewal action will be performed as we can't for sure validate the Auth Certificate state on the offline servers") } } else { Write-Verbose ("Unable to query AuthConfig - therefore no action will be executed") } } end { return [PSCustomObject]@{ CurrentAuthCertificateThumbprint = $authConfiguration.CurrentCertificateThumbprint CurrentAuthCertificateLifetimeInDays = $currentAuthCertificateValidInDays ReplaceRequired = $replaceRequired CurrentAuthCertificateImportRequired = $importCurrentAuthCertificateRequired NextAuthCertificateThumbprint = $authConfiguration.NextCertificateThumbprint NextAuthCertificateLifetimeInDays = $nextAuthCertificateValidInDays ConfigureNextAuthRequired = $configureNextAuthRequired NextAuthCertificateImportRequired = $importNextAuthCertificateRequired NumberOfUnreachableServers = $exchangeServersUnreachableList.Count UnreachableServersList = $exchangeServersUnreachableList AuthCertificateFoundOnServers = $currentAuthCertificateFoundOnServersList AuthCertificateMissingOnServers = $currentAuthCertificateMissingOnServersList NextAuthCertificateFoundOnServers = $nextAuthCertificateFoundOnServersList NextAuthCertificateMissingOnServers = $nextAuthCertificateMissingOnServersList HybridSetupDetected = ($null -ne $hybridConfiguration) StopProcessingDueToHybrid = $stopProcessingDueToHybrid MultipleExchangeADSites = $multipleExchangeSites } } } function Test-IsServerValidForAuthCertificateGeneration { [CmdletBinding()] [OutputType([bool])] param( [string]$ComputerName = $env:COMPUTERNAME, [ScriptBlock]$CatchActionFunction ) <# Validates that the server on which the script runs is a mailbox server running Exchange major version 15 or greater #> try { Write-Verbose "Calling: $($MyInvocation.MyCommand)" $isValid = $false Write-Verbose ("Trying to query Exchange Server details") $exchangeServerDetails = Get-ExchangeServer -Identity $ComputerName -ErrorAction Stop if (($exchangeServerDetails.IsMailboxServer) -and (($exchangeServerDetails.AdminDisplayVersion -Match "^Version 15"))) { Write-Verbose ("Exchange Server role and version is VALID to renew the Auth Certificate") $isValid = $true } else { Write-Verbose ("Exchange Server role or version is INVALID to renew the Auth Certificate") } } catch { Write-Verbose ("Unable to query Exchange Server details - Exception: $($Error[0].Exception.Message)") Invoke-CatchActionError $CatchActionFunction } return $isValid } function Write-DebugLog($Message) { $Script:Logger = $Script:Logger | Write-LoggerInstance $Message } function Main { param() if (-not(Confirm-Administrator)) { Write-Warning ("The script needs to be executed in elevated mode. Start the Exchange Management Shell as an Administrator.") $Error.Clear() Start-Sleep -Seconds 2 exit } Invoke-ErrorMonitoring $versionsUrl = "https://aka.ms/MEAC-VersionsUrl" Write-Host ("Monitor Exchange Auth Certificate script version $($BuildVersion)") -ForegroundColor Green $currentErrors = $Error.Count if ($ScriptUpdateOnly) { switch (Test-ScriptVersion -AutoUpdate -VersionsUrl $versionsUrl -Confirm:$false) { ($true) { Write-Host ("Script was successfully updated") -ForegroundColor Green } ($false) { Write-Host ("No update of the script performed") -ForegroundColor Yellow } default { Write-Host ("Unable to perform ScriptUpdateOnly operation") -ForegroundColor Red } } return } if ((-not($SkipVersionCheck)) -and (Test-ScriptVersion -AutoUpdate -VersionsUrl $versionsUrl -Confirm:$false)) { Write-Host ("Script was updated. Please rerun the command") -ForegroundColor Yellow return } Invoke-ErrorCatchActionLoopFromIndex $currentErrors if ($PrepareADForAutomationOnly) { Write-Host ("Mode: Prepare AD account to run the script as scheduled task") $newAuthCertificateParamsAccountOnly = @{ Password = $Password DomainToUse = $ADAccountDomain CatchActionFunction = ${Function:Invoke-CatchActions} WhatIf = $WhatIfPreference } $adAccountSuccessfullyCreated = New-AuthCertificateManagementAccount @newAuthCertificateParamsAccountOnly if ($adAccountSuccessfullyCreated) { Write-Host ("Account: 'SM_ad0b1fe3a1a3' successfully created - please run the script as follows:") -ForegroundColor Green Write-Host "" Write-Host (".\MonitorExchangeAuthCertificate.ps1 -ConfigureScriptToRunViaScheduledTask -AutomationAccountCredential (Get-Credential)") -ForegroundColor Green } else { Write-Host ("Unable to prepare the Auth Certificate automation account - please check the verbose script log for more details") -ForegroundColor Yellow } return } $exchangeShell = Confirm-ExchangeShell -CatchActionFunction ${Function:Invoke-CatchActions} $exitScriptDueToShellRequirementsNotFullFilled = $false if (-not($exchangeShell.ShellLoaded)) { Write-Warning ("Unable to load Exchange Management Shell") $exitScriptDueToShellRequirementsNotFullFilled = $true } else { if ($exchangeShell.ToolsOnly) { Write-Warning ("The script must be run on an Exchange server") $exitScriptDueToShellRequirementsNotFullFilled = $true } if ($exchangeShell.EdgeServer) { Write-Warning ("The script cannot be run on an Edge Transport server") $exitScriptDueToShellRequirementsNotFullFilled = $true } if ($exchangeShell.RemoteShell) { Write-Warning ("Running the script via Remote Shell is not supported") $exitScriptDueToShellRequirementsNotFullFilled = $true } if ($exchangeShell.Major -lt 15) { Write-Warning ("The script must be run on Exchange 2013 or higher") $exitScriptDueToShellRequirementsNotFullFilled = $true } } if ($exitScriptDueToShellRequirementsNotFullFilled) { $Error.Clear() Start-Sleep -Seconds 2 exit } Set-ADServerSettings -ViewEntireForest $true $localServerFqdn = (([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME)).HostName).ToLower() if ($ExportAuthCertificatesAsPfx) { Write-Host ("Mode: Export all Exchange Auth Certificates available on this system") if ((Test-IsServerValidForAuthCertificateGeneration -CatchActionFunction ${Function:Invoke-CatchActions}) -eq $false) { Write-Host ("This server does not meet the requirements to run the script.") -ForegroundColor Yellow return } $authCertificateExportParams = @{ Password = $Password CatchActionFunction = ${Function:Invoke-CatchActions} WhatIf = $WhatIfPreference } $authCertificateExportStatusObject = Export-ExchangeAuthCertificate @authCertificateExportParams if ($authCertificateExportStatusObject.CertificatesAvailableToExport) { Write-Host ("There is/are $($authCertificateExportStatusObject.NumberOfCertificatesToExport) certificate(s) that could be exported") if ($authCertificateExportStatusObject.ExportSuccessful) { Write-Host ("All of them were successfully exported to the following directory: $($PSScriptRoot)") -ForegroundColor Green } else { Write-Host ("Some of the certificates couldn't be exported - please check the verbose log") -ForegroundColor Yellow Write-Host ("Thumbprints of the certificates that couldn't be exported:") -ForegroundColor Yellow Write-Host ("$([string]::Join(", ", $authCertificateExportStatusObject.UnableToExportCertificatesList))") -ForegroundColor Yellow } } else { Write-Host ("There are no Auth Certificates on the system that are available to export") } return } if ($TestEmailNotification) { Write-Host ("Mode: Test email notification feature") $sendEmailNotificationTestParams = @{ To = $SendEmailNotificationTo Subject = "[Test] An Exchange Auth Certificate maintenance action was performed" Importance = "Low" Body = "This is a test message sent by the MonitorExchangeAuthCertificate.ps1 script.
No action is required!" EwsServiceUrl = (Get-WebServicesVirtualDirectory -Server $env:COMPUTERNAME -ADPropertiesOnly).InternalUrl.AbsoluteUri BodyAsHtml = $true CatchActionFunction = ${Function:Invoke-CatchActions} } if ($TrustAllCertificates) { $sendEmailNotificationTestParams.Add("IgnoreCertificateMismatch", $true) } # Check for the last value as Send-EwsMailMessage returns the SendAndSaveCopy() result too (not sure how to suppress this yet) if (Send-EwsMailMessage @sendEmailNotificationTestParams) { Write-Host ("Please check if the test message was received by the following recipient(s): $($SendEmailNotificationTo)") } else { Write-Host ("We hit an exception while processing your test email message. Please check the log file") -ForegroundColor Yellow Write-Host ("`n$($Error[0].Exception.Message)") -ForegroundColor Red } return } if ($ConfigureScriptToRunViaScheduledTask) { Write-Host ("Mode: Configure monitoring script to run via scheduled task") if ((Test-IsServerValidForAuthCertificateGeneration -CatchActionFunction ${Function:Invoke-CatchActions}) -eq $false) { Write-Host ("This server does not meet the requirements to run the script.") -ForegroundColor Yellow return } try { try { $dcToUseAsConfigDC = (Get-ExchangeServer -Identity $env:COMPUTERNAME -Status -ErrorAction Stop).CurrentConfigDomainController } catch { $dcToUseAsConfigDC = Get-GlobalCatalogServer -CatchActionFunction ${Function:Invoke-CatchActions} } Write-Host ("We use the following Domain Controller: $($dcToUseAsConfigDC)") $buildExchangeAuthManagementAccountParams = @{ DomainController = $dcToUseAsConfigDC CatchActionFunction = ${Function:Invoke-CatchActions} WhatIf = $WhatIfPreference } if ($null -ne $AutomationAccountCredential) { $buildExchangeAuthManagementAccountParams.Add("UseExistingAccount", $true) $buildExchangeAuthManagementAccountParams.Add("AccountCredentialObject", $AutomationAccountCredential) } elseif ($null -ne $Password) { $buildExchangeAuthManagementAccountParams.Add("PasswordToSet", $Password) } else { Write-Host ("Please provide a password for the automation account") -ForegroundColor Yellow Write-Host ("You can do so by using the '-Password' parameter or by using the '-AutomationAccountCredential' parameter") -ForegroundColor Yellow return } $adAccountInfo = Build-ExchangeAuthCertificateManagementAccount @buildExchangeAuthManagementAccountParams if ($null -ne $adAccountInfo) { Write-Host ("Account for automation was successfully created: $($adAccountInfo.UserPrincipalName)") $Username = $adAccountInfo.UserPrincipalName $Password = $adAccountInfo.Password $scriptInfo = Copy-ScriptToExchangeDirectory -CatchActionFunction ${Function:Invoke-CatchActions} -WhatIf:$WhatIfPreference if ($null -ne $scriptInfo) { Write-Host ("Script: $($scriptInfo.ScriptName) was successfully copied over to: $($scriptInfo.WorkingDirectory)") $registerSchTaskParams = @{ Username = $Username Password = $Password WorkingDirectory = $scriptInfo.WorkingDirectory ScriptName = $scriptInfo.ScriptName IgnoreOfflineServers = $IgnoreUnreachableServers IgnoreHybridConfig = $IgnoreHybridConfig CatchActionFunction = ${Function:Invoke-CatchActions} WhatIf = $WhatIfPreference } if ($null -ne $SendEmailNotificationTo) { Write-Host ("We're trying to notify the following recipient(s): $($SendEmailNotificationTo)") $registerSchTaskParams.Add("SendEmailNotificationTo", $SendEmailNotificationTo) if ($TrustAllCertificates) { Write-Host ("We trust all certificates when connecting to EWS service") $registerSchTaskParams.Add("TrustAllCertificates", $true) } } $schTaskResults = Register-AuthCertificateRenewalTask @registerSchTaskParams } else { Write-Host ("We couldn't copy the script: $($scriptInfo.ScriptName) to: $($scriptInfo.WorkingDirectory)") -ForegroundColor Red } } else { Write-Host ("Something went wrong while preparing the automation account") -ForegroundColor Red } if ($schTaskResults) { Write-Host ("The scheduled task was created successfully") -ForegroundColor Green } else { Write-Host ("The scheduled task wasn't created - please check the verbose script log for more details") -ForegroundColor Red } } catch { Write-Verbose ("Exception: $($Error[0].Exception.Message)") } return } if ($ValidateAndRenewAuthCertificate) { Write-Host ("Mode: Testing and replacing or importing the Auth Certificate (if required)") } else { Write-Host ("The script was run without parameter therefore, only a check of the Auth Certificate configuration is performed and no change will be made") } if ((Test-IsServerValidForAuthCertificateGeneration -CatchActionFunction ${Function:Invoke-CatchActions}) -eq $false) { Write-Host ("This server does not meet the requirements to run the script.") -ForegroundColor Yellow return } if ($null -ne $SendEmailNotificationTo) { $sendEmailNotificationParams = @{ To = $SendEmailNotificationTo Subject = "[Action required] An Exchange Auth Certificate maintenance action was performed" Importance = "High" EwsServiceUrl = (Get-WebServicesVirtualDirectory -Server $env:COMPUTERNAME -ADPropertiesOnly).InternalUrl.AbsoluteUri BodyAsHtml = $true CatchActionFunction = ${Function:Invoke-CatchActions} } $emailBodyBase = "On $(Get-Date) we performed an Exchange Auth Certificate maintenance action.
" + "Due to your Exchange Server or organization configuration, manual actions may be required.

" } $authCertificateStatusParams = @{ IgnoreUnreachableServers = $IgnoreUnreachableServers IgnoreHybridSetup = $IgnoreHybridConfig CatchActionFunction = ${Function:Invoke-CatchActions} } $authCertStatus = Get-ExchangeAuthCertificateStatus @authCertificateStatusParams $noRenewalDueToUnreachableServers = (($authCertStatus.NumberOfUnreachableServers -gt 0) -and ($IgnoreUnreachableServers -eq $false)) $stopProcessingDueToHybrid = $authCertStatus.StopProcessingDueToHybrid $renewalActionRequired = (($authCertStatus.ReplaceRequired) -or ($authCertStatus.ConfigureNextAuthRequired) -or ($authCertStatus.CurrentAuthCertificateImportRequired) -or ($authCertStatus.NextAuthCertificateImportRequired)) if ($authCertStatus.ReplaceRequired) { $renewalActionWording = "The Auth Certificate in use must be replaced by a new one." } elseif ($authCertStatus.ConfigureNextAuthRequired) { $renewalActionWording = "The Auth Certificate configured as next Auth Certificate must be configured or replaced by a new one." } elseif (($authCertStatus.CurrentAuthCertificateImportRequired) -or ($authCertStatus.NextAuthCertificateImportRequired)) { $renewalActionWording = "The current or next Auth Certificate is missing on some servers and must be imported." } else { $renewalActionWording = "No renewal action is required" } if ($noRenewalDueToUnreachableServers) { Write-Host ("We couldn't validate if the Auth Certificate is properly configured because $($authCertStatus.NumberOfUnreachableServers) servers were unreachable.") -ForegroundColor Yellow Write-Host ("The unreachable servers are: $([string]::Join(", ", $authCertStatus.UnreachableServersList))") -ForegroundColor Yellow } elseif ($stopProcessingDueToHybrid) { Write-Host ("We have not made any configuration change because Exchange Hybrid has been detected in your environment.") -ForegroundColor Yellow Write-Host ("Please rerun the script using the '-IgnoreHybridConfig `$true' parameter to perform the renewal action.") -ForegroundColor Yellow Write-Host ("It's also required to run the Hybrid Configuration Wizard (HCW) after the primary Auth Certificate was replaced.") -ForegroundColor Yellow } else { if (($ValidateAndRenewAuthCertificate) -and ($renewalActionRequired)) { Write-Host ("Renewal scenario: $($renewalActionWording)") if ($authCertStatus.ReplaceRequired) { $replaceExpiredAuthCertificateParams = @{ ReplaceExpiredAuthCertificate = $true CatchActionFunction = ${Function:Invoke-CatchActions} WhatIf = $WhatIfPreference } $renewalActionResult = New-ExchangeAuthCertificate @replaceExpiredAuthCertificateParams $emailBodyRenewalScenario = "The Auth Certificate in use was invalid (expired) or not available on all Exchange Servers within your organization.
" + "It was immediately replaced by a new one which is already active.

" } elseif ($authCertStatus.ConfigureNextAuthRequired) { $configureNextAuthCertificateParams = @{ ConfigureNextAuthCertificate = $true CurrentAuthCertificateLifetimeInDays = $authCertStatus.CurrentAuthCertificateLifetimeInDays CatchActionFunction = ${Function:Invoke-CatchActions} WhatIf = $WhatIfPreference } $renewalActionResult = New-ExchangeAuthCertificate @configureNextAuthCertificateParams $emailBodyRenewalScenario = "The new Auth Certificate will replace the current one on: $($renewalActionResult.AuthCertificateActivationDate), " + "as soon as the AuthAdmin servicelet runs the next time (from the mentioned date within 12 hours).

" } elseif (($authCertStatus.CurrentAuthCertificateImportRequired) -or ($authCertStatus.NextAuthCertificateImportRequired)) { if ($authCertStatus.CurrentAuthCertificateImportRequired) { $importCurrentAuthCertificateParams = @{ Thumbprint = $authCertStatus.CurrentAuthCertificateThumbprint ServersToImportList = $authCertStatus.AuthCertificateMissingOnServers CatchActionFunction = ${Function:Invoke-CatchActions} WhatIf = $WhatIfPreference } if ($authCertStatus.AuthCertificateMissingOnServers.ToLower().Contains($localServerFqdn)) { Write-Verbose ("Current Auth Certificate can't be exported from the local system - must be exported from another server") $importCurrentAuthCertificateParams.Add("ExportFromServer", $authCertStatus.AuthCertificateFoundOnServers[0]) } $importCurrentAuthCertificateResults = Import-ExchangeAuthCertificateToServers @importCurrentAuthCertificateParams $emailBodyImportCurrentAuthCertificateResult = "The current Auth Certificate is valid but was missing on some servers.
" + "It was imported to the following server(s): $([string]::Join(", ", $importCurrentAuthCertificateResults.ImportedToServersList))

" if ($importCurrentAuthCertificateResults.ImportToServersFailedList.Count -gt 0) { $emailBodyImportCurrentAuthCertificateResult += "We failed to import it to the following servers: $([string]::Join(", ", $importCurrentAuthCertificateResults.ImportToServersFailedList))
" + "Please export the Auth Certificate manually and import it on these Exchange server(s).

" } } if ($authCertStatus.NextAuthCertificateImportRequired) { $importNextAuthCertificateParams = @{ Thumbprint = $authCertStatus.NextAuthCertificateThumbprint ServersToImportList = $authCertStatus.NextAuthCertificateMissingOnServers CatchActionFunction = ${Function:Invoke-CatchActions} WhatIf = $WhatIfPreference } if ($authCertStatus.NextAuthCertificateMissingOnServers.ToLower().Contains($localServerFqdn)) { Write-Verbose ("Next Auth Certificate can't be exported from the local system - must be exported from another server") $importNextAuthCertificateParams.Add("ExportFromServer", $authCertStatus.NextAuthCertificateFoundOnServers[0]) } $importNextAuthCertificateResults = Import-ExchangeAuthCertificateToServers @importNextAuthCertificateParams $emailBodyImportNextAuthCertificateResult = "The next Auth Certificate is valid but was missing on some servers.
" + "It was imported to the following server(s): $([string]::Join(", ", $importNextAuthCertificateResults.ImportedToServersList))

" if ($importNextAuthCertificateResults.ImportToServersFailedList.Count -gt 0) { $emailBodyImportNextAuthCertificateResult += "We failed to import it to the following servers: $([string]::Join(", ", $importNextAuthCertificateResults.ImportToServersFailedList))
" + "Please export the next Auth Certificate manually and import it on these Exchange server(s).

" } } } if ($authCertStatus.HybridSetupDetected) { $emailBodyHybrid = "Please ensure to run the Hybrid Configuration Wizard (HCW) as soon as the new Auth Certificate replaces the active one." } if ($renewalActionResult.RenewalActionPerformed) { $emailBodyRenewalAction = "New Exchange Auth Certificate thumbprint: $($renewalActionResult.NewCertificateThumbprint)
" + $emailBodyRenewalScenario } if ($authCertStatus.MultipleExchangeADSites) { $emailBodyMultiADSites = "Please validate that the newly created Auth Certificate was successfully replicated to all Exchange Servers (except Edge Transport) " + "which are located in another Active-Directory site.
" + "You can do so by running the following command against one Exchange Server per AD site:

" + "Get-ExchangeCertificate -Server 'ServerName' -Thumbprint $($renewalActionResult.NewCertificateThumbprint)

" + "If you run the script again, it will try to import the newly created certificate to all servers where it's missing.

" + "However, if you still find that the Auth Certificate is missing on a server in a different AD site, please follow these steps:

" + "1. Export the Auth Certificate: .\MonitorExchangeAuthCertificate.ps1 -ExportAuthCertificatesAsPfx
" + "2. Import it to the Computer Accounts 'Personal' certificate store on an Exchange Server per other AD site

" + "The Auth Certificate will then be automatically replicated to all Exchange Servers within this AD site.

" } $emailBodyFailure = "We ran into an issue while trying to renew the Exchange Auth Certificate. Please check the verbose script log for more details.
" + "You can find it under: '$($Script:Logger.FullPath)' on computer: $($env:COMPUTERNAME)" if (($renewalActionResult.RenewalActionPerformed) -and ($authCertStatus.HybridSetupDetected -eq $false)) { if ($null -ne $emailBodyBase) { if ($authCertStatus.MultipleExchangeADSites) { $finalEmailBody = $emailBodyBase + $emailBodyRenewalAction + $emailBodyMultiADSites } else { $finalEmailBody = $emailBodyBase + $emailBodyRenewalAction + "No further action is required on your part." } } Write-Host ("") Write-Host ("The renewal action was successfully performed") -ForegroundColor Green } elseif (($renewalActionResult.RenewalActionPerformed) -and ($authCertStatus.HybridSetupDetected)) { if ($null -ne $emailBodyBase) { if ($authCertStatus.MultipleExchangeADSites) { $finalEmailBody = $emailBodyBase + $emailBodyRenewalAction + $emailBodyMultiADSites + $emailBodyHybrid } else { $finalEmailBody = $emailBodyBase + $emailBodyRenewalAction + $emailBodyHybrid } } Write-Host ("") Write-Host ("The renewal action was successfully performed - the new Auth Certificate will become active on: $($renewalActionResult.AuthCertificateActivationDate)") -ForegroundColor Green Write-Host ("Please ensure to run the Hybrid Configuration Wizard (HCW) as soon as the new Auth Certificate becomes active.") -ForegroundColor Green } elseif (($null -ne $importCurrentAuthCertificateResults) -or ($null -ne $importNextAuthCertificateResults)) { $importFailedAppendixWording = ( "Our approach to automatically import the certificate failed." + "`r`nPlease export the Auth Certificate manually and import it to the Exchange servers where it's missing." + "`r`nIt's sufficient to import it to at least one Exchange server per AD site." + "`r`nIt will then be automatically deployed to all Exchange servers within that particular AD site." ) $importTriedWording = "`r`nWe've tried to import it to those Exchange servers and this is the result:" $importFailedWording = "Import failed: {0}" $importSuccessfulWording = "Import successful: {0}" if ($null -ne $importCurrentAuthCertificateResults) { Write-Host ("") if ($null -ne $emailBodyBase) { $finalEmailBody = $emailBodyBase + $emailBodyImportCurrentAuthCertificateResult } Write-Host ("The current Auth Certificate: $($authCertStatus.CurrentAuthCertificateThumbprint) is valid but missing on the following server(s):") -ForegroundColor Yellow Write-Host ([string]::Join(", ", $authCertStatus.AuthCertificateMissingOnServers)) -ForegroundColor Yellow if ($importCurrentAuthCertificateResults.ExportSuccessful) { Write-Host ($importTriedWording) if ($importCurrentAuthCertificateResults.ImportedToServersList.Count -gt 0) { Write-Host ($importSuccessfulWording -f [string]::Join(", ", $importCurrentAuthCertificateResults.ImportedToServersList)) -ForegroundColor Green } if ($importCurrentAuthCertificateResults.ImportToServersFailedList.Count -gt 0) { Write-Host ($importFailedWording -f [string]::Join(", ", $importCurrentAuthCertificateResults.ImportToServersFailedList)) -ForegroundColor Yellow Write-Host ($importFailedAppendixWording) -ForegroundColor Yellow } } else { Write-Host $importFailedAppendixWording -ForegroundColor Yellow } } if ($null -ne $importNextAuthCertificateResults) { Write-Host ("") if (($null -ne $emailBodyBase) -and ($null -eq $finalEmailBody)) { # No email content available as the current Auth Certificate wasn't imported before $finalEmailBody = $emailBodyBase + $emailBodyImportNextAuthCertificateResult } elseif (($null -ne $emailBodyBase) -and ($null -ne $finalEmailBody)) { # Email content available as the current Auth Certificate was imported too $finalEmailBody = $finalEmailBody + $emailBodyImportNextAuthCertificateResult } Write-Host ("The next Auth Certificate: $($authCertStatus.NextAuthCertificateThumbprint) is valid but missing on the following server(s):") -ForegroundColor Yellow Write-Host ([string]::Join(", ", $authCertStatus.NextAuthCertificateMissingOnServers)) -ForegroundColor Yellow if ($importNextAuthCertificateResults.ExportSuccessful) { Write-Host ($importTriedWording) if ($importNextAuthCertificateResults.ImportedToServersList.Count -gt 0) { Write-Host ($importSuccessfulWording -f [string]::Join(", ", $importNextAuthCertificateResults.ImportedToServersList)) -ForegroundColor Green } if ($importNextAuthCertificateResults.ImportToServersFailedList.Count -gt 0) { Write-Host ($importFailedWording -f [string]::Join(", ", $importNextAuthCertificateResults.ImportToServersFailedList)) -ForegroundColor Yellow Write-Host ($importFailedAppendixWording) -ForegroundColor Yellow } } else { Write-Host $exportFailedWording -ForegroundColor Yellow } } } else { if ($null -ne $emailBodyBase) { $finalEmailBody = $emailBodyBase + $emailBodyFailure } Write-Host ("") Write-Host ("There was an issue while performing the appropriate action - please check the verbose script log for more details.") -ForegroundColor Red } } else { Write-Host "" Write-Host ("Current Auth Certificate thumbprint: $($authCertStatus.CurrentAuthCertificateThumbprint)") -ForegroundColor Cyan Write-Host ("Current Auth Certificate is valid for $($authCertStatus.CurrentAuthCertificateLifetimeInDays) day(s)") -ForegroundColor Cyan if (-not([string]::IsNullOrEmpty($authCertStatus.NextAuthCertificateThumbprint))) { Write-Host ("Next Auth Certificate thumbprint: $($authCertStatus.NextAuthCertificateThumbprint)") -ForegroundColor Cyan Write-Host ("Next Auth Certificate is valid for $($authCertStatus.NextAuthCertificateLifetimeInDays) day(s)") -ForegroundColor Cyan } if ($authCertStatus.MultipleExchangeADSites) { Write-Host ("We've detected Exchange servers in multiple AD sites") -ForegroundColor Cyan } if ($authCertStatus.HybridSetupDetected) { Write-Host ("Exchange Hybrid was detected in this environment") -ForegroundColor Cyan } if ($authCertStatus.NumberOfUnreachableServers -gt 0) { Write-Host ("Number of unreachable Exchange servers: $($authCertStatus.NumberOfUnreachableServers)") -ForegroundColor Cyan } if ($authCertStatus.AuthCertificateMissingOnServers.Count -gt 0) { Write-Host ("`r`nThe actively used Auth Certificate is missing on the following servers:") -ForegroundColor Cyan Write-Host ("$([string]::Join(", ", $authCertStatus.AuthCertificateMissingOnServers))") -ForegroundColor Cyan } if ($authCertStatus.NextAuthCertificateMissingOnServers.Count -gt 0) { Write-Host ("`r`nThe certificate which is configured as next Auth Certificate is missing on the following servers:") -ForegroundColor Cyan Write-Host ("$([string]::Join(", ", $authCertStatus.NextAuthCertificateMissingOnServers))") -ForegroundColor Cyan } Write-Host ("") Write-Host ("Test result: $($renewalActionWording)") -ForegroundColor Cyan if (($authCertStatus.AuthCertificateMissingOnServers.Count -gt 0) -or ($authCertStatus.NextAuthCertificateMissingOnServers.Count -gt 0)) { Write-Host ("`rThe script will try to import the certificate to the missing servers automatically (as long as it's valid).") -ForegroundColor Cyan } } if (($renewalActionRequired) -and ($renewalActionResult.RenewalActionPerformed) -and ($authCertStatus.MultipleExchangeADSites)) { $multipleExchangeADSitesWording = ( "We've successfully created a new certificate which was then configured as Auth Certificate." + "`r`nThe new certificate has the following thumbprint: $($renewalActionResult.NewCertificateThumbprint)" + "`r`n`nWe've also detected that Exchange is installed in multiple Active Directory sites. In rare cases the Exchange certificate servicelet " + "will fail to deploy the certificate to the other AD sites. `r`nYou can validate that the certificate was deployed by running the following command " + "on an Exchange server located in a different AD site than this server:" + "`r`n`nGet-ExchangeCertificate -Server -Thumbprint $($renewalActionResult.NewCertificateThumbprint)" + "`r`n`nIf you run the script again, it will try to import the certificate to the server(s) where it's missing." + "`r`nHowever, we recommend to wait for at least 24 hours to let Exchange Server perform the certificate replication task." ) Write-Host "" Write-Host ($multipleExchangeADSitesWording) -ForegroundColor Yellow } if ((-not($WhatIfPreference)) -and (($renewalActionResult.RenewalActionPerformed) -or ($null -ne $importCurrentAuthCertificateResults) -or ($null -ne $importNextAuthCertificateResults)) -and (-not([System.String]::IsNullOrEmpty($SendEmailNotificationTo)))) { Write-Host ("`r`nTrying to send out email notification to the following recipients: $($SendEmailNotificationTo)") $sendEmailNotificationParams.Add("Body", $finalEmailBody) if ($TrustAllCertificates) { $sendEmailNotificationParams.Add("IgnoreCertificateMismatch", $true) } if (Send-EwsMailMessage @sendEmailNotificationParams) { Write-Host ("An email message was successfully sent") } else { Write-Host ("We ran into an issue while trying to notify you via email - please check the log of the script") -ForegroundColor Yellow } } } } try { $loggerParams = @{ LogName = "AuthCertificateMonitoringLog" LogDirectory = (New-AuthCertificateMonitoringLogFolder -WhatIf:$WhatIfPreference) AppendDateTime = $true ErrorAction = "SilentlyContinue" } if (-not($WhatIfPreference)) { $Script:Logger = Get-NewLoggerInstance @loggerParams SetProperForegroundColor SetWriteHostAction ${Function:Write-DebugLog} SetWriteVerboseAction ${Function:Write-DebugLog} } Main } finally { Write-Host "" if (-not($WhatIfPreference)) { Write-Host ("Log file written to: $($Script:Logger.FullPath)") } else { Write-Host ("Script was executed by using '-WhatIf' parameter - no action was performed and no log file was generated") } Write-Host "" Write-Host ("Do you have feedback regarding the script? Please email ExToolsFeedback@microsoft.com.") -ForegroundColor Green Write-Host "" if ($Error.Count -ne 0) { foreach ($e in (Get-UnhandledErrors)) { Write-Host ("Unhandled error hit:") -ForegroundColor Red Write-Host ($e.ErrorInformation) -ForegroundColor Red } } else { Write-Verbose ("No errors occurred within the script") } if (-not($WhatIfPreference)) { RevertProperForegroundColor } } # SIG # Begin signature block # MIIn2gYJKoZIhvcNAQcCoIInyzCCJ8cCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA7c6mhVIJT/N34 # ovBrPWvqe8/VUUvfg8iHgdMmqFm7jaCCDXYwggX0MIID3KADAgECAhMzAAADTrU8 # esGEb+srAAAAAANOMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjMwMzE2MTg0MzI5WhcNMjQwMzE0MTg0MzI5WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDdCKiNI6IBFWuvJUmf6WdOJqZmIwYs5G7AJD5UbcL6tsC+EBPDbr36pFGo1bsU # p53nRyFYnncoMg8FK0d8jLlw0lgexDDr7gicf2zOBFWqfv/nSLwzJFNP5W03DF/1 # 1oZ12rSFqGlm+O46cRjTDFBpMRCZZGddZlRBjivby0eI1VgTD1TvAdfBYQe82fhm # WQkYR/lWmAK+vW/1+bO7jHaxXTNCxLIBW07F8PBjUcwFxxyfbe2mHB4h1L4U0Ofa # +HX/aREQ7SqYZz59sXM2ySOfvYyIjnqSO80NGBaz5DvzIG88J0+BNhOu2jl6Dfcq # jYQs1H/PMSQIK6E7lXDXSpXzAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUnMc7Zn/ukKBsBiWkwdNfsN5pdwAw # RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW # MBQGA1UEBRMNMjMwMDEyKzUwMDUxNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci # tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG # CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 # MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAD21v9pHoLdBSNlFAjmk # mx4XxOZAPsVxxXbDyQv1+kGDe9XpgBnT1lXnx7JDpFMKBwAyIwdInmvhK9pGBa31 # TyeL3p7R2s0L8SABPPRJHAEk4NHpBXxHjm4TKjezAbSqqbgsy10Y7KApy+9UrKa2 # kGmsuASsk95PVm5vem7OmTs42vm0BJUU+JPQLg8Y/sdj3TtSfLYYZAaJwTAIgi7d # hzn5hatLo7Dhz+4T+MrFd+6LUa2U3zr97QwzDthx+RP9/RZnur4inzSQsG5DCVIM # pA1l2NWEA3KAca0tI2l6hQNYsaKL1kefdfHCrPxEry8onJjyGGv9YKoLv6AOO7Oh # JEmbQlz/xksYG2N/JSOJ+QqYpGTEuYFYVWain7He6jgb41JbpOGKDdE/b+V2q/gX # UgFe2gdwTpCDsvh8SMRoq1/BNXcr7iTAU38Vgr83iVtPYmFhZOVM0ULp/kKTVoir # IpP2KCxT4OekOctt8grYnhJ16QMjmMv5o53hjNFXOxigkQWYzUO+6w50g0FAeFa8 # 5ugCCB6lXEk21FFB1FdIHpjSQf+LP/W2OV/HfhC3uTPgKbRtXo83TZYEudooyZ/A # Vu08sibZ3MkGOJORLERNwKm2G7oqdOv4Qj8Z0JrGgMzj46NFKAxkLSpE5oHQYP1H # tPx1lPfD7iNSbJsP6LiUHXH1MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq # hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 # IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG # EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG # A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg # Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC # CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03 # a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr # rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg # OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy # 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9 # sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh # dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k # A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB # w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn # Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90 # lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w # ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o # ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD # VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa # BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny # bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG # AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t # L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV # HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG # AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl # AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb # C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l # hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6 # I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0 # wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560 # STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam # ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa # J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah # XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA # 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt # Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr # /Xmfwb1tbWrJUnMTDXpQzTGCGbowghm2AgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAANOtTx6wYRv6ysAAAAAA04wDQYJYIZIAWUDBAIB # BQCggcYwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEINVKNssOxXeJsJe/GUwnqn6C # pfY82tJxehVLgqZl3Q1YMFoGCisGAQQBgjcCAQwxTDBKoBqAGABDAFMAUwAgAEUA # eABjAGgAYQBuAGcAZaEsgCpodHRwczovL2dpdGh1Yi5jb20vbWljcm9zb2Z0L0NT # Uy1FeGNoYW5nZSAwDQYJKoZIhvcNAQEBBQAEggEAanuBACtlyUqQotJS4cOfeW0p # rWwYDx6cBh00fMPJkHk4fAQdVKcZg3Ix7gRdv/vsxOwWuoK70ZDeS07Vl2Khr9OI # JHGQ1xz8FtZbB4N2L+50htdV8f0Giee5RtM3xRLkzWK1WG9983gYJfXbGXexndAA # jkCNE556Ue0CswHf+QLP+L4+vl02Ol/svur+rqxw5ZrPhH4IvfUMhRh7ngz003kM # y9oF9qddOPBn2GNbLkINnyYd9fzYArmlwitT9cgA3ogib0VgJk2jUMUe6asG2/yl # OSOEnVwfll46ffSbfRkDJz9ZRWxEDSVcO3igWjiZ+7D327ZQ4GTENjnYDyf0BqGC # FywwghcoBgorBgEEAYI3AwMBMYIXGDCCFxQGCSqGSIb3DQEHAqCCFwUwghcBAgED # MQ8wDQYJYIZIAWUDBAIBBQAwggFZBgsqhkiG9w0BCRABBKCCAUgEggFEMIIBQAIB # AQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCCXSLwRrOPtpNVyzOEOy614 # lHdFidhJy1dsRUzSTHhyrgIGZULFZSuQGBMyMDIzMTExNDE3NTYxOC42NDJaMASA # AgH0oIHYpIHVMIHSMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ # MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u # MS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQx # JjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOjg2REYtNEJCQy05MzM1MSUwIwYDVQQD # ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloIIRezCCBycwggUPoAMCAQIC # EzMAAAHdXVcdldStqhsAAQAAAd0wDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMC # VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV # BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp # bWUtU3RhbXAgUENBIDIwMTAwHhcNMjMxMDEyMTkwNzA5WhcNMjUwMTEwMTkwNzA5 # WjCB0jELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UE # CxMkTWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQL # Ex1UaGFsZXMgVFNTIEVTTjo4NkRGLTRCQkMtOTMzNTElMCMGA1UEAxMcTWljcm9z # b2Z0IFRpbWUtU3RhbXAgU2VydmljZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC # AgoCggIBAKhOA5RE6i53nHURH4lnfKLp+9JvipuTtctairCxMUSrPSy5CWK2Dtri # QP+T52HXbN2g7AktQ1pQZbTDGFzK6d03vYYNrCPuJK+PRsP2FPVDjBXy5mrLRFzI # HHLaiAaobE5vFJuoxZ0ZWdKMCs8acjhHUmfaY+79/CR7uN+B4+xjJqwvdpU/mp0m # Aq3earyH+AKmv6lkrQN8zgrcbCgHwsqvvqT6lEFqYpi7uKn7MAYbSeLe0pMdatV5 # EW6NVnXMYOTRKuGPfyfBKdShualLo88kG7qa2mbA5l77+X06JAesMkoyYr4/9CgD # FjHUpcHSODujlFBKMi168zRdLerdpW0bBX9EDux2zBMMaEK8NyxawCEuAq7++7kt # FAbl3hUKtuzYC1FUZuUl2Bq6U17S4CKsqR3itLT9qNcb2pAJ4jrIDdll5Tgoqef5 # gpv+YcvBM834bXFNwytd3ujDD24P9Dd8xfVJvumjsBQQkK5T/qy3HrQJ8ud1nHSv # tFVi5Sa/ubGuYEpS8gF6GDWN5/KbveFkdsoTVIPo8pkWhjPs0Q7nA5+uBxQB4zlj # EjKz5WW7BA4wpmFm24fhBmRjV4Nbp+n78cgAjvDSfTlA6DYBcv2kx1JH2dIhaRnS # eOXePT6hMF0Il598LMu0rw35ViUWcAQkUNUTxRnqGFxz5w+ZusMDAgMBAAGjggFJ # MIIBRTAdBgNVHQ4EFgQUbqL1toyPUdpFyyHSDKWj0I4lw/EwHwYDVR0jBBgwFoAU # n6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFt # cCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcw # AoZQaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3Nv # ZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIw # ADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZI # hvcNAQELBQADggIBAC5U2bINLgXIHWbMcqVuf9jkUT/K8zyLBvu5h8JrqYR2z/ea # O2yo1Ooc9Shyvxbe9GZDu7kkUzxSyJ1IZksZZw6FDq6yZNT3PEjAEnREpRBL8S+m # bXg+O4VLS0LSmb8XIZiLsaqZ0fDEcv3HeA+/y/qKnCQWkXghpaEMwGMQzRkhGwcG # dXr1zGpQ7HTxvfu57xFxZX1MkKnWFENJ6urd+4teUgXj0ngIOx//l3XMK3Ht8T2+ # zvGJNAF+5/5qBk7nr079zICbFXvxtidNN5eoXdW+9rAIkS+UGD19AZdBrtt6dZ+O # dAquBiDkYQ5kVfUMKS31yHQOGgmFxuCOzTpWHalrqpdIllsy8KNsj5U9sONiWAd9 # PNlyEHHbQZDmi9/BNlOYyTt0YehLbDovmZUNazk79Od/A917mqCdTqrExwBGUPbM # P+/vdYUqaJspupBnUtjOf/76DAhVy8e/e6zR98PkplmliO2brL3Q3rD6+ZCVdrGM # 9Rm6hUDBBkvYh+YjmGdcQ5HB6WT9Rec8+qDHmbhLhX4Zdaard5/OXeLbgx2f7L4Q # QQj3KgqjqDOWInVhNE1gYtTWLHe4882d/k7Lui0K1g8EZrKD7maOrsJLKPKlegce # J9FCqY1sDUKUhRa0EHUW+ZkKLlohKrS7FwjdrINWkPBgbQznCjdE2m47QjTbMIIH # cTCCBVmgAwIBAgITMwAAABXF52ueAptJmQAAAAAAFTANBgkqhkiG9w0BAQsFADCB # iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl # ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMp # TWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEw # OTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UE # CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z # b2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQ # Q0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOThpkzntHIh # C3miy9ckeb0O1YLT/e6cBwfSqWxOdcjKNVf2AX9sSuDivbk+F2Az/1xPx2b3lVNx # WuJ+Slr+uDZnhUYjDLWNE893MsAQGOhgfWpSg0S3po5GawcU88V29YZQ3MFEyHFc # UTE3oAo4bo3t1w/YJlN8OWECesSq/XJprx2rrPY2vjUmZNqYO7oaezOtgFt+jBAc # nVL+tuhiJdxqD89d9P6OU8/W7IVWTe/dvI2k45GPsjksUZzpcGkNyjYtcI4xyDUo # veO0hyTD4MmPfrVUj9z6BVWYbWg7mka97aSueik3rMvrg0XnRm7KMtXAhjBcTyzi # YrLNueKNiOSWrAFKu75xqRdbZ2De+JKRHh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9 # fvzZnkXftnIv231fgLrbqn427DZM9ituqBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdH # GO2n6Jl8P0zbr17C89XYcz1DTsEzOUyOArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7X # KHYC4jMYctenIPDC+hIK12NvDMk2ZItboKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiE # R9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/ # eKtFtvUeh17aj54WcmnGrnu3tz5q4i6tAgMBAAGjggHdMIIB2TASBgkrBgEEAYI3 # FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQWBBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAd # BgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEE # AYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29t # L3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMI # MBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMB # Af8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1Ud # HwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3By # b2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcBAQRO # MEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2Vy # dHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MA0GCSqGSIb3DQEBCwUAA4IC # AQCdVX38Kq3hLB9nATEkW+Geckv8qW/qXBS2Pk5HZHixBpOXPTEztTnXwnE2P9pk # bHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gng # ugnue99qb74py27YP0h1AdkY3m2CDPVtI1TkeFN1JFe53Z/zjj3G82jfZfakVqr3 # lbYoVSfQJL1AoL8ZthISEV09J+BAljis9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHC # gRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTpkbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6 # MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEU # BHG/ZPkkvnNtyo4JvbMBV0lUZNlz138eW0QBjloZkWsNn6Qo3GcZKCS6OEuabvsh # VGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJsWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+ # fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrp # NPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0dFtq0Z4+7X6gMTN9vMvpe784cETRkPHI # qzqKOghif9lwY1NNje6CbaUFEMFxBmoQtB1VM1izoXBm8qGCAtcwggJAAgEBMIIB # AKGB2KSB1TCB0jELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAO # BgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEt # MCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYw # JAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo4NkRGLTRCQkMtOTMzNTElMCMGA1UEAxMc # TWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUANiNH # GWXbNaDPxnyiDbEOciSjFhCggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UE # CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z # b2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQ # Q0EgMjAxMDANBgkqhkiG9w0BAQUFAAIFAOj9vZcwIhgPMjAyMzExMTQxNzM0MTVa # GA8yMDIzMTExNTE3MzQxNVowdzA9BgorBgEEAYRZCgQBMS8wLTAKAgUA6P29lwIB # ADAKAgEAAgIW5gIB/zAHAgEAAgIl5zAKAgUA6P8PFwIBADA2BgorBgEEAYRZCgQC # MSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqG # SIb3DQEBBQUAA4GBABEiqdKN+EBxcwsjGUp8fo67vbIqsJ5lnwubj1R03nFEr2mk # YtlE09mXpSCyXZ8DNYIG+n4UNebTF9Eqcdn1TMqhUrOVZqhYwdpeAclAygwOAOSD # JUztRsTRcu8OtyZTuWit8SG8k9k9g7utfyEAfrQqXSOtx0D1+bVW3pb14HGPMYIE # DTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAHd # XVcdldStqhsAAQAAAd0wDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzEN # BgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgc1aJNKn9xGSnFAU2Eq4CcvM1 # QoVmbAsDBJ94S9/7nUEwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCBh/w4t # mmWsT3iZnHtH0Vk37UCN02lRxY+RiON6wDFjZjCBmDCBgKR+MHwxCzAJBgNVBAYT # AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBU # aW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB3V1XHZXUraobAAEAAAHdMCIEIJGauttw # He6RtfSrtffbDI4657AI6mEZsuQZDcXHG0bMMA0GCSqGSIb3DQEBCwUABIICAGj0 # Nk05Zt+qvQcoWdYYrhlYaSRx0iAmzJtIBOuN4u69D9BiPxyAgMwY2rDWmeAbXci2 # TL7mgQV6/cocFYA5GP/zs/sKCxjfl6F3EQhWwtzua2ZNsrVnSwFC5Xq1PC5/P7tO # A0GVSbDUyMvqFGoeoguM5dRY3dYm5UNKwPrfAXmyIGh0rT0Ol7xcQQQY0K10L3MO # 9PeDOUa6x6DRO9/YTDBa4souD7sOwBPTdkkjHai2IsyZtQDPd8gi5zsFZIgzGuLA # PcFboUgHXm28EzouRLwBgevwEzHhd2LpW/vhxfXPql9aDeoKAeIZ/tbNNcLoVxCy # /fn7vZBwGuqBVW+Bp3h0sc82kx3QAQPJbB5idad6bHo9+j5VbUwIICDvRvXc6kGI # IC6CsPOnJDD2fDFKnBN20l+bJNSUXTn/8CHcnGii2OKxiwk0CLWbfZHLHpHHkTEo # pNEwjyvfSaUsUw7WOZK46+bRFIakAyZw8uFTm/mjqVMfpCXQCElEfJqeVGMQxUQQ # LFDpS+4h7+Eh37I2sJk5wrDur5HmnMrcRTudygItDroTRMkrUUyMdPe/PLi0aFcK # h5rdYRpFq1Bq/j34rNBt+ho+IKM5RZLEwViQlZ11NZllEgKc9grljhxzY3DvIHbd # gdw0TH3aemMUhmDHNQnYPwEhcaQf/8Ip+6au/m8p # SIG # End signature block