Deploy Let's encrypt certificate with Exchange 2019 CU14 on Windows 2022 server

Hi,

I’m facing an issue with deployment of certificate with Echange 2019 CU14 services.
The tool deploy correctly the certificate on IIS default web site and it appears to be deployed successfully in Exchange Admin Console.

However I would like to get this certificate bind to SMTP, POP and IMAP services so I tried to use the out of the box Exchange script provided by Certify The web tool.
But when I try it, I get this error

2024-06-04 22:52:22.837 +02:00 [INF] ---- Beginning Request [Exchange_2019_IIS] ----
2024-06-04 22:52:22.837 +02:00 [INF] Certify/6.0.18.0 (Windows; Microsoft Windows NT 10.0.20348.0)
2024-06-04 22:52:22.840 +02:00 [INF] Beginning certificate request process: Exchange_2019_IIS using ACME provider Anvil
2024-06-04 22:52:22.840 +02:00 [INF] The selected Certificate Authority is: Let’s Encrypt
2024-06-04 22:52:22.840 +02:00 [INF] Requested identifiers to include on certificate: xxxxx [dns];xxxxxxxxx [dns]
2024-06-04 22:52:24.328 +02:00 [INF] Created ACME Order: https://acme-v02.api.letsencrypt.org/acme/order/xxxxxxx
2024-06-04 22:52:24.617 +02:00 [INF] Order is ready and valid. Auth challenges will not be re-attempted.
2024-06-04 22:52:24.617 +02:00 [INF] [Progress] Order authorizations already completed.
2024-06-04 22:52:24.629 +02:00 [INF] Resuming certificate request using CA: Let’s Encrypt
2024-06-04 22:52:24.629 +02:00 [INF] [Progress] Requesting certificate via Certificate Authority
2024-06-04 22:52:30.416 +02:00 [INF] [Progress] Completed certificate request.
2024-06-04 22:52:30.469 +02:00 [INF] [Progress] Performing automated certificate binding
2024-06-04 22:52:32.345 +02:00 [INF] Completed certificate request and automated binding updates
2024-06-04 22:52:32.345 +02:00 [INF] [Progress] New certificate received and standard deployment performed OK.
2024-06-04 22:52:32.346 +02:00 [INF] Performing Post-Request (Deployment) Tasks…
2024-06-04 22:52:32.349 +02:00 [INF] Task [Deploy to Exchange] :: Task is enabled and primary request was successful.
2024-06-04 22:52:32.364 +02:00 [INF] Executing command via PowerShell
2024-06-04 22:52:33.183 +02:00 [ERR] Powershell Task Completed.Error: Add-PSSnapin: Cannot load Windows PowerShell snap-in Microsoft.Exchange.Management.PowerShell.E2010 because of the following error: The type initializer for ‘Microsoft.Exchange.Data.Directory.Globals’ threw an exception.
At line:7 char:1

  • Add-PSSnapIn Microsoft.Exchange.Management.PowerShell.E2010

Error: Enable-ExchangeCertificate: The term ‘Enable-ExchangeCertificate’ is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:28 char:2

  • Enable-ExchangeCertificate -Thumbprint $result.ManagedItem.Certif ...
    
  • ~~~~~~~~~~~~~~~~~~~~~~~~~~
    

2024-06-04 22:52:33.183 +02:00 [ERR] Deploy to Exchange :: Powershell Task Completed.Error: Add-PSSnapin: Cannot load Windows PowerShell snap-in Microsoft.Exchange.Management.PowerShell.E2010 because of the following error: The type initializer for ‘Microsoft.Exchange.Data.Directory.Globals’ threw an exception.
At line:7 char:1

  • Add-PSSnapIn Microsoft.Exchange.Management.PowerShell.E2010

Error: Enable-ExchangeCertificate: The term ‘Enable-ExchangeCertificate’ is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:28 char:2

  • Enable-ExchangeCertificate -Thumbprint $result.ManagedItem.Certif ...
    
  • ~~~~~~~~~~~~~~~~~~~~~~~~~~
    

2024-06-04 22:52:58.761 +02:00 [INF] [Preview Mode] Completed certificate request and automated binding updates

How can I solve this issue?

Thank you

Thanks, I’m not an exchange expert but this error appears to be that there is some exception happening while it’s loading the powershell module for exchange. We haven’t had any similar reports but that doesn’t mean nobody else has encountered this.

