Replace ESX host certificates with CA signed wildcard certificate using PowerShell!

Recently we were requested to install CA signed certificates on our ESX hosts to pass a security audit.

The thought of doing this manually bored me! so I wrote the following script – which recursively puts each host into maintenance, installs new certificate, then reboots the host, takes it out of maintenance and tests the certificate! The script also produces a detailed log file.

This script requires plink and pscp EXE files to be present in the C:\Windows folder.
Download @ https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html

You need access to vCenter over port 443 from the machine you are running this script from.

You need access to your hosts over port 22 from the machine you are running this script from.

You need to generate a CA signed cert and Private key prior to running this script – rename cert as rui.crt, key as rui.key, and place both in a local directory on the machine you are running this script from – there are plenty of blog posts out there with instructions on how to setup a local MS CA, generate a cert and key. Or check out my blog post about replacing machine SSL certs on PSC servers using MS CA – it covers the required steps.

We decided to use a WILDCARD cert – so COMMON NAME and SUBJECT ALTERNATIVE NAME parameters for cert was defined as *.domain.com

Make sure to add your root and intermediate certs into the trusted certs store on vCenter – you can use the UI for this now.

Recommend disabling vSphere HA for the duration process, it does get confused after the cert is changed.

#############################################################################
#       Author: Cengiz Ulusahin
#       Version: 1.3
#       Date: 29/03/2021
#       Description: Replace ESX self-signed certs with a CA signed wildcard 
#       certificate
#       
#       !IMPORTANT NOTES!
#
#       This script requires plink and pscp EXE files to be present in the 
#       C:\Windows folder.  
#       Download @ https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html
#       You need access to vCenter over port 443 from the machine you are running 
#       this script from.
#       You need access to your hosts over port 22 from the machine you are running
#       this script from.
#       You need to generate a CA signed cert and Private key prior to running
#       this script - rename cert as rui.crt, key as rui.key, and place both in 
#       a local directory on the machine you are running this script from.
#       Make sure to add your root and intermediate certs into the trusted certs 
#       store on vCenter.
#       
#       !WARNING!
#
#       This script will recursively restart your ESX hosts after putting them 
#       in maintenance. 
#
#       Recommend disabling vSphere HA for the duration process, 
#       it does get confused with the cert change.
#
#############################################################################

#Function to generate wait time progress bar
function Start-Sleep($seconds) {
    $doneDT = (Get-Date).AddSeconds($seconds)
    while ($doneDT -gt (Get-Date)) {
        $secondsLeft = $doneDT.Subtract((Get-Date)).TotalSeconds
        $percent = ($seconds - $secondsLeft) / $seconds * 100
        Write-Progress -Activity "Sleeping" -Status "Sleeping..." -SecondsRemaining $secondsLeft -PercentComplete $percent
        [System.Threading.Thread]::Sleep(500)
    }
    Write-Progress -Activity "Sleeping" -Status "Sleeping..." -SecondsRemaining 0 -Completed
}

#Function for Logging
Function Write-Log {
    Param ([string]$logstring)
    $stamp = (Get-Date).toString("yyyy-MM-dd HH:mm:ss")
    $line = "$stamp $logstring"
    Add-content $logfile -value $line
}

