I just fought through all the various guides and outdated suggestions, and lots of AI generated garbage using non-existent commands, to come up with this script. Now you won’t have to.
If you’re struggling with your own script and don’t want to throw it all away and adapt (adopt?) this one, the key takeaways are to include both the MachineKeySet and PersistKeySet storage flags, and to use $binding.AddSslCertificate rather than other suggestions from the interwebs on how to set a certificate for a given site. The “Exportable” flag is not needed, contrary to what many suggest, and is indeed a security issue in some setups.
Not using the correct storage flags will give you vague and misleading error messages, in typical Microsoft fashion, such as
A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520)
Fun, huh? Well, here’s my script:
# Enable strict error handling
$ErrorActionPreference = "Stop"
# Variables
$certUrl = "https://internal.local/api/latest_certificate"
$outputCertPath = "$env:temp\certificate.pfx"
$username = "basicSecretUsername"
$password = "basicSecretPassword"
$certPassword = "Re4llyH4rdCertific4teP4ssword"
$certStoreName = "WebHosting"
$certCN = "*.mydomain.com"
# Download the certificate
Write-Host "Downloading the certificate..."
$authHeader = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${username}:${password}"))
Invoke-RestMethod -Uri $certUrl -Headers @{ Authorization = $authHeader } -OutFile $outputCertPath
Write-Host "Certificate downloaded to $outputCertPath."
# Install the certificate in the WebHosting store
Write-Host "Installing the certificate..."
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$cert.Import(
$outputCertPath,
$certPassword,
[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet `
-bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet
)
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store $certStoreName, "LocalMachine"
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$store.Add($cert)
$store.Close()
Write-Host "Certificate installed in the $certStoreName store."
# Get the thumbprint of the newly installed certificate
$newCertThumbprint = $cert.Thumbprint
Write-Host "New certificate thumbprint: $newCertThumbprint"
# Update IIS HTTPS bindings
Write-Host "Updating IIS HTTPS bindings..."
Import-Module WebAdministration
Get-WebBinding -Protocol "https" | ForEach-Object {
$binding = $_
$bindingPath = $binding.ItemXPath
if ($bindingPath -match "name='([^']+)'") {
$siteName = $matches[1]
$bindingInfo = $binding.bindingInformation
Write-Host "Updating binding for site '$siteName' with binding info '$bindingInfo'"
# Enable SNI explicitly
Set-WebBinding -Name $siteName -BindingInformation $bindingInfo -PropertyName "sslFlags" -Value 1
# Set the certificate
$binding.AddSslCertificate($newCertThumbprint, $certStoreName)
Write-Host "Binding updated successfully for '$siteName'"
} else {
throw "Failed to extract site name from binding path: $bindingPath"
}
}
# Remove old certificates from the WebHosting store
Write-Host "Removing old certificates..."
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$store.Certificates | Where-Object { $_.Thumbprint -ne $newCertThumbprint -and $_.Subject -like "CN=${certCN}" } | ForEach-Object {
Write-Host "Removing certificate with thumbprint: $($_.Thumbprint)"
$store.Remove($_)
}
$store.Close()
# Clean up temporary file
Remove-Item $outputCertPath -Force
Write-Host "Temporary certificate file removed."
# Done!
Write-Host "Script completed successfully."