Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .build/cspell-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Dsamain
DTLS
dumptidset
DWORD
ecdsa
eems
EFORMS
EICAR
Expand Down Expand Up @@ -102,6 +103,7 @@ NDIS
Nego
Netlogon
netsh
nist
nmap
noderunner
notcontains
Expand All @@ -114,6 +116,7 @@ NTFS
NUMA
nupkg
odata
oids
onmicrosoft
onprem
OutlookiOS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
. $PSScriptRoot\..\DataCollection\Get-ExchangeServerCertificate.ps1
. $PSScriptRoot\..\..\..\Shared\ActiveDirectoryFunctions\Get-InternalTransportCertificateFromServer.ps1
. $PSScriptRoot\..\..\..\Shared\CertificateFunctions\Import-ExchangeCertificateFromRawData.ps1
. $PSScriptRoot\..\..\..\Shared\CertificateFunctions\New-ExchangeSelfSignedCertificate.ps1
. $PSScriptRoot\..\..\..\Shared\Invoke-CatchActionError.ps1

function New-ExchangeAuthCertificate {
Expand All @@ -19,6 +20,11 @@ function New-ExchangeAuthCertificate {
[Parameter(Mandatory = $true, ParameterSetName = "NewNextAuthCert")]
[int]$CurrentAuthCertificateLifetimeInDays,

[Parameter(Mandatory = $false, ParameterSetName = "NewPrimaryAuthCert")]
[Parameter(Mandatory = $false, ParameterSetName = "NewNextAuthCert")]
[ValidateScript({ $_ -ge 0 })]
[int]$NewAuthCertificateLifetimeInDays,

[Parameter(Mandatory = $false, ParameterSetName = "NewPrimaryAuthCert")]
[Parameter(Mandatory = $false, ParameterSetName = "NewNextAuthCert")]
[ScriptBlock]$CatchActionFunction
Expand Down Expand Up @@ -100,6 +106,16 @@ function New-ExchangeAuthCertificate {
ErrorAction = "Stop"
}

$newCustomAuthCertificateParams = @{
AlgorithmType = "RSA"
UseRSACryptoServiceProvider = $true # Make sure to set this to true as the certificate can't be used as Auth Certificate otherwise
KeySize = 2048
LifetimeInDays = $NewAuthCertificateLifetimeInDays
SubjectName = "Microsoft Exchange Server Auth Certificate"
FriendlyName = $authCertificateFriendlyName
DomainName = @()
}

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
Expand Down Expand Up @@ -187,7 +203,13 @@ function New-ExchangeAuthCertificate {
Write-Verbose ("Starting Auth Certificate creation process")
try {
if ($PSCmdlet.ShouldProcess("New-ExchangeCertificate", "Generate new Auth Certificate")) {
$newAuthCertificate = New-ExchangeCertificate @newAuthCertificateParams
if ($NewAuthCertificateLifetimeInDays -gt 0) {
Write-Verbose "Creating a custom self-signed certificate with a lifetime of $NewAuthCertificateLifetimeInDays days"
$newAuthCertificate = New-ExchangeSelfSignedCertificate @newCustomAuthCertificateParams
} else {
Write-Verbose "Creating a default self-signed certificate with a lifetime of 5 years"
$newAuthCertificate = New-ExchangeCertificate @newAuthCertificateParams
Copy link

Copilot AI Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The New-ExchangeSelfSignedCertificate function returns a PSCustomObject with Subject and Thumbprint properties, while New-ExchangeCertificate likely returns a different object structure. This inconsistency may cause issues in subsequent code that expects a specific object format.

Suggested change
$newAuthCertificate = New-ExchangeCertificate @newAuthCertificateParams
$certObj = New-ExchangeCertificate @newAuthCertificateParams
$newAuthCertificate = [PSCustomObject]@{
Thumbprint = $certObj.Thumbprint
Subject = $certObj.Subject
}

Copilot uses AI. Check for mistakes.
}
Start-Sleep -Seconds 5
} else {
$newAuthCertificateParams.GetEnumerator() | ForEach-Object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ function Get-ExchangeAuthCertificateStatus {
[OutputType([System.Object])]
param(
[bool]$IgnoreUnreachableServers = $false,

[bool]$IgnoreHybridSetup = $false,

[bool]$EnforceNewNextAuthCertificateCreation = $false,

[ScriptBlock]$CatchActionFunction
)

Expand Down Expand Up @@ -158,13 +162,15 @@ function Get-ExchangeAuthCertificateStatus {
($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
} 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)) {
($nextAuthCertificateMissingOnServersList.Count -eq 0)) -or
$EnforceNewNextAuthCertificateCreation) {
# Scenario 2: Current Auth Certificate is valid but no next Auth Certificate defined or next Auth Certificate will expire in < 120 days
# or EnforceNewNextAuthCertificateCreation was explicitly set to true
$configureNextAuthRequired = $true
} elseif (($currentAuthCertificateValidInDays -le 0) -and
($nextAuthCertificateValidInDays -ge 0)) {
Expand All @@ -186,7 +192,7 @@ function Get-ExchangeAuthCertificateStatus {

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 ("Replace of the next Auth Certificate required or explicitly desired? $($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)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
.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 EnforceNewAuthCertificateCreation
You can use this switch parameter to let the script stage a new next Auth Certificate which will become automatically active within 24 hours.
.PARAMETER CustomCertificateLifetimeInDays
You can use this parameter to specify a custom lifetime for the newly created Auth certificate.
By default, the self-signed certificate is created with a lifetime of 5 years.
.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
Expand Down Expand Up @@ -67,6 +72,17 @@
.\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.
The new Auth Certificate, which is eventually created, will have a lifetime of 5 years.
.EXAMPLE
.\MonitorExchangeAuthCertificate.ps1 -ValidateAndRenewAuthCertificate $true -CustomCertificateLifetimeInDays 365 -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.
The new Auth Certificate, which is eventually created, will be created with a lifetime of 365 days (1 year).
.EXAMPLE
.\MonitorExchangeAuthCertificate.ps1 -EnforceNewAuthCertificateCreation -CustomCertificateLifetimeInDays 365 -Confirm:$false
Runs the script in Auth Certificate enforcement mode.A new Auth Certificate is created and staged as new next Auth Certificate.
Copy link

Copilot AI Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space after the period in 'mode.A new Auth Certificate'. Should be 'mode. A new Auth Certificate'.

Suggested change
Runs the script in Auth Certificate enforcement mode.A new Auth Certificate is created and staged as new next Auth Certificate.
Runs the script in Auth Certificate enforcement mode. A new Auth Certificate is created and staged as new next Auth Certificate.

Copilot uses AI. Check for mistakes.
The Exchange AuthAdmin servicelet will publish the newly created Auth Certificate as soon as it processes it the next time (usually in a 12 hour time frame).
The new Auth Certificate, which is created, will be created with a lifetime of 365 days (1 year).
.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
Expand All @@ -87,12 +103,23 @@ param(
[Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")]
[bool]$ValidateAndRenewAuthCertificate = $false,

[Parameter(Mandatory = $false, ParameterSetName = "EnforceNewNextAuthCertificateConfiguration")]
[switch]$EnforceNewAuthCertificateCreation,

[Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")]
[Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")]
[Parameter(Mandatory = $false, ParameterSetName = "EnforceNewNextAuthCertificateConfiguration")]
[ValidateScript({ $_ -ge 0 })]
[int]$CustomCertificateLifetimeInDays = 0,

[Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")]
[Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")]
[Parameter(Mandatory = $false, ParameterSetName = "EnforceNewNextAuthCertificateConfiguration")]
[bool]$IgnoreUnreachableServers = $false,

[Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")]
[Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")]
[Parameter(Mandatory = $false, ParameterSetName = "EnforceNewNextAuthCertificateConfiguration")]
[bool]$IgnoreHybridConfig = $false,

[Parameter(Mandatory = $false, ParameterSetName = "SetupAutomaticExecutionADRequirements")]
Expand Down Expand Up @@ -134,6 +161,7 @@ param(

[Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")]
[Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")]
[Parameter(Mandatory = $false, ParameterSetName = "EnforceNewNextAuthCertificateConfiguration")]
[Parameter(Mandatory = $false, ParameterSetName = "SetupAutomaticExecutionADRequirements")]
[switch]$SkipVersionCheck
)
Expand Down Expand Up @@ -169,7 +197,7 @@ 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.")
Write-Warning ("The script must be executed in elevated mode. Start the Exchange Management Shell as an administrator.")
$Error.Clear()
Start-Sleep -Seconds 2
exit
Expand Down Expand Up @@ -399,6 +427,8 @@ function Main {

if ($ValidateAndRenewAuthCertificate) {
Write-Host ("Mode: Testing and replacing or importing the Auth Certificate (if required)")
} elseif ($EnforceNewAuthCertificateCreation) {
Write-Host ("Mode: Enforce new next Auth Certificate creation")
} 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")
}
Expand All @@ -423,9 +453,10 @@ function Main {
}

$authCertificateStatusParams = @{
IgnoreUnreachableServers = $IgnoreUnreachableServers
IgnoreHybridSetup = $IgnoreHybridConfig
CatchActionFunction = ${Function:Invoke-CatchActions}
IgnoreUnreachableServers = $IgnoreUnreachableServers
IgnoreHybridSetup = $IgnoreHybridConfig
EnforceNewNextAuthCertificateCreation = $EnforceNewAuthCertificateCreation
CatchActionFunction = ${Function:Invoke-CatchActions}
}
$authCertStatus = Get-ExchangeAuthCertificateStatus @authCertificateStatusParams

Expand All @@ -439,7 +470,7 @@ function Main {
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."
$renewalActionWording = "The Auth Certificate configured as next Auth Certificate must be configured or replaced by a new one or is created on express request."
} elseif (($authCertStatus.CurrentAuthCertificateImportRequired) -or
($authCertStatus.NextAuthCertificateImportRequired)) {
$renewalActionWording = "The current or next Auth Certificate is missing on some servers and must be imported."
Expand All @@ -455,23 +486,28 @@ function Main {
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
if (($ValidateAndRenewAuthCertificate -or
$EnforceNewAuthCertificateCreation) -and
($renewalActionRequired)) {
Write-Host ("Renewal scenario: $($renewalActionWording)")
if ($authCertStatus.ReplaceRequired) {
$replaceExpiredAuthCertificateParams = @{
ReplaceExpiredAuthCertificate = $true
CatchActionFunction = ${Function:Invoke-CatchActions}
WhatIf = $WhatIfPreference
ReplaceExpiredAuthCertificate = $true
NewAuthCertificateLifetimeInDays = $CustomCertificateLifetimeInDays
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.<BR>" +
"It was immediately replaced by a new one which is already active.<BR><BR>"
} elseif ($authCertStatus.ConfigureNextAuthRequired) {
# Set CurrentAuthCertificateLifetimeInDays to 2 in case that EnforceNewAuthCertificateCreation was used
# We do that to ensure that the new Auth Certificate will become active next time the AuthAdmin servicelet processes it
$configureNextAuthCertificateParams = @{
ConfigureNextAuthCertificate = $true
CurrentAuthCertificateLifetimeInDays = $authCertStatus.CurrentAuthCertificateLifetimeInDays
NewAuthCertificateLifetimeInDays = $CustomCertificateLifetimeInDays
CurrentAuthCertificateLifetimeInDays = if ($EnforceNewAuthCertificateCreation) { 2 } else { $authCertStatus.CurrentAuthCertificateLifetimeInDays }
CatchActionFunction = ${Function:Invoke-CatchActions}
WhatIf = $WhatIfPreference
}
Expand Down
Loading
Loading