Samples/IIS-SingleServer.ps1
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Currently using Write-Host because it supports -NoNewLine')] param() <# Sample Script: IIS - Single Server 1. Request a new certificate from a primary server 2. Install the certificate and update all IIS SSL bindings 3. Build a 'log' of the results of these operations and optionally post to Slack #> # ################################# # # SCRIPT VARIABLES # # Modify the values below as needed # # ################################# # # The path and filename of the log file that we'll create as part of this script $logFile = "$($env:ProgramData)\CertifiCat-PS\logs\operational\certificat-ps-Log-$(get-date -format "MM-dd-yyyy-hh-mm-ss").txt" # A list of domains that should be included in the certificate. The first domain in the list will be the primary domain and the subsequent ones will be included as SANs $DomainList = "web1.example.com","web2.example.com" # When true, CertifiCat-PS will validate each of the domains in the list above to ensure that they are expected # CertifiCat-PS is configured by default to validate domains against the pattern: "(.)*.rit.edu". To modify this, use Set-CertifiCatDomainValidation -domainPattern "regex pattern here" # To disable this check completely, set this value to $false $ValidateDomains = $true # Set to true to post a message to Slack upon script completion $PostToSlack = $true # Provide the Slack incoming webhook URL to use, if desired $slackUrl = "" # Provide the port that should be tested at the end of the script as part of validating the certificate installation $tlsPortToTest = 443 # Specify the maximum number of attempts that the script should make to obtain a certificate # This is useful in cases where the ACME server (or upstream certificate provider) is slow in processing the request $maxRetries = 8 # This specifies the maximum possible number of seconds that the script will randomly wait before requesting a new certificate # This is useful when multiple servers are scheduled for simultaneous renewal # To disable this, set the value to 0 $maxJitter = 1800 # ################################# # # BEGIN SCRIPT # # ################################# # # log the start time $startTime = Get-Date # Build a log file (for simplicity, we'll just use Powershell's build-in Start-Transcript) Start-Transcript $logFile # Import the CertifiCat-PS module Import-Module CertifiCat-PS -Force if($null -eq (Get-Module CertifiCat-PS)){ return "Unable to load the CertifiCat-PS module -- please verify that it is installed (was it possibly installed in the scope of a CurrentUser rather than LocalMachine?)" } # Extract the primary domain from the DomainList if($DomainList.Count -gt 1){ $MainDomain = $DomainList[0] } else { $MainDomain = $DomainList } # Check to see if we're ready for renewal $readyToRenew = Confirm-ACMERenewalReadiness -DomainName $MainDomain # Dump the contents of the return object for logging Write-Host "`n`nConfirm-ACMERenewalReadiness Object:" $readyToRenew # Check to see if we're really ready to renew if($readyToRenew.ReadyForRenewal){ # We are -- let's do it! Write-Host "Ready to renew!" # Call the CertifiCat-PS function to get a new cert and update our bindings # Decide if we should sanity check the domains in the SAN list or not if($ValidateDomains){ $newCert = Initialize-NewACMECertificate -DomainList $DomainList -UpdateBindings -jitter $maxJitter } else { $newCert = Initialize-NewACMECertificate -DomainList $DomainList -UpdateBindings -jitter $maxJitter -SkipDomainValidation } # Dump the contents of the return object for logging Write-Host "`n`nInitialize-NewACMECertificate Object:" $newCert # count of the total number of retries needed $totalRetries = 0 # Check to see if the call was successfull if($null -eq $newCert.Certificate.Thumbprint){ # the ACME server (or upstream certificate issuer) might be slow in processing our request Write-Host "Sleeping for 3 Minutes..." Start-Sleep -Seconds 180 # 3 Minutes # Attempt to repair / complete the order $newCert = Repair-NewACMEOrder -MainDomain $MainDomain -UpdateBindings $totalRetires++ # Check to see if waiting 3 minutes was long enough if($null -eq $newCert.Certificate.Thumbprint){ # It wasn't... now we're going to loop for a while and wait progressively longer for($attempt = 1; $attempt -le $maxRetries; $attempt++){ # increment the retry counter $totalRetires++ # calculate the amount of time to wait before retrying $sleepDuration = $attempt * 5 * 60 # each attempt will wait for 5 minutes * the attempt number (converted to seconds for start-sleep) Write-Host "Sleeping for $($sleepDuration / 60) Minutes..." Start-Sleep $sleepDuration # Attempt another repair $newCert = Repair-NewACMEOrder -MainDomain $MainDomain -UpdateBindings # Dump the contents of the return object for logging Write-Host "`n`nRepair-NewACMEOrder Object after $attempt additional attempt(s):" $newCert # Check to see if we now have the certificate if($null -ne $newCert.Certificate.Thumbprint){ # we have the certificate -- we can exit out of the loop $attempt = 1000 } } } else { # We retrieved the cert after one retry # Dump the contents of the return object for logging Write-Host "`n`nRepair-NewACMEOrder Object after initial attempt:" $newCert } } # Check the host to ensure that the certificate is actually new $serverTest = Assert-SiteCertificate -hostToTest $MainDomain -portToTest $tlsPortToTest if($serverTest.CertificateThumbprint -eq $newCert.Certificate.Thumbprint){ $serverValidated = $true } else { $serverValidated = $false Write-Host "Certificate does not appear to have been changed. We would expect the server to be presenting thumbprint $($newCert.Certificate.Thumbprint) but it is presenting $($serverTest.CertificateThumbprint). Did something happen with the renewal?" } if($PostToSlack -and !([string]::IsNullOrEmpty($slackUrl))){ # Build overall status message for slack if(($serverValidated) -and ($newCert.CertificateImported) -and ($newCert.BindingsUpdated) -and ($newCert.CertificateCentralized)){ $slackMain = "ACME Certificate Renewed *Successfully* on *$([System.Net.Dns]::GetHostByName($env:computername).HostName)*:\n" } else { $slackMain = "ACME Certificate Renewed *Unsuccessfully* on *$([System.Net.Dns]::GetHostByName($env:computername).HostName)*:\n" } # Build emoji for each step $slackMain += "Import Cert into Store: " if($newCert.CertificateImported) { $slackMain += ":white_check_mark:" } else { $slackMain += ":warning:"} $slackMain += " | Centralize Cert Material: " if($newCert.CertificateCentralized) { $slackMain += ":white_check_mark:" } else { $slackMain += ":warning:"} $slackMain += " | Update Binding(s): " if($newCert.BindingsUpdated) { $slackMain += ":white_check_mark:" } else { $slackMain += ":warning:"} $slackMain += " | Validate Server Cert: " if($serverValidated) { $slackMain += ":white_check_mark:" } else { $slackMain += ":warning:"} #construct a link to crt.sh $crtShLink = "https://crt.sh?serial=" + $newCert.Certificate.SerialNumber #obtain the SAN list $certSans = $newCert.Certificate.DnsNameList -join ", " # build the footer $slackFooter = "Log in: $logFile\nCertificate SANs: $certSans\nFull Certificate Details: $crtShLink" # Construct the payload $slackPayload = @{ "blocks" = @(@{ "type" = "section"; "text" = @{ "type" = "mrkdwn"; "text" = $slackMain } }; @{ "type" = "context"; "elements" = @( @{ "type" = "mrkdwn"; "text" = $slackFooter } )};)} # Post to Slack Invoke-RestMethod -Uri $slackUrl -Method Post -Body (ConvertTo-Json $slackPayload -depth 5).Replace('\\n', '\n') } } else { # Nothing to do here -- we'll log that the certificate isn't ready for renewal yet Write-Host "Not ready to renew! Certificate expires: $(($readyToRenew.Certificates)[0].NotAfter)" } # log the end time $endTime = Get-Date # Add a few remaining bits and close the log Write-Host "Script Started: $startTime | Ended: $endTime | Total Execution Time: $($endTime - $startTime)" if($readyToRenew.ReadyForRenewal) { Write-Host "Total Retires Needed to Retrieve Cert: $totalRetries" } Stop-Transcript |