Request (and automatically renew) LetsEncrypt certificate using PowerShell and Route53 DNS service
This post will show you how to request & renew free LetsEncrypt certificate using PowerShell in automated way.
As an addition, I will show you how you can use it for MailEnable SMTP server.
We will use:
MailEnable as a free SMTP server, but the certificate can be created the same way for other SMTP/Web servers too
PowerShell script to request/renew LetsEncrypt certificate using Posh-ACME module run through
Scheduled task
LetsEncrypt DNS-01 challenge type for issuing the certificate
AWS Route53 DNS service, because it has API that allows you to scope permissions to modify just one specific DNS record
The result will be set & forget solution for managing LetsEncrypt certificates ๐.
SMTP certificate requirements
The default LetsEncrypt certificate is just fine
It has to have a private key and an SMTP server service account (SYSTEM in the case of MailEnable) has to be able to read it
In the case of MailEnable SMTP server, it has to be placed in computer's personal certificate store
Creating AWS IAM user with permission to modify just LetsEncrypt DNS verification TXT record
Because we will use DNS challenge to request/renew the LetsEncrypt certificate, we need to use DNS provider that supports API access and set up some automation around it.
I've used this great post, which you can check to get more details.
In general, we need to create in the AWS console:
IAM user
User access key
IAM policy defining required permissions
Create AWS IAM user
Create an ordinary IAM user with default settings
Create a user Access key
Open newly created user Settings
\ Security credentials
\ Create access key
Select Third-party service
type
Make a note of the newly created Access key and its Secret! We will need this later when setting up PowerShell automation.
Create AWS IAM policy
Now we will create an IAM policy that will allow our user to set and modify only selected TXT record (LetsEncrypt TXT record for satisfying the challenge).
Create a new policy
When on Specify permissions
tab, switch setting to JSON
.
Copy the following JSON and use it to replace the default one.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "route53:GetChange",
"Resource": "arn:aws:route53:::change/*"
},
{
"Effect": "Allow",
"Action": "route53:ListHostedZonesByName",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": ["route53:ListResourceRecordSets"],
"Resource": ["arn:aws:route53:::hostedzone/Z11111112222222333333"]
},
{
"Effect": "Allow",
"Action": ["route53:ChangeResourceRecordSets"],
"Resource": ["arn:aws:route53:::hostedzone/Z11111112222222333333"],
"Condition": {
"ForAllValues:StringEquals": {
"route53:ChangeResourceRecordSetsNormalizedRecordNames": [
"_acme-challenge.example.com"
],
"route53:ChangeResourceRecordSetsRecordTypes": ["TXT"]
}
}
}
]
}
Now we need to modify it a little bit!
Replace all Z11111112222222333333
with your hosted zone ID and example.com
with a domain name for which you will create the certificate (for example smtp.contoso.com).
Zone ID can be found in
Route 53
\Hosted zones
\<the zone that will host the TXT record>
\Hosted zone details
Save the policy.
For more details check https://paulgalow.com/aws-route-53-iam-policy-letsencrypt-dns/
Attach IAM policy to the IAM user
Switch to Policies
menu and filter your custom policies (Customer managed
).
Pick the one you've created, select Entities attached
and use Attach
button to attach it to our IAM user.
The user now has the minimum possible permission to set the LetsEncrypt TXT challenge record.
Set up PowerShell script for managing certificate creation & renewal
As stated earlier, the PSH script uses Posh-ACME module hence, we need to install it.
Install-Module "Posh-ACME"
Export Access key credential for making unattended AWS authentication
To be able to request and renew a LetsEncrypt certificate in the future, we must securely store AWS secret generated earlier.
Because DPAPI is used to store these credentials, the same account that will be used to run PSH script that requests and renews a LetsEncrypt certificate must be used to run the following code!
And it has to be a SYSTEM account because we need to install the requested certificate to the Computer Personal Certificate Store (because of MailEnable) and at the same time MailEnable server service account (which is a SYSTEM) must be able to read its private key (if you customize your MailEnable to run under different account, you can use any account with admin privileges to run this script).
Save following code to ps1
file and run it using Scheduled Task
under SYSTEM account.
$accessKey
, $accessKeySecret
, $xmlPath
variables! $accessKey
and $accessKeySecret
corresponds to the AWS user access secret. $xmlPath
is a path to XML, where this secret will be securely stored.######## YOU NEED TO MODIFY THIS SECTION !!!
$accessKey = "<accessKey>"
$accessKeySecret = "<accessKeySecret>"
$xmlPath = "folderWhereAWSCredentialsAreStored\aws_api_credential.xml"
######## YOU NEED TO MODIFY THIS SECTION !!!
$securedAccessKeySecret = ConvertTo-SecureString $accessKeySecret -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential $accessKey, $securedAccessKeySecret
Export-Clixml -InputObject $credential -Path $xmlPath -Encoding UTF8 -Force -ea Stop
As a result you will get aws_api_credential.xml
file with securely stored AWS credentials.
Create 'letsencrypt_cert.ps1' PSH script
Copy the following code and save it as letsencrypt_cert.ps1
file.
$certDomainName
, $expireContact
, $cred
variables to match your environment!# run this script every day at random time
# lets encrypt certificates have 90 days validity and can be renewed 30 days before expiration, but when trying every day, you will be notified ASAP if some problem occurs
# https://poshac.me/docs/v4/Tutorial/#plugins
# https://paulgalow.com/aws-route-53-iam-policy-letsencrypt-dns/
$ErrorActionPreference = "Stop"
######## YOU NEED TO MODIFY THIS SECTION !!!
$certDomainName = "smtp.contoso.com"
$expireContact = "postmaster@contoso.com"
$cred = Import-Clixml "folderWhereAWSCredentialsAreStored\aws_api_credential.xml"
# uncomment if you want to change default location for Posh-ACME module config (by default %LOCALAPPDATA%\Posh-ACME)
# here are cached all orders, retrieved certificates, credentials (protected by DPAPI) etc!
# "Setting Posh-ACME config location"
# $env:POSHACME_HOME = "C:\folderWhereYouWantToStorePOSHACMEConfiguration"
# [Void][System.IO.Directory]::CreateDirectory($env:POSHACME_HOME)
######## YOU NEED TO MODIFY THIS SECTION !!!
Start-Transcript (Join-Path $PSScriptRoot ((Split-Path $PSCommandPath -Leaf) + ".log"))
Import-Module -Name "Posh-ACME"
Set-PAServer "LE_PROD" # LE_STAGE for testing purposes
if (!(Get-PAAccount -List)) {
"No account exists, creating"
New-PAAccount -Contact $expireContact -AcceptTOS
}
$pArgs = @{
R53AccessKey = $cred.UserName
R53SecretKey = $cred.Password
}
# check whether required certificate order exists
"Getting existing orders"
$PAOrder = Get-PAOrder -Name $certDomainName
if ($PAOrder) {
# PluginArgs (credentials) are cached, but I want to always use the saved ones (in case of change)
"Trying to renew"
$renewed = Submit-Renewal -Name $certDomainName -PluginArgs $pArgs -Verbose
if ($renewed) {
"Renewal was successful"
"Applying new certificate to MailEnable server"
#region remove old certificate & restart the MailEnable service
# if previously selected in MailEnable settings, certificate should be reused automatically if placed in the Personal cert. store again
"Stopping MailEnable services"
Stop-Service -DisplayName "MailEnable*"
# remove old certificate
"Removing old $certDomainName certificate from Personal Store"
Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.FriendlyName -eq $certDomainName } | Remove-Item
# install new certificate
"Installing certificate to Personal store"
Get-PACertificate -Name $certDomainName | Install-PACertificate -StoreLocation LocalMachine -StoreName My
"Starting MailEnable services"
Start-Service -DisplayName "MailEnable*"
#endregion remove old certificate & restart the MailEnable service
} else {
return "No renewal needed"
}
} else {
# certificate order doesn't exist
"Creating new certificate request"
# PFX private key will be protected by first 15 chars of the API secret
New-PACertificate -Name $certDomainName -Domain $certDomainName -AcceptTOS -Contact $expireContact -Plugin Route53 -PluginArgs $pArgs -PfxPass ($cred.GetNetworkCredential().password.substring(0, 15)) -Force -Verbose
# install new certificate
"Installing certificate to Personal store"
Get-PACertificate -Name $certDomainName | Install-PACertificate -StoreLocation LocalMachine -StoreName My
# as error to be notified that something has to be done to get this working fully
throw "Certificate was created, but now you need to select it in the MailEnable configuration manually to finish the setup"
}
The code is based on the following official tutorial https://poshac.me/docs/v4/Tutorial/.
Create scheduled task to run 'letsencrypt_cert.ps1'
Now when we have ready aws_api_credential.xml
file with AWS credentials and letsencrypt_cert.ps1
with an actual script, we need to automate the invocation of it.
Open Scheduled Task
and create a new Scheduled Task
as follows.
The random delay is to minimize the chance of LetsEncrypt service throttling.
For the first time, run the scheduled task manually, to immediately get the requested certificate.
Invocation result will be logged in directory where letsencrypt_cert.ps1
script is located, named as letsencrypt_cert.ps1.log
.
When the script finishes, you should see new certificate in the computer's personal certificate store, which means, everything is working as expected โค.
Set up MailEnable server to use our LetsEncrypt certificate
To be able to use the certificate with MailEnable SMTP server:
The certificate has to be placed in computer's personal certificate store
- because renewal process creates a new one with each run, make sure you delete the expired one (my PSH script manages that)
The certificate has to contain a private key
MailEnable service account (SYSTEM by default) has to have the right to read our certificate private key
Now when the LetsEncrypt certificate exists, you have to open the MailEnable console and select it there
Restart the server, send a test email, and check MailEnable SMTP\Logs\Debug
log. You should see something like this
Which mean that everything is working as it should!