From googling it looks similar to this problem nd may be fixed by ensure the latest updates to .net are installed:

I’d suggest reviewing whether the exchange machine running Certify The Web has all the usual admin tools installed.

Our built in task uses this script:

So it’s possible to adapt it and runit as your own script using the Run Powershell Script task. Scripting | Certify The Web Docs

Note that if you add/update a deployment task you don’t need to Request Certificate again to test it, you can save the changes and click the play button next to the task to just use the latest certificate with the task. Otherwise if you keep requesting the same cert the CA starts to rate limit you which isn’t ideal.

Thank you.

I have .NET 4.8.1 installed on that server as I’m running Exchange on windows 2022.
All Exchange admin tools are installed on that server too.

If I run as administrator each command separately in a PowerShell window, I’m able to load the Snapin, run the command Enable-ExchangeCertificate (even if I don’t add any parameters). At least they are recognised by the shell.

PS C:\Windows\system32> Add-PSSnapin Microsoft.Exchange.Management.Powershell.E2010
PS C:\Windows\system32> Enable-ExchangeCertificate

cmdlet Enable-ExchangeCertificate at command pipeline position 1
Supply values for the following parameters:
Thumbprint:
Services:
Enable-ExchangeCertificate : Cannot bind argument to parameter ‘Thumbprint’ because it is an empty string.
At line:1 char:1

  • Enable-ExchangeCertificate
  •   + CategoryInfo          : InvalidData: (:) [Enable-ExchangeCertificate], ParameterBindingValidationException
      + FullyQualifiedErrorId : ParameterArgumentValidationErrorEmptyStringNotAllowed,Microsoft.Exchange.Management.Syst
     emConfigurationTasks.EnableExchangeCertificate

I made some progress.

How to pass arguments to the task using Custom PowerShell task

I get this

Error: Enable-ExchangeCertificate : Cannot bind argument to parameter ‘Services’ because it is null.
Error: At D:\Scripts\CTW\Exchange.ps1:28 char:97

and put this in task arguments field: arg1=“SMTP,IIS”;arg2=true
I ticked “pass Result as first Arg”

Not sure about how to pass arguments to the script :slight_smile:

param($result, $services, [switch] $cleanupPreviousCerts = $false, [switch] $addDoNotRequireSslFlag = $false)

Thanks in advance

Progressed a bit more again :slight_smile:

I get this when I arrive to the script part here it should cleanup previous cert

Cleaning up previous certs in Exchange
Error: You cannot call a method on a null-valued expression.
Error: At D:\Scripts\CTW\Exchange.ps1:37 char:2
Error: + Get-ExchangeCertificate -DomainName $Certificate.Subject.split("= …
Error: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Error: + CategoryInfo : InvalidOperation: (:slight_smile: , RuntimeException
Error: + FullyQualifiedErrorId : InvokeMethodOnNull

parameters in script looks like this: param($result, $services, [switch] $cleanupPreviousCerts = $true, [switch] $addDoNotRequireSslFlag = $false)

and “Arguments” in task tab: services=SMTP,IIS;

I got some errors when passing boolean arguments to the script

2024-06-05 22:16:15.014 +02:00 [INF] Error: D:\Scripts\CTW\Exchange.ps1 : Cannot process argument transformation on parameter ‘addDoNotRequireSslFlag’. Cannot
Error: convert value “System.String” to type “System.Management.Automation.SwitchParameter”. Boolean parameters accept only
Error: Boolean values and numbers, such as $True, $False, 1 or 0.
Error: At C:\Program Files\CertifyTheWeb\Scripts\Internal\Script-Wrapper.ps1:36 char:15
Error: + & $scriptFile @wrappedArguments
Error: + ~~~~~~~~~~~~~~~~~
Error: + CategoryInfo : InvalidData: (:slight_smile: [Exchange.ps1], ParameterBindingArgumentTransformationException
Error: + FullyQualifiedErrorId : ParameterArgumentTransformationError,Exchange.ps1

to summarise, the script (Exchange.ps1 slightly adapted to get it more verbose helping troubleshooting) is executed using local service account on exchange server
to be able to add the Exchange snap-in, I need to tick “Launch new process” in task settings.

Enable-ExchangeCertificate command works, certificate is bind with desired services.

Now, just the cleanup is not working because running the command get-exchangecertificate as local system user trigger an access denied then the variable content is never filled in and cleanup fails.

I’m not a very good PowerShell expert, how can I pass exchange admin credentials within the script prior executing cleanup? Or maybe creating another task using Exchange admin credentials specifically for cleanup and executed after the deployment task.

1 Like

So it sounds like the original problem is helped by running out of process with a new powershell session, that suggests a conflict or side effect with the DLLs already loaded by our own process.

You can run your script as an impersonated user (e.g. the Exchange Admin) that might help with the cleanup step. It may be work wrapping that in a try catch depending on how important the cleanup is, as cert imported in different ways by different users can have different private key permissions, which could the sticking point here: about Try Catch Finally - PowerShell | Microsoft Learn

1 Like

I finally got it working. Helped by another script I found on this site.

Maybe not the most efficients script but they do what I need them to do.

Task under Certify the web tool is launched “as a new process” with local service account. AES key generation and password encryption need to be run only once (or when password change or if you want to change the AES key used for password encryption) and outside CTW tool.

=======================================================

  1. Generation of AES key (separate script)
$KeyFile = "D:\Scripts\CTW\AES.key"
$Key = New-Object Byte[] 32
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Key)
$Key | out-file $KeyFile 

=======================================================
2) Password for Exchange admin user encryption (separate script as well)

$PasswordFile = "D:\Scripts\CTW\Password.txt"
$KeyFile = "D:\Scripts\CTW\AES.key"
$Key = Get-Content $KeyFile
$Password = Read-Host -AsSecureString | ConvertFrom-SecureString -key $Key | Out-File $PasswordFile 

=======================================================
3) Cleanup of certificates (separate script)

