If you are dealing with SSL Certificates you are surely well
aware of LetsEncrypt. It is awesome and
it is free. Its one caveat however is the certificate expiry – 3 months. If you
are dealing with a lot of certificates the renewals can quickly became
cumbersome.
Previously I showcased a script for checking IIS Servers for Expiring
Certificates here.
Now I’m going to show you a PowerShell script for automated issuance and
renewal of LetsEncrypt certificates for IIS.
The script not only handles the issuance of the certificates, but also assigns the acquired certificate to the IIS Site. If a HTTPS binding doesn’t
exists it will create one.
The Script requires that ACMESharp is installed.
You should also initialize the ACME Vault and and setup an ACME Registration before running the script. This can be done with these simple commands:
I published the project on GitHub, so you can download it from there:
https://github.com/GLubomirov/Lets-Encrypt_Automate_PowerShell
You should also initialize the ACME Vault and and setup an ACME Registration before running the script. This can be done with these simple commands:
####Import ACME Module
import-module ACMESharp
####Initialize ACME Vault
import-module ACMESharp
####Initialize ACME Vault
Initialize-ACMEVault
####Set up new 'account'
tied to an email address
New-AcmeRegistration
-Contacts "yourMail@mail.com"
-AcceptTos |
out-null
https://github.com/GLubomirov/Lets-Encrypt_Automate_PowerShell
The Script has three parameters:
·
domain - This is the DNS. It should be accessible
from the Internet.
·
iisSiteName - This is the Name of the Site as
seen in the IIS Management Console.
·
renew - If you are creating a Certificate for
this Site for the First time this should be "False". If you are
renewing a certificate set it to "True"
Here is an example:
.\PATHTOSCRIPT\Lets-Encrypt_Automate_PowerShell.ps1
-domain "reportifier.com"
-iisSiteName "reportifier.com"
-renew "False"
Here is the full script. Better yet, go to GitHub and
download it from there.
param([string]$domain,[string]$iisSiteName, [string]$renew);
####PARAMETERS
## domain - This is the DNS of the Site. It
should be accessable from the Internet.
## iisSiteName - This is the Name of the Site as
seen in the IIS Management Console.
## renew - If you are creating a Certificate for
this Site for the First time this should be "False". If you are
renewing a certificate set it to "True"
####EXAMPLE
##
.\PATHTOSCRIPT\Lets-Encrypt_Automate_PowerShell.ps1 -domain
"reportifier.com" -iisSiteName "reportifier.com" -renew
"False"
###############################################################################################
##Initialize
###############################################################################################
$ErrorActionPreference
= "Stop"
####Check if PowerShell
is ran as Administrator. IIS is not available without Admin Priviliges
If(-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
return "This
script needs to be run As Administrator"
Break
}
####Import ACMESharp
try{
import-module ACMESharp
} catch{
return "Couldn't
load ACMESharp."
break
}
####Get script location
$scriptpath
= $MyInvocation.MyCommand.Path
$dir
= Split-Path
$scriptpath
####Fail Variables
$failInvalid
= $false
$failOnCert
= $false
$finalStatus
= "Success"
####The ACME ALias is
set to be the same as the Domain.
$alias
= $domain
####Cert Paths
$certname
= $alias
+"_"+"$(get-date -format yyyy-MM-dd--HH-mm)"
$pfxfile
= "$dir\$certname.pfx"
$SiteFolder
= Join-Path
-Path 'C:\inetpub\wwwroot'
-ChildPath $iissitename
$initializevault
= $FALSE
$createregistration
= $FALSE
#Not used. Left for
Reference. If ACME Vault is not initialized it should be before running the
script. This is a one time operation.
if($initializevault) {
Initialize-ACMEVault
}
#Not used. Left for
Reference. If there is no ACME Registration you should create on before running
the script. This is a one time operation.
if($createregistration) {
# Set up new 'account' tied to an email
address
New-AcmeRegistration -Contacts "$email" -AcceptTos | out-null
start-sleep -Seconds
2
}
####Get Acme Vault
$vault
= Get-ACMEVault
-VaultProfile :sys
####Change to the Vault
folder
cd
"C:\ProgramData\ACMESharp\sysVault"
####Check if alias
already created. This is obsolete. Left for reference.
#$aliasCheck =
$vault.Identifiers | where {$_.Alias -eq $alias}
#if($aliasCheck){
# $createalias = $TRUE
#} else {
# $createalias = $TRUE
#}
###Due to
"authorizations for these names not found or expired" error we now
create an Alias every time (for both - New Bindings and Renewals). The reason
for this
##is that the
authorization for the domain is active for one month but the certificates are for
3 months, so alot of users started getting this error after trying
##to renew their
certificate after three months.
$createalias
= $TRUE
####Generate Random
Alias
$alias
= $alias
+ -join ((1..10) | %{(65..90) + (97..122) | Get-Random} | % {[char]$_})
###############################################################################################
##Functions
###############################################################################################
####Check the Request
Status is ready before continuing. Keeps checking until Status is Valid or
Failed.
function
checkReqStatus {
$statusFull = (Update-ACMEIdentifier $alias -ChallengeType
http-01).Challenges
$statusHTTP = $statusFull | where {$_.Type -eq "http-01"}
if($statusHTTP.status -eq "Pending"){
####Loop Again
Start-Sleep -Seconds
10
checkReqStatus
} elseif ($statusHTTP.status -eq "Valid"){
####Continue with script
} else {
$failInvalid = $true
}
}
####Check if Certificate
is Ready before Downloading. If Certificate is not ready after 5 tries it
fails.
function
checkCertStatus {
$i++
$certFull =
update-AcmeCertificate $certname
if($certFull.Alias){
if((!($certFull.IssuerSerialNumber))
-and ($i
-le 5)){
Start-Sleep
-Seconds 10
checkCertStatus
}
} else {
$failOnCert = $true
}
}
###############################################################################################
##Core
###############################################################################################
####Check if Binding
already Exists
$obj
= get-webconfiguration
"//sites/site[@name='$iissitename']"
$binding
= $obj.bindings.Collection
| where {(($_.protocol -eq "HTTPS")
-and ($_.bindingInformation -eq
("*:443:" +
$domain)))}
####Proceed only if
there is no such binding or if the renew param is true
if((!($binding))
-or ($renew
-eq "True")){
###############################################################################################
##New Alias
###############################################################################################
if($createalias){
####Associate a new site
try{
New-AcmeIdentifier
-Dns $domain
-Alias $alias
-ErrorAction Stop
| out-null
} catch {
$finalStatus
= "Error:
AcmeIdentifier already exists or creation failed"
return
"Error: AcmeIdentifier already exists or
creation failed"
}
start-sleep -Seconds
2
####Prove the site exists and is
accessible
try{
Complete-ACMEChallenge
$alias -ChallengeType
http-01 -Handler
iis -HandlerParameters
@{WebSiteRef="$iissitename"}
-ErrorAction Stop
| out-null
} catch {
$finalStatus
= "Error:
ACMEChallenge Complete Failed"
return
"Error: ACMEChallenge Complete Failed"
}
start-sleep -Seconds
2
####Validate site
try {
Submit-ACMEChallenge
$alias -ChallengeType
http-01 -ErrorAction
Stop |
out-null
} catch {
$finalStatus
= "Error:
ACMEChallenge Submit Failed"
return
"Error: ACMEChallenge Submit Failed"
}
####Check until Pending changes to Valid
or Invalid
checkReqStatus
}
###############################################################################################
##Generate Certificate
###############################################################################################
if($failInvalid
-eq $false){
####Generate a certificate
New-ACMECertificate ${alias} -Generate
-Alias $certname
| out-null
start-sleep -Seconds
2
####Submit the certificate
Submit-ACMECertificate $certname | out-null
start-sleep -Seconds
2
####Check Certificate Status until
Certificate is Ready
$i =
0
checkCertStatus
if($failOnCert
-eq $false){
####Export
Certificate to PFX file
Get-ACMECertificate
$certname -ExportPkcs12
$pfxfile |
out-null
start-sleep
-Seconds 2
####Import
Certificate
$certRootStore
= “LocalMachine”
$certStore
= "My"
$pfx
= New-Object
System.Security.Cryptography.X509Certificates.X509Certificate2
$pfx.import($pfxfile,$pfxPass,“Exportable,MachineKeySet,PersistKeySet”)
$store
= New-Object
System.Security.Cryptography.X509Certificates.X509Store($certStore,$certRootStore)
$store.Open('ReadWrite')
$store.Add($pfx)
$store.Close()
$certThumbprint
= $pfx.Thumbprint
if($renew -eq "False"){
####Create
Binding
try{
New-WebBinding
-Name $iissitename
-IPAddress "*"
-Port 443 -Protocol "https"
-HostHeader $domain
-SslFlags 1 -ErrorAction Stop
} catch{
return
"Error: New Web Binding Failed"
}
}
####Set
Certificate
$obj
= get-webconfiguration
"//sites/site[@name='$iissitename']"
$binding
= $obj.bindings.Collection
| where {(($_.protocol -eq "HTTPS")
-and ($_.bindingInformation -eq
("*:443:" +
$domain)))}
$method
= $binding.Methods["AddSslCertificate"]
$methodInstance
= $method.CreateInstance()
$methodInstance.Input.SetAttributeValue("certificateHash", $certThumbprint)
$methodInstance.Input.SetAttributeValue("certificateStoreName", $certStore)
$methodInstance.Execute()
} else {
return
"Error: Generation of Certificate failed"
}
} else {
return "Error:
ACMEChallenge invalid."
}
} else
{
return "Error:
Binding already exists. Renew Off."
}
return
$finalStatus
Your Github Script link is not working. Goes to an Error 404 page.
ReplyDeleteFixed. Sorry for late answer. Turns out Blogger doesn't notify you about new comments in any way unless you check.
DeleteHi George,
ReplyDeleteI'm using your script and it works great. I'm trying to generate a cert that would cover both domain.com and www.domain.com urls, but I'm struggling with it.
I've found a hint here but didn't manage to include it in your script.
https://github.com/ebekker/ACMESharp/issues/141
Hi.
DeleteHappy the script is working for you. This seems like a cool feature. I'm crazy busy lately but will put multiple SANs on the agenda when updating the script.
We have a similar case with alot of dynamically created DNSs on a single site and we cover it with just making a binding and certificate for each DNS. After all the certificate generation and renewal is automated, so it is not adding any work.
Also Wildcard certificates are coming to LetsEncrypt in January 2018.
https://letsencrypt.org/2017/07/06/wildcard-certificates-coming-jan-2018.html
Cheers!
Hi George
ReplyDeleteI keep getting the below error. What am I missing? Thanks.
Submit-ACMECertificate : Error creating new cert :: authorizations for these names not found or expired:
test-cert.mydomain.com
At C:\WinSfw\LetsEncrypt\Lets-Encrypt_Automate_PowerShell.ps1:168 char:9
+ Submit-ACMECertificate $certname | out-null
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : PermissionDenied: (ACMESharp.Vault.Model.CertificateInfo:CertificateInfo) [Submit-ACMECe
rtificate], AcmeWebException
+ FullyQualifiedErrorId : urn:acme:error:unauthorized (403),ACMESharp.POSH.SubmitCertificate
This is on Certificate Renewal right?
DeleteJust pushed an update to the script in Github to fix this.
The reason for this is that the authorization for the domain is active for one month but the certificates are for 3 months, so you will get this error after trying to renew your certificate after more then one month.
Now we just authorize the domain every time with new alias.
https://github.com/GLubomirov/Lets-Encrypt_Automate_PowerShell
Hi George
DeleteI am actually testing this on a freshly installed windows. After you uploaded the modified script I kept trying different ways. But I am still getting the same error.
PS C:\WinSfw\LetsEncrypt> .\Lets-Encrypt_Automate_PowerShell.ps1 -domain "test-cert.mydomain.com" -iisSiteName "Default Web Site" -renew "False"
Submit-ACMECertificate : Error creating new cert :: authorizations for these names not found or expired:test-cert.mydomain.com
At C:\WinSfw\LetsEncrypt\Lets-Encrypt_Automate_PowerShell.ps1:177 char:9
+ Submit-ACMECertificate $certname | out-null
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : PermissionDenied: (ACMESharp.Vault.Model.CertificateInfo:CertificateInfo) [Submit-ACMECertificate], AcmeWebException
+ FullyQualifiedErrorId : urn:acme:error:unauthorized (403),ACMESharp.POSH.SubmitCertificate
PS C:\ProgramData\ACMESharp\sysVault>