#Function to check SSL cert
#Put this together using existing functions detailed in links below
#https://docs.microsoft.com/en-us/archive/blogs/parallel_universe_-_ms_tech_blog/reading-a-certificate-off-a-remote-ssl-server-for-troubleshooting-with-powershell
#https://dscottraynsford.wordpress.com/2016/12/24/test-website-ssl-certificates-continuously-with-powershell-and-pester/
#https://www.powershellgallery.com/packages/Remoting/0.2.1.4/Content/functions%5CGet-RemoteCert.ps1
function check_ssl {
    #Create a TCP Socket to the host and a port number
    $tcpsocket = New-Object System.Net.Sockets.Socket( `
            [System.Net.Sockets.SocketType]::Stream,
        [System.Net.Sockets.ProtocolType]::Tcp)
    $tcpsocket.connect($esx, $port)

    #Test if the socket got connected
    if (!$tcpsocket) {
        Write-Host 'Error Opening Connection: 443 on' $esx 'Host unreachable, cannot test cert, make sure host is functioning, terminating script now!'
        Write-Log ("Error Opening Connection: 443 on $($esx) Host unreachable, cannot test cert, make sure host is functioning, terminating script now!")
        Exit
    }
    else {
        #Socket got connected get the TCP stream ready to read the certificate
        Write-Host 'Successfully connected to' $esx 'on port' $port
        $tcpstream = New-Object System.Net.Sockets.NetworkStream($tcpsocket, $true)
        Write-Host 'Reading SSL certificate...'
        #Create an SSL Connection
        $sslstream = New-Object System.Net.Security.SslStream($tcpstream, $true)
        #Force the SSL Connection to send us the certificate
        $sslstream.AuthenticateAsClient($esx, $null, $protocolname, $false)
        #Read the certificate
        $certinfo = [System.Security.Cryptography.X509Certificates.X509Certificate2]$sslstream.remotecertificate
        if ($certinfo.SerialNumber -eq $wildcardserial) {
            Write-Host 'Certificate on' $esx 'OK!'
            Write-Log ("Certificate on $($esx) OK!")
        }
        else {
            Write-Host 'Certificate test on' $esx 'failed, check cert manually, moving on to next host!'
            Write-Log ("Certificate test on $($esx) failed, check cert manually, moving on to next host!") 
        }
    }   
}

#Variables
$vcserver = Read-Host -Prompt 'Enter vCenter name'
$cluster = Read-Host -Prompt 'Enter Host Cluster name'
$vccreds = Get-Credential -Message "Please enter VC creds"
$esxcreds = Get-Credential -Message "Please enter ESX SSH creds"
$esxnewsslpath = Read-Host -Prompt 'Enter path to CA signed cert and key e.g c:\temp\new-cert'
$logfilepath = Read-Host -Prompt 'Enter path to log file e.g c:\temp\new-cert'
$date = (Get-Date).toString("yyyy-MM-dd_HH-mm-ss")
$logfile = New-Item $logfilepath\log-file-$date.txt -Force
$wildcardserial = Read-Host -Prompt 'Enter wildcard cert serial'

connect-viserver $vcserver -username $vcusername -credential $vccreds

Write-Host 'Connected to' $vcserver
Write-Log ("Connected to $($vcserver)")

$esxhosts = Get-Cluster $cluster | Get-VMHost
#Use below to test script by importing multiple host names from CSV
#$csv = (Import-Csv $esxnewsslpath\esx-hosts.csv).Name
#$esxhosts = Get-VMHost $csv
#Use below to test script on a single host
#$esxhosts = Get-VMHost HOST NAME

foreach ($esx in $esxhosts) {

    Write-Host 'Putting' $esx 'in Maintenance Mode'
    Write-Log ("Putting $($esx) in Maintenance Mode")
    
    # Place the selected host into Maintenance Mode.
    $esx | Set-vmhost -State Maintenance

    while ((Get-VMHost $esx).ConnectionState -ne 'Maintenance') {
          
        Start-Sleep -Seconds 10
    }

    Write-Host $esx 'is in Maintenance Mode' 
    Write-Log ("$($esx) is in Maintenance Mode") 

    Write-Host 'Checking' $esx 'Lockdown Mode status' 
    Write-Log ("Checking $($esx) Lockdown Mode status") 

    If ((($esx | Get-View).config.admindisabled) -eq $true) {

        Write-Host 'Disabling Lockdown Mode on' $esx 
        Write-Log ("Disabling Lockdown Mode on $($esx)")

        #Disable Lockdown Mode
        ($esx | Get-View).ExitLockdownMode()
    }
    else {

        Write-Host 'Lockdown Mode on' $esx 'is already disabled'
        Write-Log ("Lockdown Mode on $($esx) is already disabled")    
    }

    #More variables
    $esxusername = $esxcreds.GetNetworkCredential().username	
    $esxpassword = $esxcreds.GetNetworkCredential().password
    $esxsslpath = "/etc/vmware/ssl"

    Write-Host 'Checking SSH service status on' $esx
    Write-Log ("Checking SSH service status on $($esx)")

    #Start SSH service on Host
    $sshservice = (Get-VMHostService -VMHost $esx -Server $vcserver | Where { $_.Key -eq "TSM-SSH" })

    if ($sshservice.Running -eq $false) {
        
        Write-Host 'Starting SSH service on' $esx
        Write-Log ("Starting SSH service on $($esx)")

        Start-VMHostService -HostService $sshservice -Confirm:$false
    }
    else {

        Write-Host 'SSH service is already running on' $esx 
        Write-Log ("SSH service is already running on $($esx)")   
    }

    Write-Host 'Checking SSH credentials for' $esx
    Write-Log ("Checking SSH credentials for $($esx)")

    #Check ESX authentication
    #Run once with no -batch to accept ssh key
    Echo "y" | pscp.exe -scp -pw $esxpassword -ls $esxusername@"$esx":$esxsslpath
    #Run again with -batch to populate variable 
    $auth = (Echo "y" | pscp.exe -scp -batch -pw $esxpassword -ls $esxusername@"$esx":$esxsslpath)

    if ($auth -eq $null) {
    
        Write-Host 'Authentication check for' $esx 'was unsuccessful, check your ESX root creds and start again, terminating script now!'
        Write-Log ("Authentication check for $($esx) was unsuccessful, check your ESX root creds and start again, terminating script now")
        Stop-VMHostService -HostService $sshservice -Confirm:$false
        ($esx | Get-View).EnterLockdownMode()
        Exit   
    }
    else {

        Write-Host 'Authentication check for' $esx 'was successful!'
        Write-Log ("Authentication check for $($esx) was successful!") 
    }

    Write-Host 'Creating ESX cert backup path on' $esx
    Write-Log ("Creating ESX cert backup path on $($esx)")

    #Get ESX scratch partition
    $esxscratch = $esx | Get-AdvancedSetting -Name "ScratchConfig.CurrentScratchLocation"
    $esxsslbkppath = $esxscratch.value

    #Create folder on ESX scratch partition to backup certs
    echo "y" | plink.exe -batch -ssh -pw $esxpassword $esxusername@$esx "mkdir -p $esxsslbkppath/certs"

    Write-Host 'Backing up ESX cert and key to backup path'
    Write-Log ("Backing up ESX cert and key to backup path $($esxsslbkppath)")
    
    #Backup current certs
    echo "y" | plink.exe  -batch -ssh -pw $esxpassword $esxusername@"$esx" "cp -f $esxsslpath/rui.crt $esxsslbkppath/certs" 
    echo "y" | plink.exe  -batch -ssh -pw $esxpassword $esxusername@"$esx" "cp -f $esxsslpath/rui.key $esxsslbkppath/certs" 

    Write-Host 'Uploading new ESX cert and key'
    Write-Log ("Uploading new ESX cert and key to $($esx)")

    #Upload new certs
    echo "y" | pscp.exe -scp -pw $esxpassword "$esxnewsslpath\rui.crt" $esxusername@"$esx":"$esxsslpath"  
    echo "y" | pscp.exe -scp -pw $esxpassword "$esxnewsslpath\rui.key" $esxusername@"$esx":"$esxsslpath"

    Write-Host 'Stopping SSH service on' $esx
    Write-Log ("Stopping SSH service on $($esx)")

    #Stop SSH service on Host
    Stop-VMHostService -HostService $sshservice -Confirm:$false

    Write-Host 'Enabling Lockdown Mode on' $esx
    Write-Log ("Enabling Lockdown Mode on $($esx)")

    #Enable Lockdown mode
    ($esx | Get-View).EnterLockdownMode()

    Write-Host 'Rebooting' $esx
    Write-Log ("Rebooting $($esx)")
    
    #Reboot host
    Restart-VMHost $esx -confirm:$false -force

    while ((Get-VMHost $esx).ConnectionState -ne 'NotResponding') {

        Start-Sleep -Seconds 10
    }

    Write-Host 'Still rebooting' $esx  
    Write-Log ("Still rebooting $($esx)") 

    while ((Get-VMHost $esx).ConnectionState -ne 'Maintenance') {

        #In my environment hosts take about 5-10 minutes to reboot
        Start-Sleep -Seconds 600
        #This if/else statement is for when the host after successful reboot doesn't automatically connect back to vCenter
        #If the connection is forced and host is still not connected, the sleep cycle will start again - won't stop until host is connected and is in maintenance
        #Make sure host is healthy and is in a rebooting state if the sleep cycle starts second time around
        #Set-VMHost cmd within if will throw an error, that's OK!
        if ((Get-VMHost $esx).ConnectionState -ne 'Maintenance') {
            Set-VMHost -VMHost $esx -State Maintenance
            Start-Sleep -Seconds 10
            Write-Host $esx 'was forced to connect'
            Write-Log ("$($esx) was forced to connect")
        }
        else {
            Write-Host $esx 'connected back automatically'
            Write-Log ("$($esx) connected back automatically")
        }
    }

    #Dont know why but vCenter needs this Disconnect / Connect step to accept the new cert, reboot is not enough, otherwise I've experienced issues with vSphere HA
    Write-Host 'Disconnecting' $esx 'from vCenter'
    Write-Log ("Disconnecting $($esx) from vCenter")

    Get-VMHost -Name $esx | set-vmhost -State Disconnected

    Start-Sleep -Seconds 10

    Write-Host 'Connecting' $esx 'to vCenter'
    Write-Log ("Connecting $($esx) to vCenter")

    #Set-VMHost cmd will throw an error, that's OK!
    Get-VMHost -Name $esx | set-vmhost -State Maintenance

    Start-Sleep -Seconds 10

    Write-Host $esx 'reboot process complete, taking host out of Maintenance Mode'
    Write-Log ("$($esx) reboot process complete, taking host out of Maintenance Mode")

    Get-VMHost -Name $esx | set-vmhost -State Connected

    while ((Get-VMHost $esx).ConnectionState -ne 'Connected') {

        Start-Sleep -Seconds 10
    }

    Write-Host $esx 'is out of Maintenance Mode'
    Write-Log ("$($esx) is out of Maintenance Mode")

    #Test certificate
    #More variables
    $protocolname = "tls12"
    $port = "443"
    check_ssl
}

Write-Host 'Disconnecting' $vcserver
Write-Log ("Disconnecting $($vcserver)")

#Disconnect vCenter server
Disconnect-VIServer $vcserver -Confirm:$false

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.