# 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](https://www.mailenable.com/) 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](https://letsencrypt.org/docs/challenge-types/#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](https://paulgalow.com/aws-route-53-iam-policy-letsencrypt-dns/), 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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1705241437684/9fff495f-99b2-4ed2-a3aa-50e85cfdfb53.png align="center")

## Create a user Access key

Open newly created user `Settings` \\ `Security credentials` \\ `Create access key`

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1705241534109/da6ec9c8-be8d-43d6-90bd-dbc64ea9b083.png align="center")

Select `Third-party service` type

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1705241757819/b592a4ca-6d96-4593-9eb8-903ee42e7c9f.png align="center")

**Make a note** of the newly created **Access key** and its **Secret**! We will need this later when setting up PowerShell automation.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1705241857320/bd555367-c958-4f5b-9680-10373916be73.png align="center")

## 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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1705242036146/c1862371-d00a-4863-9377-21e10c6d7240.png align="center")

When on `Specify permissions` tab, switch setting to `JSON`.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1705242105793/cef7b1a1-0135-43cd-ab68-23d50a2dc1c4.png align="center")

Copy the following JSON and use it to replace the default one.

```json
{
  "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`](http://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`
> 
> ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1705242850948/ceffdb83-a78c-4b62-a765-f0fb5b1ed02e.png align="center")

Save the policy.

> For more details check [https://paulgalow.com/aws-route-53-iam-policy-letsencrypt-dns/](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`).

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1705242366194/b97eeebe-38e6-4619-9aa5-bde6ec604990.png align="center")

Pick the one you've created, select `Entities attached` and use `Attach` button to attach it to our IAM user.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1705242460022/27542a22-f894-43aa-beee-ad52ca0962b9.png align="center")

The user now has the minimum possible permission to set the LetsEncrypt TXT challenge record.

---

# Set up PowerShell script for managing certificate creation & renewal

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The following steps need to be done on the system where the certificate will be requested!</div>
</div>

As stated earlier, the PSH script uses [Posh-ACME](https://poshac.me) module hence, we need to install it.

```powershell
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.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Don't forget to modify <code>$accessKey</code>, <code>$accessKeySecret</code>, <code>$xmlPath</code> variables! <code>$accessKey</code> and <code>$accessKeySecret</code> corresponds to the AWS user access secret. <code>$xmlPath</code> is a path to XML, where this secret will be securely stored.</div>
</div>

```powershell
######## 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.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Don't forget to modify <code>$certDomainName</code>, <code>$expireContact</code>, <code>$cred</code> variables to match your environment!</div>
</div>

```powershell
# 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/](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.

![run as SYSTEM with admin privileges](https://cdn.hashnode.com/res/hashnode/image/upload/v1705316465816/33d0dbe1-cc62-4ec8-b79d-a2588f5d00c1.png align="center")

![run daily with 8 hours random delay](https://cdn.hashnode.com/res/hashnode/image/upload/v1705316524501/90e4cb6e-5f80-4e2c-9107-1a2f7be2c68f.png align="center")

The random delay is to minimize the chance of LetsEncrypt service throttling.

![run letsencrypt_cert.ps1 script](https://cdn.hashnode.com/res/hashnode/image/upload/v1705320653569/9a1d8c9d-796f-4d9d-8ba8-9719e82795ed.png align="center")

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1705308631272/d9193acc-59c6-4f06-a36b-4ccab696e603.png align="center")

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">It seems like even if you select <strong>NONE</strong> from the drop-down list, but the correct certificate exists, it will be automatically used anyway!</div>
</div>

Restart the server, send a test email, and check MailEnable `SMTP\Logs\Debug` log. You should see something like this

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1705309057855/26182fe7-3f72-42e9-9339-f29fe5fda1d2.png align="center")

Which mean that everything is working as it should!