$ExchUser = "Domain\user"
$PasswordFile = "D:\Scripts\CTW\Password.txt"
$KeyFile = "D:\Scripts\CTW\AES.key"
$Key = Get-Content $KeyFile
$ExchPasswd = Get-Content $PasswordFile | ConvertTo-SecureString -Key $Key

$ExchCred = new-object -TypeName System.Management.Automation.PSCredential -ArgumentList $ExchUser,$Exchpasswd


 $ServerSession = New-Pssession -ConfigurationName Microsoft.Exchange -ConnectionURI http://your server.domain/Powershell -Authentication Kerberos -Credential $ExchCred
Import-PSSession $ServerSession -DisableNameChecking -AllowClobber

if ($cleanupPreviousCerts -eq $true)
{
    Write-Host ""	
    Write-Host "Cleaning up previous cert in Exchange"
    Write-Host ""
    Write-Host "========================================================================================================================================================="
    
    # Find out how many matching certificates we have - the first one is always the one Exchange is currently using
    $Count = (Get-ExchangeCertificate -Server $SourceServer -DomainName $SearchDomain).Thumbprint.Count	
    Write-Host "Number of certificates for"$SearchDomain ":"$Count

    #Throw error if nothing is returned, grab the thumbprint if only 1 is found, grab the first entry in the array if more than 1 is found
    Switch ($Count)
    {
        0 { "ERROR: No certificate found!"; Break }
        1 { "ERROR: Only current certificate found!"; Break }
        2 { $ThumbprintList = (Get-ExchangeCertificate -Server $SourceServer -DomainName $SearchDomain).Thumbprint[1] }
        Default { $ThumbprintList = (Get-ExchangeCertificate -Server $SourceServer -DomainName $SearchDomain).Thumbprint[1] }
    }
        foreach ($Thumbprintitem in (Get-ExchangeCertificate -Server $SourceServer -DomainName $SearchDomain).Thumbprint)
    {
        Write-Host "Thumbprint for Exchange certificates on"$SourceServer" for "$SearchDomain": "$Thumbprintitem 
    }
    Write-Host "Certificate to be removed has the following thumbprint:"$ThumbprintList
    Remove-ExchangeCertificate -Thumbprint $ThumbprintList -Confirm:$false
}
Remove-PSSession $ServerSession

1 Like

Thanks, glad you got it working. To be clear though there is something unusual going on in the first place as you shouldn’t have had to do any of this.

It would be useful if any other users who see this could confirm if they are seeing the same problem (error loading the exchange powershell modules) and in what configuration.