Skip to content

Instantly share code, notes, and snippets.

Last active August 22, 2022 18:32
What would you like to do?
Let’s Encrypt for Windows and IIS, using the ACME-PS powershell module
import-module 'ACME-PS'
# This is an updated Let's Encrypt script using the ACME-PS module
# The original script (using ACMESharp) is by Marc Durdin
# This directory is used to store your account key and service directory urls as well as orders and related data
$acmeStateDir = "C:\Certs\AcmeState";
# Script parameters
$domain = ""
$alsodowww = $true
$certname = "$domain-$(get-date -format yyyy-MM-dd--HH-mm)"
$iissitename = "Default Web Site"
$documentRoot = "C:\inetpub\wwwroot";
# Environmental variables
$PSEmailServer = "localhost"
$LocalEmailAddress = ""
$OwnerEmailAddress = ""
$pfxfile = "C:\Certs\$certname.pfx"
$CertificatePassword = "PASSWORD!"
# Script setup - should be no need to change things below this point
$ErrorActionPreference = "Stop"
$EmailLog = @()
# Utility functions
function Write-Log {
Write-Host $args[0]
$script:EmailLog += $args[0]
Try {
Write-Log "Generating a new identifier for $domain"
$dnsIdentifiers = @($domain);
if ($alsodowww) {
Write-Log "And www.$domain"
$dnsIdentifiers = @($domain, "www.$domain");
Write-Log "Creating a new order for $dnsIdentifiers"
$order = New-ACMEOrder -State $acmeStateDir -Identifiers $dnsIdentifiers;
# Fetch the authorizations for that order
Write-Log "Fetch the authorizations for order $($order.ResourceUrl)"
$authorizations = @(Get-ACMEAuthorization -State $acmeStateDir -Order $order);
foreach ($authz in $authorizations) {
Write-Log "Get the challenge for authorization $($authZ.ResourceUrl)"
$challenge = Get-ACMEChallenge -State $acmeStateDir -Authorization $authZ -Type "http-01";
$chFilename = [System.IO.Path]::Combine($documentRoot, $challenge.Data.RelativeUrl.Replace("/", "\").TrimStart('\'));
$chDirectory = [System.IO.Path]::GetDirectoryName($chFilename);
# Ensure the challenge directory exists
if (-not (Test-Path $chDirectory)) {
Write-Log "Creating directory $chDirectory"
New-Item -Path $chDirectory -ItemType Directory;
#Write-Log "Writing token $($challenge.Data.Content) to file $chFilename"
Write-Log "Writing token data to file $chFilename"
Set-Content -Path $chFilename -Value $challenge.Data.Content -NoNewline;
Write-Log "Completing challenge $($challenge.Url)"
$completechallenge = Complete-ACMEChallenge -State $acmeStateDir -Challenge $challenge
# Wait a little bit and update the order, until we see the status 'ready' or 'invalid'
while ($order.Status -notin ("ready", "invalid")) {
Start-Sleep -Seconds 10;
Write-Log "Updating order $($order.ResourceUrl), status is $($order.Status)"
$order = Update-ACMEOrder -State $acmeStateDir -PassThru -Order $order
# Should the order get invalid, use Get-ACMEAuthorizationError to list error details.
if ($order.Status -ieq ("invalid")) {
$ordererror = Get-ACMEAuthorizationError -State $acmeStateDir -Order $order;
throw "Order was invalid,certificate cannot be issued: $ordererror";
# Complete the order - this will issue a certificate singing request
Write-Log "Completing order and making CSR"
Complete-ACMEOrder -State $acmeStateDir -Order $order -GenerateCertificateKey;
# Now we wait until the ACME service provides the certificate url
while (-not $order.CertificateUrl) {
Start-Sleep -Seconds 15
$order = Update-ACMEOrder -State $acmeStateDir -PassThru -Order $order
Write-Log "Exporting PFX certificate to $pfxfile"
Export-ACMECertificate -State $acmeStateDir -Order $order -Path $pfxfile -Password (ConvertTo-SecureString -String $CertificatePassword -AsPlainText -Force);
# Import the certificate to the local machine certificate store
Write-Log "Import pfx certificate $pfxfile"
$certRootStore = "LocalMachine"
$certStore = "My"
$pfx = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$pfx.Import($pfxfile, $CertificatePassword, "Exportable,PersistKeySet,MachineKeySet")
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store($certStore, $certRootStore)
$certThumbprint = $pfx.Thumbprint
####Set Certificate
$obj = get-webconfiguration "//sites/site[@name='$iissitename']"
$binding = $obj.bindings.Collection | where { (($_.protocol -eq "HTTPS") -and ($_.bindingInformation -eq ("*:443:$domain"))) }
Write-Log "Replace IIS certificate for site $iissitename in binding $($binding.bindingInformation)"
$method = $binding.Methods["AddSslCertificate"]
$methodInstance = $method.CreateInstance()
$methodInstance.Input.SetAttributeValue("certificateHash", $certThumbprint)
$methodInstance.Input.SetAttributeValue("certificateStoreName", $certStore)
if ($alsodowww) {
$binding = $obj.bindings.Collection | where { (($_.protocol -eq "HTTPS") -and ($_.bindingInformation -eq ("*:443:www.$domain"))) }
Write-Log "Replace IIS certificate for site $iissitename in binding $($binding.bindingInformation)"
$method = $binding.Methods["AddSslCertificate"]
$methodInstance = $method.CreateInstance()
$methodInstance.Input.SetAttributeValue("certificateHash", $certThumbprint)
$methodInstance.Input.SetAttributeValue("certificateStoreName", $certStore)
# Finished
Write-Log "Finished, see if it works here: https://$domain/"
if ($alsodowww) {
Write-Log "And here: https://www.$domain/"
$Body = $EmailLog | out-string
Send-MailMessage -SmtpServer $PSEmailServer -From $LocalEmailAddress -To $OwnerEmailAddress -Subject "Let's Encrypt certificate renewed for $domain" -Body $Body
Catch {
Write-Host $_.Exception
$ErrorMessage = $_.Exception | format-list -force | out-string
$EmailLog += "Let's Encrypt certificate renewal for $domain failed with exception`n$ErrorMessage`r`n`r`n"
$Body = $EmailLog | Out-String
Send-MailMessage -SmtpServer $PSEmailServer -From $LocalEmailAddress -To $OwnerEmailAddress -Subject "Let's Encrypt certificate renewal for $domain failed with exception" -Body $Body
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment