Update vCenter-SSL.ps1

This commit is contained in:
2025-11-15 23:52:45 -06:00
parent d686599cf0
commit 0d413a623e

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env pwsh #!/usr/bin/env pwsh
# ----------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------
# vCenter + Posh-ACME + PowerCLI Machine SSL automation # vCenter Machine SSL automation for vSphere 8.0 U3
# - Uses Let's Encrypt via Posh-ACME # - Uses Let's Encrypt via Posh-ACME (PowerDNS)
# - Does NOT re-issue if existing cert has >30 days left # - Skips issuance if cert >30 days
# - Still pushes cert to vCenter if thumbprint differs # - Pushes Posh-ACME cert into vCenter using Set-VIMachineCertificate
# - Uses Set-VIMachineCertificate (no direct REST /rest/... calls) # - Works WITHOUT deprecated REST endpoints (no 404s)
# ----------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------
. /opt/idssys/nodemgmt/conf/powerwall/settings.ps1 . /opt/idssys/nodemgmt/conf/powerwall/settings.ps1
@@ -12,15 +12,10 @@
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
# ---------------------------- # ----------------------------
# Config / constants # Logging
# ---------------------------- # ----------------------------
$LogFile = "/opt/idssys/nodemgmt/logs/vc-ssl.log" $LogFile = "/opt/idssys/nodemgmt/logs/vc-ssl.log"
$RenewalWindow = 30 # days: don't re-issue if cert still valid longer than this
$DnsSleep = 15 # seconds for Posh-ACME DNS propagation
# ----------------------------
# Logging helper
# ----------------------------
function Write-Log { function Write-Log {
param( param(
[Parameter(Mandatory)][ValidateSet('INFO','WARN','ERROR')] [Parameter(Mandatory)][ValidateSet('INFO','WARN','ERROR')]
@@ -28,288 +23,223 @@ function Write-Log {
[Parameter(Mandatory)] [Parameter(Mandatory)]
[string]$Message [string]$Message
) )
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$line = "[{0}] {1}: {2}" -f $ts, $Level, $Message $line = "[{0}] {1}: {2}" -f $ts,$Level,$Message
Write-Host $line Write-Host $line
try { try {
$dir = Split-Path $LogFile -Parent $dir = Split-Path $LogFile -Parent
if ($dir -and -not (Test-Path $dir)) { if (-not (Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null New-Item -Path $dir -ItemType Directory -Force | Out-Null
} }
Add-Content -Path $LogFile -Value $line Add-Content -Path $LogFile -Value $line
} catch { } catch {}
Write-Host "[WARN] Failed to write to log file $LogFile : $($_.Exception.Message)" -ForegroundColor Yellow
}
} }
# ----------------------------
# Error handler
# ----------------------------
function Show-Failure { function Show-Failure {
param([System.Management.Automation.ErrorRecord]$ErrorRecord) param([System.Management.Automation.ErrorRecord]$ErrorRecord)
Write-Log ERROR $ErrorRecord.Exception.Message
$msg = $ErrorRecord.Exception.Message Write-Host "======================================================" -ForegroundColor Red
Write-Log -Level 'ERROR' -Message $msg Write-Host "ERROR: $($ErrorRecord.Exception.Message)" -ForegroundColor Red
Write-Host "======================================================" -ForegroundColor Red
Write-Host "================================================================" -ForegroundColor Red
Write-Host "ERROR: A system exception was caught." -ForegroundColor Red
Write-Host $msg -ForegroundColor Red
Write-Host "================================================================" -ForegroundColor Red
exit 1 exit 1
} }
# ---------------------------- # ----------------------------
# Ensure modules # Safety constants
# ----------------------------
$RenewalWindow = 30
$DnsSleep = 15
# ----------------------------
# Ensure PowerCLI + Posh-ACME
# ---------------------------- # ----------------------------
try { try {
if (-not (Get-Module -ListAvailable -Name VMware.PowerCLI)) { if (-not (Get-Module -ListAvailable -Name VMware.PowerCLI)) {
Write-Log INFO "VMware.PowerCLI not found. Installing..." Write-Log INFO "Installing VMware.PowerCLI..."
Install-Module -Name VMware.PowerCLI -Force -Scope AllUsers -AllowClobber Install-Module -Name VMware.PowerCLI -Scope AllUsers -Force -AllowClobber
} }
Import-Module VMware.PowerCLI -ErrorAction Stop Import-Module VMware.PowerCLI -ErrorAction Stop
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null
Write-Log INFO "VMware.PowerCLI loaded." Write-Log INFO "VMware.PowerCLI loaded."
} catch { } catch { Show-Failure $_ }
Show-Failure $_
}
try { try {
if (-not (Get-Module -ListAvailable -Name Posh-ACME)) { if (-not (Get-Module -ListAvailable -Name Posh-ACME)) {
Write-Log INFO "Posh-ACME not found. Installing..." Write-Log INFO "Installing Posh-ACME..."
Install-Module -Name Posh-ACME -Force -Scope AllUsers Install-Module -Name Posh-ACME -Scope AllUsers -Force
} }
Import-Module Posh-ACME -ErrorAction Stop Import-Module Posh-ACME -ErrorAction Stop
Write-Log INFO "Posh-ACME loaded." Write-Log INFO "Posh-ACME loaded."
} catch { } catch { Show-Failure $_ }
Show-Failure $_
}
# ---------------------------- # ----------------------------
# Connect to vCenter # Connect to vCenter
# ---------------------------- # ----------------------------
try { try {
Write-Log INFO "Connecting to vCenter $VCENTERHOST..." Write-Log INFO "Connecting to vCenter $VCENTERHOST..."
$null = Connect-VIServer -Server $VCENTERHOST -User $VCENTERUSER -Password $VCENTERPASS -Force Connect-VIServer -Server $VCENTERHOST -User $VCENTERUSER -Password $VCENTERPASS -Force | Out-Null
Write-Host "========================================" Write-Host "========================================"
Write-Host "SUCCESS: Connected to vCenter." Write-Host "SUCCESS: Connected to vCenter."
Write-Host "========================================" Write-Host "========================================"
} catch { } catch { Show-Failure $_ }
Show-Failure $_
}
# ---------------------------- # ----------------------------
# Optional: list VMs (sanity check) # Get VM list (sanity)
# ---------------------------- # ----------------------------
try { try {
$vms = Get-VM $vms = Get-VM
Write-Log INFO "Retrieved $($vms.Count) VMs from vCenter." Write-Log INFO "Retrieved $($vms.Count) VMs from vCenter."
} catch { } catch {
Write-Log WARN "Unable to retrieve VM list. Continuing anyway. Error: $($_.Exception.Message)" Write-Log WARN "Could not retrieve VM list: $($_.Exception.Message)"
} }
# ---------------------------- # ----------------------------
# Posh-ACME: find existing cert for VCENTERHOST # Locate Posh-ACME cert object
# ---------------------------- # ----------------------------
$paCert = $null $paCert = $null
try { try {
$allPaCerts = Get-PACertificate -List $allPaCerts = Get-PACertificate -List
if ($allPaCerts) { if ($allPaCerts) {
Write-Log INFO "Found $($allPaCerts.Count) Posh-ACME cert(s)." Write-Log INFO "Found $($allPaCerts.Count) Posh-ACME cert(s)."
$allPaCerts | foreach ($c in $allPaCerts) {
ForEach-Object { Write-Log INFO (" MainDomain={0} Exp={1}" -f $c.MainDomain, $c.NotAfter)
Write-Log INFO (" MainDomain={0} SANs={1} Exp={2}" -f $_.MainDomain, ($_.AllSANs -join ','), $_.NotAfter) }
}
$paCert = $allPaCerts | $paCert = $allPaCerts |
Where-Object { Where-Object { $_.MainDomain -eq $VCENTERHOST -or ($_.AllSANs -contains $VCENTERHOST) } |
$_.MainDomain -eq $VCENTERHOST -or ($_.AllSANs -contains $VCENTERHOST)
} |
Sort-Object NotAfter -Descending | Sort-Object NotAfter -Descending |
Select-Object -First 1 Select-Object -First 1
} }
} catch { } catch {
Write-Log WARN "Get-PACertificate failed: $($_.Exception.Message)" Write-Log WARN "Failed to query Posh-ACME certs: $($_.Exception.Message)"
} }
$needNewCert = $false $needNewCert = $false
if ($paCert) { if ($paCert) {
$daysLeft = ($paCert.NotAfter - (Get-Date)).TotalDays $daysLeft = ($paCert.NotAfter - (Get-Date)).TotalDays
Write-Log INFO ("Existing cert expires {0} (~{1:N0} days)" -f $paCert.NotAfter, $daysLeft) Write-Log INFO "Existing cert expires $($paCert.NotAfter) (~$([math]::Round($daysLeft)) days)."
if ($daysLeft -le $RenewalWindow) { $needNewCert = $true }
if ($daysLeft -le $RenewalWindow) { else { Write-Log INFO "Skipping ACME issuance: certificate valid >30 days." }
Write-Log INFO "Existing cert within $RenewalWindow days of expiry. Will request new ACME certificate."
$needNewCert = $true
} else {
Write-Log INFO "Skipping ACME issuance: Existing certificate valid >$RenewalWindow days."
}
} else { } else {
Write-Log WARN "No existing Posh-ACME certificate found for $VCENTERHOST. Will request new certificate." Write-Log WARN "No existing Posh-ACME cert found. Will issue new."
$needNewCert = $true $needNewCert = $true
} }
# ---------------------------- # ----------------------------
# ACME / Posh-ACME certificate issuance (if needed) # ACME issuance (if needed)
# ---------------------------- # ----------------------------
if ($needNewCert) { if ($needNewCert) {
try { try {
# Build PowerDNS plugin args $pluginArgs = @{
$pArgs = @{
PowerDNSApiHost = $WDNSHOST PowerDNSApiHost = $WDNSHOST
PowerDNSApiKey = (if ($PDNSAPI -is [string]) { ConvertTo-SecureString $PDNSAPI -AsPlainText -Force } else { $PDNSAPI }) PowerDNSApiKey = (ConvertTo-SecureString $PDNSAPI -AsPlainText -Force)
PowerDNSUseTLS = $true PowerDNSUseTLS = $true
PowerDNSPort = 443 PowerDNSPort = 443
PowerDNSServerName = 'localhost' PowerDNSServerName = "localhost"
} }
Write-Log INFO "Requesting new ACME certificate via Posh-ACME..." Write-Log INFO "Requesting new ACME certificate..."
New-PACertificate ` New-PACertificate `
-MainDomain $VCENTERHOST ` -MainDomain $VCENTERHOST `
-Plugin PowerDNS ` -Plugin PowerDNS `
-PluginArgs $pArgs ` -PluginArgs $pluginArgs `
-Contact $ACMEEMAIL ` -Contact $ACMEEMAIL `
-AcceptTOS ` -AcceptTOS `
-DnsSleep $DnsSleep ` -DnsSleep $DnsSleep `
-Force ` -Verbose -Force
-Verbose
# Refresh cert info
$paCert = Get-PACertificate $paCert = Get-PACertificate
Write-Log INFO ("New cert issued. NotAfter={0}" -f $paCert.NotAfter) Write-Log INFO "New ACME certificate issued."
} catch { }
Write-Log ERROR ("ACME request failed: {0}" -f $_.Exception.Message) catch {
Write-Host "================================================================" -ForegroundColor Yellow Write-Log ERROR "ACME issuance failed: $($_.Exception.Message)"
Write-Host "WARN: Lets Encrypt may have rate-limited or rejected the request." -ForegroundColor Yellow
Write-Host "================================================================" -ForegroundColor Yellow
if (-not $paCert) { if (-not $paCert) {
Write-Host "==================================================" -ForegroundColor Red Write-Host "No fallback certificate exists; aborting." -ForegroundColor Red
Write-Host "ERROR: No usable certificate available. Aborting." -ForegroundColor Red
Write-Host "==================================================" -ForegroundColor Red
exit 1 exit 1
} else {
Write-Log WARN "Falling back to existing Posh-ACME certificate despite issuance failure."
} }
Write-Log WARN "Falling back to existing certificate."
} }
} }
# Ensure we have a cert object now
if (-not $paCert) {
Write-Log ERROR "No Posh-ACME certificate object available. Aborting."
exit 1
}
# ---------------------------- # ----------------------------
# Resolve cert file paths from Posh-ACME # Locate certificate files (LEAF ONLY!)
# ---------------------------- # ----------------------------
$certFolder = Split-Path $paCert.CertFile -Parent $certFolder = Split-Path $paCert.CertFile -Parent
if (-not $certFolder) {
Write-Log ERROR "Unable to determine certificate folder from Posh-ACME. Aborting."
exit 1
}
$certPath = $paCert.FullChainFile # cert + chain (ideal for browsers) $certPath = Join-Path $certFolder "cert.cer" # LEAF ONLY
$keyPath = $paCert.KeyFile # private key $keyPath = Join-Path $certFolder "cert.key" # PRIVATE KEY
$chainPath = $paCert.ChainFile # issuing CA chain only (for Add-VITrustedCertificate) $chainPath = Join-Path $certFolder "chain.cer" # INTERMEDIATE
Write-Log INFO "Using Posh-ACME cert folder: $certFolder" Write-Log INFO "Using cert folder: $certFolder"
Write-Log INFO "Using certificate files:" Write-Log INFO " CERT(leaf) : $certPath"
Write-Log INFO " CERT : $certPath" Write-Log INFO " KEY : $keyPath"
Write-Log INFO " KEY : $keyPath" Write-Log INFO " CHAIN : $chainPath"
Write-Log INFO " CHAIN: $chainPath"
foreach ($f in @($certPath, $keyPath, $chainPath)) { foreach ($f in @($certPath,$keyPath,$chainPath)) {
if (-not (Test-Path $f)) { if (-not (Test-Path $f)) {
Write-Log ERROR "Missing certificate file: $f" Write-Log ERROR "Missing certificate file: $f"
Write-Host ""
Write-Host "================================================================" -ForegroundColor Red
Write-Host "ERROR: Missing certificate file: $f" -ForegroundColor Red
Write-Host "================================================================" -ForegroundColor Red
exit 1 exit 1
} }
} }
# ---------------------------- # ----------------------------
# Ensure CA chain is trusted in vCenter # Add chain to trusted store
# ---------------------------- # ----------------------------
try { try {
$trustedChainPem = Get-Content $chainPath -Raw Write-Log INFO "Adding/updating CA chain to vCenter trusted store..."
if ($trustedChainPem -and $trustedChainPem.Trim().Length -gt 0) { $pemChain = Get-Content $chainPath -Raw
Write-Log INFO "Adding/updating CA chain in vCenter trusted store (if not already present)..." if ($pemChain.Trim().Length -gt 0) {
Add-VITrustedCertificate -PemCertificateOrChain $trustedChainPem -VCenterOnly -ErrorAction SilentlyContinue | Out-Null Add-VITrustedCertificate -PemCertificateOrChain $pemChain -VCenterOnly -ErrorAction SilentlyContinue | Out-Null
} else {
Write-Log WARN "CHAIN file appears empty; skipping trusted root update."
} }
} catch { } catch {
Write-Log WARN "Add-VITrustedCertificate failed (non-fatal): $($_.Exception.Message)" Write-Log WARN "Failed to add trusted chain: $($_.Exception.Message)"
} }
# ---------------------------- # ----------------------------
# Compare current vCenter machine cert vs Posh-ACME cert # Compare current vCenter cert
# ---------------------------- # ----------------------------
$currentVcCert = $null $vcCert = $null
try { try { $vcCert = Get-VIMachineCertificate -VCenterOnly }
$currentVcCert = Get-VIMachineCertificate -VCenterOnly catch { Write-Log WARN "Unable to read current vCenter cert: $($_.Exception.Message)" }
} catch {
Write-Log WARN "Get-VIMachineCertificate failed: $($_.Exception.Message). Will assume update is required."
}
$needPushToVc = $true $needPush = $true
if ($currentVcCert) { if ($vcCert) {
Write-Log INFO ("Current vCenter cert: Subject='{0}' NotAfter={1} Thumbprint={2}" -f ` Write-Log INFO ("Current vCenter cert: Subject={0} NotAfter={1}" -f $vcCert.Subject,$vcCert.NotValidAfter)
$currentVcCert.Subject, $currentVcCert.NotValidAfter, $currentVcCert.Thumbprint) if ($vcCert.Thumbprint -eq $paCert.Thumbprint) {
Write-Log INFO "vCenter already using the Posh-ACME certificate."
if ($currentVcCert.Thumbprint -eq $paCert.Thumbprint) { $needPush = $false
Write-Log INFO "vCenter is already using the same certificate as Posh-ACME. No update needed."
$needPushToVc = $false
} else { } else {
Write-Log INFO "vCenter certificate differs from Posh-ACME cert. Update required." Write-Log INFO "vCenter cert differs; update required."
} }
} else {
Write-Log WARN "Could not read current vCenter machine cert. Assuming update is required."
} }
# ---------------------------- # ----------------------------
# Push certificate into vCenter via Set-VIMachineCertificate # Apply certificate (PowerCLI)
# ---------------------------- # ----------------------------
if ($needPushToVc) { if ($needPush) {
try { try {
Write-Log INFO "Uploading and applying certificate via Set-VIMachineCertificate..." $leafPem = Get-Content $certPath -Raw # ONE cert block
$keyPem = Get-Content $keyPath -Raw # private key
$pemCert = Get-Content $certPath -Raw # fullchain.cer (leaf + chain) Write-Log INFO "Applying certificate via Set-VIMachineCertificate..."
$pemKey = Get-Content $keyPath -Raw # cert.key (private key) Set-VIMachineCertificate -PemCertificate $leafPem -PemKey $keyPem | Out-Null
if (-not $pemCert.Trim()) { Write-Host "==========================================================="
throw "PEM certificate content appears empty." Write-Host "SUCCESS: vCenter Machine SSL certificate updated." -ForegroundColor Green
} Write-Host "==========================================================="
if (-not $pemKey.Trim()) {
throw "PEM key content appears empty."
}
# IMPORTANT: Write-Log INFO "Certificate applied successfully."
# Set-VIMachineCertificate understands a PEM cert + separate PEM private key.
# It wraps the vSphere /api/vcenter/certificate-management/vcenter/tls API for us.
Set-VIMachineCertificate -PemCertificate $pemCert -PemKey $pemKey | Out-Null
Write-Host "===================================================================="
Write-Host "SUCCESS: vCenter Machine SSL certificate has been updated." -ForegroundColor Green
Write-Host "===================================================================="
Write-Log INFO "Set-VIMachineCertificate completed successfully."
Write-Log INFO "Note: On vSphere 8.0 U2+ the swap is seamless; older versions may restart vCenter."
} catch { } catch {
Write-Log ERROR ("Failed to apply certificate via Set-VIMachineCertificate: {0}" -f $_.Exception.Message) Write-Log ERROR "Failed updating vCenter cert: $($_.Exception.Message)"
Write-Host "=============================================================================================" -ForegroundColor Red Write-Host "===========================================================" -ForegroundColor Red
Write-Host "ERROR: Failed to apply certificate via Set-VIMachineCertificate." -ForegroundColor Red Write-Host "ERROR applying certificate." -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red Write-Host $($_.Exception.Message) -ForegroundColor Red
Write-Host "=============================================================================================" -ForegroundColor Red Write-Host "===========================================================" -ForegroundColor Red
exit 1 exit 1
} }
} else { } else {
Write-Log INFO "Skipping vCenter certificate update because thumbprints already match." Write-Log INFO "Skipping cert update; already matched."
} }
# ---------------------------- # ----------------------------
@@ -317,4 +247,4 @@ if ($needPushToVc) {
# ---------------------------- # ----------------------------
Write-Host "==========================================================" -ForegroundColor Green Write-Host "==========================================================" -ForegroundColor Green
Write-Host "INFO: Script complete. Log: $LogFile" -ForegroundColor Green Write-Host "INFO: Script complete. Log: $LogFile" -ForegroundColor Green
Write-Host "==========================================================" -ForegroundColor Green Write-Host "=========================================================="