Security is good. Using HTTPS whenever feasible is therefore good. Historically, SSL certificates are absurdly expensive (for level of effort involved for the issuer.) Then Let’s Encrypt happened, and all was right in the world. Well, almost…If you run a Linux-based OS, using LE (Let’s Encrypt) is pretty simple for almost all common web servers. For Windows, things are improving for IIS, but for Exchange server (email), it takes a bit more work. Certificates are used in multiple locations for exchange, and in many cases, a single cert will need to be used for both the internal and external host names. This is possible, using the SAN (Subject Alternative Name) field of the certificate, but presents some challenges:
- LE will not issue a certificate for a domain that it cannot resolve and verify (for very good security reasons)
- The usual scripts that can register the cert for IIS will not update Exchange server
Imagine then, my delight, when I found a powershell script that not only handles the initial certificate request, but also the renewals! There was only one small catch: the script, provided by the amazing Franky on his website is written in German. Sure the powershell cmdlets are in English, but all the status messages and error messages are in German. Google Translate to the rescue!
After translating both the script and the comments on the Franky’s page, I am able to present, in English the script for Exchange server 2016 and Let’s Encrypt (republished with permission):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 |
Param( [bool]$Renew ) clear write-host "" write-host "----------------------------------------------------------------" write-host " _______ _ ___ _ (_______) _ (_) / __|_) _ _ _____ ____ _| |_ _ _| |__ _ ____ _____ _| |_ _____ | | | ___ |/ ___|_ _) (_ __) |/ ___|____ (_ _) ___ | | |_____| ____| | | |_| | | | | ( (___/ ___ | | |_| ____| \______)_____)_| \__)_| |_| |_|\____)_____| \__)_____) _______ _ (_______) (_) _ _ _______ ___ ___ _ ___ _| |_ _____ ____ _| |_ | ___ |/___)/___) |/___|_ _|____ | _ (_ _) | | | |___ |___ | |___ | | |_/ ___ | | | || |_ |_| |_(___/(___/|_(___/ \__)_____|_| |_| \__) " -foregroundcolor cyan write-host "" write-host " Certificate Assistant v1.0" write-host " Automatic Let's Encrypt Certificates for Exchange 2016" write-host "" write-host " Frank Zoechling (www.FrankysWeb.de)" write-host " English translation by Sean Cox (www.QuillandGear.net)" write-host "----------------------------------------------------------------" #Load ACME Sharp module write-host " Loading ACMESharp module..." Import-Module ACMESharp -ea 0 $CheckACMEModule = get-module ACMESharp if (!$CheckACMEModule) { write-host " Warning: ACME Sharp Module not found" -foregroundcolor yellow write-host " Trying to install ACMESharp module..." -foregroundcolor yellow Install-Module -Name ACMESharp -AllowClobber Import-Module ACMESharp -ea 0 $CheckACMEModule = get-module ACMESharp if (!$CheckACMEModule) { write-host " Error: ACME Sharp module could not be installed" -foregroundcolor red exit } } #Load the IIS PowerShell module write-host " Loading IIS Web Administration Module..." Import-Module Webadministration -ea 0 $CheckIISModule = get-module Webadministration if (!$CheckIISModule) { write-host " Web Administration Module not found" -foregroundcolor red exit } #Load Exchange SnapIn write-host " Load Exchange Management Shell..." Add-PSSnapin *exchange* -ea 0 $CheckExchangeSnapin = Get-PSSnapin *exchange* if (!$CheckExchangeSnapin) { write-host " Exchange SnapIn not found" -foregroundcolor red exit } if ($renew -ne $True) { #Query the configured DNS names write-host "" write-host " Read Exchange configuration..." $ExchangeServer = (Get-ExchangeServer $env:computername).Name [array]$CertNames += ((Get-ClientAccessService -Identity $ExchangeServer).AutoDiscoverServiceInternalUri.Host).ToLower() [array]$CertNames += ((Get-OutlookAnywhere -Server $ExchangeServer).Internalhostname.Hostnamestring).ToLower() [array]$CertNames += ((Get-OutlookAnywhere -Server $ExchangeServer).ExternalHostname.Hostnamestring).ToLower() [array]$CertNames += ((Get-MapiVirtualDirectory -Server $ExchangeServer).Internalurl.Host).ToLower() [array]$CertNames += ((Get-MapiVirtualDirectory -Server $ExchangeServer).ExternalUrl.Host).ToLower() [array]$CertNames += ((Get-OabVirtualDirectory -Server $ExchangeServer).Internalurl.Host).ToLower() [array]$CertNames += ((Get-OabVirtualDirectory -Server $ExchangeServer).ExternalUrl.Host).ToLower() [array]$CertNames += ((Get-ActiveSyncVirtualDirectory -Server $ExchangeServer).Internalurl.Host).ToLower() [array]$CertNames += ((Get-ActiveSyncVirtualDirectory -Server $ExchangeServer).ExternalUrl.Host).ToLower() [array]$CertNames += ((Get-WebServicesVirtualDirectory -Server $ExchangeServer).Internalurl.Host).ToLower() [array]$CertNames += ((Get-WebServicesVirtualDirectory -Server $ExchangeServer).ExternalUrl.Host).ToLower() [array]$CertNames += ((Get-EcpVirtualDirectory -Server $ExchangeServer).Internalurl.Host).ToLower() [array]$CertNames += ((Get-EcpVirtualDirectory -Server $ExchangeServer).ExternalUrl.Host).ToLower() [array]$CertNames += ((Get-OwaVirtualDirectory -Server $ExchangeServer).Internalurl.Host).ToLower() [array]$CertNames += ((Get-OwaVirtualDirectory -Server $ExchangeServer).ExternalUrl.Host).ToLower() $CertNames = $CertNames | select –Unique write-host "----------------------------------------------------------------" write-host "" write-host " The following DNS names were found:" write-host "" foreach ($Certname in $CertNames) { write-host " $certname" -foregroundcolor cyan } write-host "" #Add additional names? $AddName = "y" while ($AddName -match "y") { $AddName = read-host " Do you want to add more DNS names to the certificate? (y/n)" if ($AddName -match "y") { $AddHost = read-host " Please enter DNS name" $CertNames += "$addhost" } } #Output the DNS names write-host "" write-host " The following DNS names have been configured:" write-host "" foreach ($Certname in $CertNames) { write-host " $certname" -foregroundcolor cyan } write-host "" #Email address for the ACME registration write-host " Which e-mail address should be used to register with Let's Encrypt?" write-host "" write-host " If a Let's Encrypt registry has already been performed on this computer," write-host " no e-mail address must be specified." write-host $contact = read-host " E-Mail Address" $contactmail = "mailto:$contact" write-host #Add task to refresh? write-host " Do you want to create a scheduled task to automatically" write-host " renew the certificate?" write-host "" $AutoRenewTask = read-host " Automatic renewal? (y/n)" if ($AutoRenewTask -match "y") { $username = read-host " Username for the task (domain \ user)" $SecurePassword = read-host " Password" -AsSecureString } write-host "" #---------------------------------------------- #Create task to renew if ($AutoRenewTask -match "y") { $installpath = (get-location).Path #Create a scheduled task $time = "23:00" $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword) $Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) $startTime = "$time" | get-date -format s $taskService = New-Object -ComObject Schedule.Service $taskService.Connect() $rootFolder = $taskService.GetFolder($NULL) $taskDefinition = $taskService.NewTask(0) $registrationInformation = $taskDefinition.RegistrationInfo $registrationInformation = $taskDefinition.RegistrationInfo $registrationInformation.Description = "Let's Encrypt certificate renewal - www.FrankysWeb.de" $registrationInformation.Author = $username $taskPrincipal = $taskDefinition.Principal $taskPrincipal.LogonType = 1 $taskPrincipal.UserID = $username $taskPrincipal.RunLevel = 0 $taskSettings = $taskDefinition.Settings $taskSettings.StartWhenAvailable = $true $taskSettings.RunOnlyIfNetworkAvailable = $true $taskSettings.Priority = 7 $taskTriggers = $taskDefinition.Triggers $executionTrigger = $taskTriggers.Create(2) $executionTrigger.StartBoundary = $startTime $taskAction = $taskDefinition.Actions.Create(0) $taskAction.Path = "powershell.exe" $taskAction.Arguments = "-Command `"&'$installpath\CertificateAssistant.ps1' -renew:`$true`"" $job = $rootFolder.RegisterTaskDefinition("Lets Encrypt certificate renewal (www.FrankysWeb.de)" , $taskDefinition, 6, $username, $password, 1) } #---------------------------------------------- clear write-host "" write-host "---------------------------------------------------------------------------" -foregroundcolor green write-host " All information is available, should the certificate be configured? " -foregroundcolor green write-host "---------------------------------------------------------------------------" -foregroundcolor green write-host "" read-host "Start configuration? (Enter for Next / CTRL + C to cancel)" write-host "" #Perform DNS tests write-host "Check DNS records..." write-host "" $error = 0 foreach ($CertName in $CertNames) { $DNSIP = (Resolve-DnsName $CertName -Server 8.8.8.8 -DnsOnly -ea 0).IPAddress $ExtIP = (Invoke-RestMethod http://ipinfo.io/json).IP if ($DNSIP -ne $ExtIP) { write-host "Error: $CertName could not be resolved to IP $ExtIP" -foregroundcolor red $error++ } else { write-host "DNS test passed: $CertName was resolved in IP $ExtIP" -foregroundcolor green } } if ($error -gt 0) { write-host "" write-host "Error: Let's Encrypt must be able to validate the DNS names. This Exchange Server" -foregroundcolor red write-host " must be accessible via port 80 (http) under the configured DNS names from" -foregroundcolor red write-host " the Internet. For this test it tried to determine the WAN IP and resolve" -foregroundcolor red write-host " it via Google DNS" -foregroundcolor red write-host "" exit } #Check if Vault exists write-host "Check if a vault already exists..." $Vault = Get-ACMEVault if (!$Vault) { write-host "No Vault found, try to create a new Vault..." $CreateVault = Initialize-ACMEVault sleep 1 $Vault = Get-ACMEVault if (!$Vault) { write-host "Error: Vault could not be created" -foregroundcolor red exit } } #Check if Let's Encrypt registry is present write-host "Check Let's Encrypt Registration..." $Registration = Get-ACMERegistration if (!$Registration) { write-host "Warning: No registration was found at Let's Encrypt, new registration is being performed" -foregroundcolor yellow $Registration = New-ACMERegistration -Contacts $contactmail -AcceptTos if (!$Registration) { write-host "Error: Could not register with Let's Encrypt" -foregroundcolor red exit } else { write-host "Registration at Let's Encrypt was done" -foregroundcolor green } } #Prepare domain names validation $CertSubject = ((Get-OutlookAnywhere -Server $ExchangeServer).ExternalHostname.Hostnamestring).ToLower() $ExchangeSANID = 1 foreach ($ExchangeSAN in $CertNames) { $CurrentDate = get-date -format ddMMyyyy $ACMEAlias = "Cert" + "$CurrentDate" + "-" + "$ExchangeSANID" $ExchangeSANID++ write-host "New Identifier:" write-host " DNS: $ExchangeSAN" write-host " Alias: $ACMEAlias" $NewID = New-ACMEIdentifier -Dns $ExchangeSAN -Alias $ACMEAlias write-host "Prepare validation:" write-host " Alias $ACMEAlias" $ValidateReq = Complete-ACMEChallenge $ACMEAlias -ChallengeType http-01 -Handler iis -HandlerParameters @{ WebSiteRef = 'Default Web Site' } [Array]$ACMEAliasArray += $ACMEAlias if ($ExchangeSAN -eq $CertSubject) {$SubjectAlias = $ACMEAlias} } #Change Let's Encrypt IIS directory to HTTP write-host "Change Let's Encrypt IIS directory to HTTP..." $IISDir = Set-WebConfigurationProperty -Location "Default Web Site/.well-known" -Filter 'system.webserver/security/access' -name "sslFlags" -Value None $IISDirCeck = (Get-WebConfigurationProperty -Location "Default Web Site/.well-known" -Filter 'system.webserver/security/access' -name "sslFlags").Value if ($IISDirCeck -match 0) { write-host "Changed to HTTP successfully" -foregroundcolor green } else { write-host "Error: Change to HTTP was unsuccessful" -foregroundcolor red exit } #Validate domain names write-host "DNS names can be validated by Let's Encrypt..." foreach ($ACMEAlias in $ACMEAliasArray) { write-host "Running validation: $ACMEAlias" $Validate = Submit-ACMEChallenge $ACMEAlias -ChallengeType http-01 } write-host "30 Second wait..." sleep -seconds 30 #Check the validation write-host "Check whether the DNS names have been validated..." foreach ($ACMEAlias in $ACMEAliasArray) { write-host "Update Alias: $ACMEAlias" $ACMEIDUpdate = Update-ACMEIdentifier $ACMEAlias $ACMEIDStatus = $ACMEIDUpdate.Status if ($ACMEIDStatus -match "valid") { write-host "Validation OK" -foregroundcolor green } else { write-host "Error: Validation for alias $ACMEAlias failed" -foregroundcolor red exit } } #Prepare and submit the certificate $SANAlias = "SAN" + "$CurrentDate" $NewCert = New-ACMECertificate $SubjectAlias -Generate -AlternativeIdentifierRefs $ACMEAliasArray -Alias $SANAlias $SubmitNewCert = Submit-ACMECertificate $SANAlias #Wait until the certificate has been issued write-host "30 Second wait..." sleep -seconds 30 #Check status write-host "Check the certificate..." $UpdateNewCert = Update-ACMECertificate $SANAlias $CertStatus = (Get-ACMECertificate $SANAlias).CertificateRequest.Statuscode sleep 5 if ($CertStatus -match "OK") { write-host "Certificate OK" -foregroundcolor green } else { write-host "Error: Certificate not issued" -foregroundcolor red exit } #Export the certificate from Vault and assign to Exchange write-host "Export the certificate to $env:temp" $CertPath = "$env:temp" + "\" + "$SANAlias" + ".pfx" $PFXPasswort = Get-Random -Minimum 1000000 -Maximum 9999999 $CertExport = Get-ACMECertificate $SANAlias -ExportPkcs12 $CertPath -CertificatePassword $PFXPasswort write-host "Check whether the certificate has been exported..." if (test-path $CertPath) { write-host "Certificate has been successfully exported" -foregroundcolor green } else { write-host "Error: The certificate was not exported" -foregroundcolor red exit } write-host "Assign and activate in Exchange" $ImportPassword = ConvertTo-SecureString -String $PFXPasswort -Force –AsPlainText Import-ExchangeCertificate -FileName $CertPath -FriendlyName $ExchangeSubject -Password $ImportPassword -PrivateKeyExportable:$true | Enable-ExchangeCertificate -Services "SMTP, IMAP, POP, IIS" –force write-host "Check whether the certificate has been activated" $CurrentCertThumbprint = (Get-ChildItem -Path IIS:SSLBindings | where {$_.port -match "443" -and $_.IPAddress -match "0.0.0.0" } | select Thumbprint).Thumbprint $ExportThumbprint = $CertExport.Thumbprint if ($CurrentCertThumbprint -eq $ExportThumbprint) { write-host "The certificate has been successfully activated" -foregroundcolor green } else { write-host "Activation failed" -foregroundcolor red exit } } #---------------------------------------RENEW------------------------------------------------ #Automatic renewal if ($renew -eq $True) { $PFXPasswort = Get-Random -Minimum 1000000 -Maximum 9999999 $CurrentCertThumbprint = (Get-ChildItem -Path IIS:SSLBindings | where {$_.port -match "443" -and $_.IPAddress -match "0.0.0.0" } | select Thumbprint).Thumbprint $ExchangeCertificate = Get-ExchangeCertificate -Thumbprint $CurrentCertThumbprint $ExchangeSANs = ($ExchangeCertificate.CertificateDomains).Address $ExchangeSubject = $ExchangeCertificate.Subject.Replace("CN=","") if ($ExchangeSANs -notcontains $ExchangeSubject) {$ExchangeSANs += $ExchangeSubject} $CurrentDate = get-date $VaildTill = $ExchangeCertificate.NotAfter $DaysLeft = ($VaildTill - $CurrentDate).Days if ($DaysLeft -le 4) #4 days before expiration { $ExchangeSANID = 1 foreach ($ExchangeSAN in $ExchangeSANs) { $CurrentDate = get-date -format ddMMyyyy $ACMEAlias = "Cert" + "$CurrentDate" + "-" + "$ExchangeSANID" $ExchangeSANID++ New-ACMEIdentifier -Dns $ExchangeSAN -Alias $ACMEAlias Complete-ACMEChallenge $ACMEAlias -ChallengeType http-01 -Handler iis -HandlerParameters @{ WebSiteRef = 'Default Web Site' } [Array]$ACMEAliasArray += $ACMEAlias if ($ExchangeSAN -match $ExchangeSubject) {$ExchangeSubjectAlias = $ACMEAlias} } foreach ($ACMEAlias in $ACMEAliasArray) { Submit-ACMEChallenge $ACMEAlias -ChallengeType http-01 } sleep -seconds 30 foreach ($ACMEAlias in $ACMEAliasArray) { Update-ACMEIdentifier $ACMEAlias } $SANAlias = "SAN" + "$CurrentDate" New-ACMECertificate $ExchangeSubjectAlias -Generate -AlternativeIdentifierRefs $ACMEAliasArray -Alias $SANAlias Submit-ACMECertificate $SANAlias sleep -seconds 30 Update-ACMECertificate $SANAlias $CertPath = "$env:temp" + "\" + "$SANAlias" + ".pfx" $CertExport = Get-ACMECertificate $SANAlias -ExportPkcs12 $CertPath -CertificatePassword $PFXPasswort $ImportPassword = ConvertTo-SecureString -String $PFXPasswort -Force –AsPlainText Import-ExchangeCertificate -FileName $CertPath -FriendlyName $ExchangeSubject -Password $ImportPassword -PrivateKeyExportable:$true | Enable-ExchangeCertificate -Services "SMTP, IMAP, POP, IIS" –force } } |
Enjoy, and if you have questions/improvements, please let Franky know here. He also has versions that work for older versions of Exchange server.