The beginning of each year, lately seems to be the time when I have to update my scripts that control the automatic management of SSL certificates. I started three years ago by learning first about Let’s Encrypt certificates, and how they could have solved my needs for automatically renewing (for free!) my SSL certificates. At the time I started to use ACMESharp: it seemed to be a great fit as it worked in powershell and had all the features I needed; but lately, it has lagged behind, and the move the ACME v2 was the final nail in its coffin.
Some background
Let me use the words that the LE team posted to explain this change:
“The original protocol used by Let’s Encrypt for certificate issuance and management is called ACMEv1. In March of 2018 we introduced support for ACMEv2, a newer version of the protocol that matches what was finalized today as RFC 8555. We have been encouraging subscribers to move to the ACMEv2 protocol.
Today we are announcing an end of life plan for ACMEv1.
In November of 2019 we will stop allowing new account registrations through our ACMEv1 API endpoint. Existing accounts will continue to function normally.
In June of 2020 we will stop allowing new domains to validate via ACMEv1.
Starting at the beginning of 2021 we will occasionally disable ACMEv1 issuance and renewal for periods of 24 hours, no more than once per month (OCSP service will not be affected). The intention is to induce client errors that might encourage subscribers to update to clients or configurations that use ACMEv2. Renewal failures should be limited since new domain validations will already be disabled and we recommend renewing certificates 30 days before they expire.
In June of 2021 we will entirely disable ACMEv1 as a viable way to get a Let’s Encrypt certificate.
We would like to remind people reading this about an upcoming change to our ACMEv2 support. Starting in November 2020 we will no longer allow unauthenticated resource GETs when using ACMEv2″
The tool I originally used, ACMESharp, is using ACME v1. So far I never had any issue using it, since my account is registered and the certificates can still be issued using v1. But lately, during a new installation of Veeam Cloud Connect and the creation of its SSL certificate, I’ve hit this error:
New-ACMERegistration : Account creation on ACMEv1 is disabled. Please upgrade your ACME client to a version that supports ACMEv2 / RFC 8555. See
https://community.letsencrypt.org/t/end-of-life-plan-for-acmev1/88430 for details.
So, I was not able to create a new account in Let’s Encrypt. I first went to check if there was a new version of ACMESharp that supported v2, but the information I found were not that clear. The author claimed in a blog post (dated back in 2018) that he was actively working on a new version of ACMESharp that would have supported ACME v2, but there was little progress on github, as it could be seen in the new ACMESharpCore project. Thikning back also to all the small but annoying issues I had in the past to make all the code work properly, I asked to myself: maybe there’s a different solution?
And it came out there was: Posh-ACME. And it was great from the first read I gave to it: as I use mainly DNS challenges to validate my certificates, the fact that the author of Posh-ACME focused on this type of challenge was the key for me.
Testing the new code
First, we need to be sure that our Windows server is running at least .NET 4.7.1. This can be checked directly via Powershell:
write-host "Checking if .NET 4.7.1 is installed..." $Net = (Get-ItemProperty "HKLM:SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full").Release -ge 461308 if($Net -eq $true){ write-output ".NET 4.7.1 is installed." } else{ write-output ".NET 4.7.1 is NOT installed. Please install it before using this script." }
Note that 461308 is the build number of .NET 4.7.1. You can use the same code to check different versions by changing this number. As expected, my server doesn’t have this version of .NET:
Once the needed version of .NET is installed, we can activate the script with some variables, and load the Posh-ACME module:
function Load-Module ($m) { # If module is imported say that and do nothing if (Get-Module | Where-Object {$_.Name -eq $m}) { write-host "Module $m is already imported." } else { # If module is not imported, but available on disk then import if (Get-Module -ListAvailable | Where-Object {$_.Name -eq $m}) { Import-Module $m } else { # If module is not imported, not available on disk, but is in online gallery then install and import if (Find-Module -Name $m | Where-Object {$_.Name -eq $m}) { Install-Module -Name $m -Force -Scope CurrentUser Import-Module $m } else { # If module is not imported, not available and not in online gallery then abort write-host "Module $m not imported, not available and not in online gallery, exiting." EXIT 1 } } } } Load-Module "Posh-ACME" Load-Module "AWSPowerShell"
Probably the best part of Posh-ACME, for me, is that it has integrated DNS plugins for a moltitude of DNS services. In this way, it’s really easy to configure the script to interact with a given DNS service and use a txt record as the challenge to validate the ownership of the domain. In my case, I’m using AWS Route53.
So, first operation, I load my AWS credentials of a user who is only capable of updating DNS records:
$AWSprofile = "vcc-le-updater" Set-AWSCredential -ProfileName $AWSprofile
You can check in the Posh-ACME tutorial what IAM role you need to create to have this user, together with many other useful information about the usage of Route53.
I then run the command:
$r53Params = @{R53ProfileName=$AWSprofile} New-PACertificate $domain -DnsPlugin Route53 -PluginArgs $r53Params
and I wait for the 2 minutes for the script to complete. This is done on purpose to allow DNS propagation once the txt record has been updated:
The script also grabs the newly created certificate for me:
Install the certificates in Cloud Connect
As in my previous examples, the final goal is not to just have the certificates, but to also automate their installation. But wait, where are the certificates? Good question: to know their location, we can run
Get-PACertificate -MainDomain $domain | fl
That’s not the most intuitive path, but the developer of the tool says that the path cannot be changed. Anyway, I only need the PFX file in order to install it. So, first of all I copy it into an easier location:
$certname = "vcc-$(get-date -format yyyyMMdd)" $pfxfile = "C:\Posh-ACME\$certname.pfx" $cert = Get-PACertificate -MainDomain $domain Copy-Item -Path $cert.PfxFile -Destination C:\Posh-ACME\$certname.pfx
Now that I have the file, I can use the same code of my previous script to install it into the certificate store first, and then apply it to Veeam Cloud Connect:
$securepfxpass = $pfxpass | ConvertTo-SecureString -AsPlainText -Force Import-PfxCertificate -FilePath $pfxfile -Password $securepfxpass -CertStoreLocation cert:\localMachine\my -Exportable asnp VeeamPSSnapin Connect-VBRServer -Server localhost $thumbprint = Get-PACertificate -MainDomain $domain $thumbprint = $thumbprint.Thumbprint $certificate = Get-VBRCloudGatewayCertificate -FromStore | Where {$_.Thumbprint -eq $thumbprint} Add-VBRCloudGatewayCertificate -Certificate $certificate Disconnect-VBRServer
To properly identify the certificate, I used its thumbprint, as this is a unique code that allows to identify each certificate.
Also, one note about the PFX password: by default is “poshacme”, but we can change it during the certificate creation by appending the proper switch:
$pfxpass = "poshacme" New-PACertificate $domain -DnsPlugin Route53 -PluginArgs $r53Params -PfxPass $pfxpass
At the end of the entire process, I finally have my certificate properly installed into Veeam Cloud Connect:
Renewals
Let’s Encrypt certificates only last 90 days. As I explained in the first post that I wrote three years ago and I linked at the beginning of this one, this is done on purpose to improve the security in case of a compromise of the certificate. Because the entire platform is consumed via API, it shouldn’t be a problem to refresh certificates so often. Once the first certificate has been created and installed, we can run a different script to renew it.
In Posh-ACME, PluginArgs are saved to the local profile and tied to the current ACME account. This is what enables easy renewals. It also means you can generate additional certificates without having to specify the PluginArgs parameter again as long as you’re using the same DNS plugin. However, because new values overwrite old values, it means that you can’t use different sets of parameters for different certificates unless you create a different ACME account.
In my case, all the parameters are going to be the same, so during the renewal I can run this simple command:
Submit-Renewal
This will inherit all the parameters already used for the first creation of the certificate, as long as I use the same ACME account. For this reason I run this command in a powershell script that is then scheduled using the same credentials I used to run the first script. There’s a minimum amount of time that Let’s Encrypt asks you to wait before they will renew a certificate:
but this can be overridden using the switch -Force.
Conclusions
Compared to the previous tool I was using, with its large amount of lines of code I had to write, the trimming of strings to have the TXT challenge availalable, and other annoying issues, the experience with Posh-ACME has been truly amazing! I’ve reduced my script from 188 to 90 lines of code, and most of them are just variables, modules and comments. I encourage everyone to test Posh-ACME, you will not be disappointed.