#!/usr/bin/env pwsh # ----------------------------------------------------------------------------------- # vCenter Machine SSL automation for vSphere 8.0 U3 # - Uses Let's Encrypt via Posh-ACME (PowerDNS) # - Skips issuance if cert >30 days # - Pushes Posh-ACME cert into vCenter using Set-VIMachineCertificate # - Works WITHOUT deprecated REST endpoints (no 404s) # ----------------------------------------------------------------------------------- . /opt/idssys/nodemgmt/conf/powerwall/settings.ps1 $ErrorActionPreference = 'Stop' # ---------------------------- # Logging # ---------------------------- $LogFile = "/opt/idssys/nodemgmt/logs/vc-ssl.log" function Write-Log { param( [Parameter(Mandatory)][ValidateSet('INFO','WARN','ERROR')] [string]$Level, [Parameter(Mandatory)] [string]$Message ) $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $line = "[{0}] {1}: {2}" -f $ts,$Level,$Message Write-Host $line try { $dir = Split-Path $LogFile -Parent if (-not (Test-Path $dir)) { New-Item -Path $dir -ItemType Directory -Force | Out-Null } Add-Content -Path $LogFile -Value $line } catch {} } function Show-Failure { param([System.Management.Automation.ErrorRecord]$ErrorRecord) Write-Log ERROR $ErrorRecord.Exception.Message Write-Host "======================================================" -ForegroundColor Red Write-Host "ERROR: $($ErrorRecord.Exception.Message)" -ForegroundColor Red Write-Host "======================================================" -ForegroundColor Red exit 1 } # ---------------------------- # Safety constants # ---------------------------- $RenewalWindow = 30 $DnsSleep = 15 # ---------------------------- # Ensure PowerCLI + Posh-ACME # ---------------------------- try { if (-not (Get-Module -ListAvailable -Name VMware.PowerCLI)) { Write-Log INFO "Installing VMware.PowerCLI..." Install-Module -Name VMware.PowerCLI -Scope AllUsers -Force -AllowClobber } Import-Module VMware.PowerCLI -ErrorAction Stop Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null Write-Log INFO "VMware.PowerCLI loaded." } catch { Show-Failure $_ } try { if (-not (Get-Module -ListAvailable -Name Posh-ACME)) { Write-Log INFO "Installing Posh-ACME..." Install-Module -Name Posh-ACME -Scope AllUsers -Force } Import-Module Posh-ACME -ErrorAction Stop Write-Log INFO "Posh-ACME loaded." } catch { Show-Failure $_ } # ---------------------------- # Connect to vCenter # ---------------------------- try { Write-Log INFO "Connecting to vCenter $VCENTERHOST..." Connect-VIServer -Server $VCENTERHOST -User $VCENTERUSER -Password $VCENTERPASS -Force | Out-Null Write-Host "========================================" Write-Host "SUCCESS: Connected to vCenter." Write-Host "========================================" } catch { Show-Failure $_ } # ---------------------------- # Get VM list (sanity) # ---------------------------- try { $vms = Get-VM Write-Log INFO "Retrieved $($vms.Count) VMs from vCenter." } catch { Write-Log WARN "Could not retrieve VM list: $($_.Exception.Message)" } # ---------------------------- # Locate Posh-ACME cert object # ---------------------------- $paCert = $null try { $allPaCerts = Get-PACertificate -List if ($allPaCerts) { Write-Log INFO "Found $($allPaCerts.Count) Posh-ACME cert(s)." foreach ($c in $allPaCerts) { Write-Log INFO (" MainDomain={0} Exp={1}" -f $c.MainDomain, $c.NotAfter) } $paCert = $allPaCerts | Where-Object { $_.MainDomain -eq $VCENTERHOST -or ($_.AllSANs -contains $VCENTERHOST) } | Sort-Object NotAfter -Descending | Select-Object -First 1 } } catch { Write-Log WARN "Failed to query Posh-ACME certs: $($_.Exception.Message)" } $needNewCert = $false if ($paCert) { $daysLeft = ($paCert.NotAfter - (Get-Date)).TotalDays Write-Log INFO "Existing cert expires $($paCert.NotAfter) (~$([math]::Round($daysLeft)) days)." if ($daysLeft -le $RenewalWindow) { $needNewCert = $true } else { Write-Log INFO "Skipping ACME issuance: certificate valid >30 days." } } else { Write-Log WARN "No existing Posh-ACME cert found. Will issue new." $needNewCert = $true } # ---------------------------- # ACME issuance (if needed) # ---------------------------- if ($needNewCert) { try { $pluginArgs = @{ PowerDNSApiHost = $WDNSHOST PowerDNSApiKey = (ConvertTo-SecureString $PDNSAPI -AsPlainText -Force) PowerDNSUseTLS = $true PowerDNSPort = 443 PowerDNSServerName = "localhost" } Write-Log INFO "Requesting new ACME certificate..." New-PACertificate ` -MainDomain $VCENTERHOST ` -Plugin PowerDNS ` -PluginArgs $pluginArgs ` -Contact $ACMEEMAIL ` -AcceptTOS ` -DnsSleep $DnsSleep ` -Verbose -Force $paCert = Get-PACertificate Write-Log INFO "New ACME certificate issued." } catch { Write-Log ERROR "ACME issuance failed: $($_.Exception.Message)" if (-not $paCert) { Write-Host "No fallback certificate exists; aborting." -ForegroundColor Red exit 1 } Write-Log WARN "Falling back to existing certificate." } } # ---------------------------- # Locate certificate files (LEAF ONLY!) # ---------------------------- $certFolder = Split-Path $paCert.CertFile -Parent $certPath = Join-Path $certFolder "cert.cer" # LEAF ONLY $keyPath = Join-Path $certFolder "cert.key" # PRIVATE KEY $chainPath = Join-Path $certFolder "chain.cer" # INTERMEDIATE Write-Log INFO "Using cert folder: $certFolder" Write-Log INFO " CERT(leaf) : $certPath" Write-Log INFO " KEY : $keyPath" Write-Log INFO " CHAIN : $chainPath" foreach ($f in @($certPath,$keyPath,$chainPath)) { if (-not (Test-Path $f)) { Write-Log ERROR "Missing certificate file: $f" exit 1 } } # ---------------------------- # Add chain to trusted store # ---------------------------- try { Write-Log INFO "Adding/updating CA chain to vCenter trusted store..." $pemChain = Get-Content $chainPath -Raw if ($pemChain.Trim().Length -gt 0) { Add-VITrustedCertificate -PemCertificateOrChain $pemChain -VCenterOnly -ErrorAction SilentlyContinue | Out-Null } } catch { Write-Log WARN "Failed to add trusted chain: $($_.Exception.Message)" } # ---------------------------- # Compare current vCenter cert # ---------------------------- $vcCert = $null try { $vcCert = Get-VIMachineCertificate -VCenterOnly } catch { Write-Log WARN "Unable to read current vCenter cert: $($_.Exception.Message)" } $needPush = $true if ($vcCert) { Write-Log INFO ("Current vCenter cert: Subject={0} NotAfter={1}" -f $vcCert.Subject,$vcCert.NotValidAfter) if ($vcCert.Thumbprint -eq $paCert.Thumbprint) { Write-Log INFO "vCenter already using the Posh-ACME certificate." $needPush = $false } else { Write-Log INFO "vCenter cert differs; update required." } } # ---------------------------- # Apply certificate (PowerCLI) # ---------------------------- if ($needPush) { try { $leafPem = Get-Content $certPath -Raw # ONE cert block $keyPem = Get-Content $keyPath -Raw # private key Write-Log INFO "Applying certificate via Set-VIMachineCertificate..." Set-VIMachineCertificate -PemCertificate $leafPem -PemKey $keyPem | Out-Null Write-Host "===========================================================" Write-Host "SUCCESS: vCenter Machine SSL certificate updated." -ForegroundColor Green Write-Host "===========================================================" Write-Log INFO "Certificate applied successfully." } catch { Write-Log ERROR "Failed updating vCenter cert: $($_.Exception.Message)" Write-Host "===========================================================" -ForegroundColor Red Write-Host "ERROR applying certificate." -ForegroundColor Red Write-Host $($_.Exception.Message) -ForegroundColor Red Write-Host "===========================================================" -ForegroundColor Red exit 1 } } else { Write-Log INFO "Skipping cert update; already matched." } # ---------------------------- # Done # ---------------------------- Write-Host "==========================================================" -ForegroundColor Green Write-Host "INFO: Script complete. Log: $LogFile" -ForegroundColor Green Write-Host "=========================================================="