Bulk install new certificates on multiple Windows Server 2016+ endpoints

Solution 1:

PowerShell is your friend.

https://docs.microsoft.com/en-us/powershell/module/pki/import-pfxcertificate
https://docs.microsoft.com/en-us/powershell/module/iisadministration/remove-iissitebinding
https://docs.microsoft.com/en-us/powershell/module/iisadministration/new-iissitebinding

You can directly pull the PFX file from a network path (\\server\share\filename.pfx) if you have the required permissions; if you need to specify credentials, use New-PSDrive.

You can put everything together in a script block and run it on remote servers using Invoke-Command; you can specify credentials here too, if required.

This can of course be done in a loop on a list of servers, such as

$serverlist = "server1","server2","server3"

foreach ($server in $serverlist)
{
    Invoke-Command -ComputerName $server -ScriptBlock
    {
        Import-PfxCertificate [...]
        Remove-IISSiteBinding [...]
        New-IISSiteBinding [...]
    }
}

If you need to create PSCredential objects, have a look here.

Last but not least, you can take in everything required (such as server names, credentials, web site names, etc) from a CSV file using Import-Csv.

Solution 2:

Hello dudes and dudettes,

Seems I've wrangled another one here with the 'ol PowerShell. Credit to @Massimo for getting me going in the right direction.

The PS script below has worked perfectly for me so far. I left comments within the script so things should be very clear for even the most novice cowboys/cowgirls.

Items to note:

  1. This was ran as a domain admin from a management machine that's subnet has VLAN access to all other segments. (I know that sounds like a security nightmare, but it is subject to full auditing and access logging as well as remains offline when not in use.)
  2. This has only been tested on Windows Server 2016 and 2019 - would be surprised if it worked for anything below 2016.
  3. Windows Server 2016 defaults to IISAdmin module 1.0, but 1.1.0.0 is required for this script so I built in the download/install from the remote repo. Feel free to remove for your own use, or use it at your own discretion.
  4. I chose to copy the .pfx file to each machine rather than install it from the network share because it simplified the process (authentication, looping, redundancy, blahblahblah). At the end of the script the .pfx is removed though. If your file has a complex password this should be fine, but do so at your own discretion.
  5. This solution applies the new cert and binding for IIS as well as enables TLS for local SMTP.
#Setup the vars below.  Yes, you gotta do some of the work...
$OldCertDirHash = 'Cert:\LocalMachine\My\1234567890QWERTYUIOPLKJHGFDSA' # Old/Expired cert's store path and hash ID
$NewCertHash = '0987654321POIUYTREWQASDFGHJKL' 
$NewCertSharePath = '\\FS01.foo.internal\ITStuff\SCENSATIVE\install\STAR.foo.com_2022.pfx' # PFX file's shared location on the network
$CertPass = Get-Credential -UserName 'Type PFX password below' -Message 'Enter password below' #Stores PXF's password obscurely
$IISSiteName = 'Default Web Site' #Website's name for new binding
$ADCPUs = Get-ADComputer -Filter * -SearchBase "OU=Testing Lab,OU=Web Servers,DC=Foo,DC=com" |  select-object -expandproperty name  #Target machines in AD using an OU's Distinguished Name
$NetworkPFXPath = 'C$\Temp\Certs' #Network path for each remote machine
$LocalPXFPath = 'C:\temp\Certs\STAR.foo.com_2022.pfx'  #Path were the .pxf file will be accessed locally on each machine


ForEach ($ADCPU in $ADCPUs)
{Write-Host Copying file $NewCertSharePath to $ADCPU $NetworkPFXPath -ForegroundColor Yellow
New-Item -Path \\$ADCPU\$NetworkPFXPath -ItemType Directory #Creates local folder on each remote machine
Copy-Item -Path $NewCertSharePath -Destination \\$ADCPU\$NetworkPFXPath # Copies pfx file from network to each remote machine's local folder
}


# ~~~ End of Setup. Magic happens below ~~~


ForEach  ($ADCPU in $ADCPUs)
  {
Write-Host $ADCPU -ForegroundColor Yellow # Highlights Server names while in process
Invoke-Command -ComputerName $ADCPU -ScriptBlock { 

#Get latest IISAdmin version from online repo with suppressed dialog
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
Install-Module -Name IISAdministration -RequiredVersion 1.1.0.0 -SkipPublisherCheck -Confirm:$False -Force

#The default IISAdmin version for server 2016 is 1.0 - below brings it up to the required version which was installed above.       
Import-Module IISAdministration -Version 1.1.0.0

#This line installs the cert from the .pfx using the password set earlier into the Computer's Personal cert store.
Import-PfxCertificate -FilePath $Using:LocalPXFPath -CertStoreLocation Cert:\LocalMachine\My -Password $Using:CertPass.Password -ErrorAction Inquire

#Use below if importing .crt rather than .pfx ~ Not in use here ~
        #Import-Certificate -FilePath "UNCPath" -CertStoreLocation Cert:\LocalMachine\My -ErrorAction Inquire

#Clear out any other active or pending IIS Server Manager tasks - prevents config file collision.        
Reset-IISServerManager -Confirm:$false

#Remove current HTTPS binding using old/expired cert
Remove-IISSiteBinding -Name 'Default Web Site' -BindingInformation '*:443:' -Protocol https -Confirm:$false

#Delete expired cert - not required but is good houskeeping
Get-ChildItem $Using:OldCertDirHash | Remove-Item -ErrorAction Ignore

#Delete Cert by subject,serialnumber,issuer,etc... ~ Not in use here. ~
#Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.FriendlyName -match '*.foo.com_2021' } | Remove-Item

#Setup https binding on 443 using new cert
New-IISSiteBinding -Name $Using:IISSiteName -BindingInformation "*:443:" -CertificateThumbPrint $Using:NewCertHash -CertStoreLocation "Cert:\LocalMachine\My" -Protocol https 

IISRESET #for obvious reasons...

Remove-Item -Path $Using:LocalPXFPath -Recurse -Verbose  #Cleanup the dir created to store the pfx file

Write-Host $ADCPU Done -ForegroundColor Green #This is more of a marker for reviewing output and would be helpful if writing to log/oputfile
    }
    
  }

Sorry for the long post, but this is a robust and thorough solution to my problem. I had myself a couple extra drinks after work for besting this one. Feel free to send me a drink if this works for you!