From 0d413a623e80aba34d76ff51ffaea4191b0f8095 Mon Sep 17 00:00:00 2001 From: David Schroeder Date: Sat, 15 Nov 2025 23:52:45 -0600 Subject: [PATCH] Update vCenter-SSL.ps1 --- inc/vCenter-SSL.ps1 | 266 ++++++++++++++++---------------------------- 1 file changed, 98 insertions(+), 168 deletions(-) diff --git a/inc/vCenter-SSL.ps1 b/inc/vCenter-SSL.ps1 index 31ac960d..e3a925c0 100644 --- a/inc/vCenter-SSL.ps1 +++ b/inc/vCenter-SSL.ps1 @@ -1,10 +1,10 @@ #!/usr/bin/env pwsh # ----------------------------------------------------------------------------------- -# vCenter + Posh-ACME + PowerCLI Machine SSL automation -# - Uses Let's Encrypt via Posh-ACME -# - Does NOT re-issue if existing cert has >30 days left -# - Still pushes cert to vCenter if thumbprint differs -# - Uses Set-VIMachineCertificate (no direct REST /rest/... calls) +# 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 @@ -12,15 +12,10 @@ $ErrorActionPreference = 'Stop' # ---------------------------- -# Config / constants +# Logging # ---------------------------- -$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 +$LogFile = "/opt/idssys/nodemgmt/logs/vc-ssl.log" -# ---------------------------- -# Logging helper -# ---------------------------- function Write-Log { param( [Parameter(Mandatory)][ValidateSet('INFO','WARN','ERROR')] @@ -28,288 +23,223 @@ function Write-Log { [Parameter(Mandatory)] [string]$Message ) - $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 try { $dir = Split-Path $LogFile -Parent - if ($dir -and -not (Test-Path $dir)) { - New-Item -ItemType Directory -Path $dir -Force | Out-Null + if (-not (Test-Path $dir)) { + New-Item -Path $dir -ItemType Directory -Force | Out-Null } Add-Content -Path $LogFile -Value $line - } catch { - Write-Host "[WARN] Failed to write to log file $LogFile : $($_.Exception.Message)" -ForegroundColor Yellow - } + } catch {} } -# ---------------------------- -# Error handler -# ---------------------------- function Show-Failure { param([System.Management.Automation.ErrorRecord]$ErrorRecord) - - $msg = $ErrorRecord.Exception.Message - Write-Log -Level 'ERROR' -Message $msg - - Write-Host "================================================================" -ForegroundColor Red - Write-Host "ERROR: A system exception was caught." -ForegroundColor Red - Write-Host $msg -ForegroundColor Red - Write-Host "================================================================" -ForegroundColor Red - + Write-Log ERROR $ErrorRecord.Exception.Message + Write-Host "======================================================" -ForegroundColor Red + Write-Host "ERROR: $($ErrorRecord.Exception.Message)" -ForegroundColor Red + Write-Host "======================================================" -ForegroundColor Red exit 1 } # ---------------------------- -# Ensure modules +# Safety constants +# ---------------------------- +$RenewalWindow = 30 +$DnsSleep = 15 + +# ---------------------------- +# Ensure PowerCLI + Posh-ACME # ---------------------------- try { if (-not (Get-Module -ListAvailable -Name VMware.PowerCLI)) { - Write-Log INFO "VMware.PowerCLI not found. Installing..." - Install-Module -Name VMware.PowerCLI -Force -Scope AllUsers -AllowClobber + 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 $_ -} +} catch { Show-Failure $_ } try { if (-not (Get-Module -ListAvailable -Name Posh-ACME)) { - Write-Log INFO "Posh-ACME not found. Installing..." - Install-Module -Name Posh-ACME -Force -Scope AllUsers + 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 $_ -} +} catch { Show-Failure $_ } # ---------------------------- # Connect to vCenter # ---------------------------- try { 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 "SUCCESS: Connected to vCenter." Write-Host "========================================" -} catch { - Show-Failure $_ -} +} catch { Show-Failure $_ } # ---------------------------- -# Optional: list VMs (sanity check) +# Get VM list (sanity) # ---------------------------- try { $vms = Get-VM Write-Log INFO "Retrieved $($vms.Count) VMs from vCenter." } 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 try { $allPaCerts = Get-PACertificate -List if ($allPaCerts) { Write-Log INFO "Found $($allPaCerts.Count) Posh-ACME cert(s)." - $allPaCerts | - ForEach-Object { - Write-Log INFO (" MainDomain={0} SANs={1} Exp={2}" -f $_.MainDomain, ($_.AllSANs -join ','), $_.NotAfter) - } - + 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) - } | + Where-Object { $_.MainDomain -eq $VCENTERHOST -or ($_.AllSANs -contains $VCENTERHOST) } | Sort-Object NotAfter -Descending | Select-Object -First 1 } } catch { - Write-Log WARN "Get-PACertificate failed: $($_.Exception.Message)" + 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 {0} (~{1:N0} days)" -f $paCert.NotAfter, $daysLeft) - - if ($daysLeft -le $RenewalWindow) { - 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." - } + 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 certificate found for $VCENTERHOST. Will request new certificate." + Write-Log WARN "No existing Posh-ACME cert found. Will issue new." $needNewCert = $true } # ---------------------------- -# ACME / Posh-ACME certificate issuance (if needed) +# ACME issuance (if needed) # ---------------------------- if ($needNewCert) { try { - # Build PowerDNS plugin args - $pArgs = @{ + $pluginArgs = @{ PowerDNSApiHost = $WDNSHOST - PowerDNSApiKey = (if ($PDNSAPI -is [string]) { ConvertTo-SecureString $PDNSAPI -AsPlainText -Force } else { $PDNSAPI }) + PowerDNSApiKey = (ConvertTo-SecureString $PDNSAPI -AsPlainText -Force) PowerDNSUseTLS = $true 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 ` -MainDomain $VCENTERHOST ` -Plugin PowerDNS ` - -PluginArgs $pArgs ` + -PluginArgs $pluginArgs ` -Contact $ACMEEMAIL ` -AcceptTOS ` -DnsSleep $DnsSleep ` - -Force ` - -Verbose + -Verbose -Force - # Refresh cert info $paCert = Get-PACertificate - Write-Log INFO ("New cert issued. NotAfter={0}" -f $paCert.NotAfter) - } catch { - Write-Log ERROR ("ACME request failed: {0}" -f $_.Exception.Message) - Write-Host "================================================================" -ForegroundColor Yellow - Write-Host "WARN: Let’s Encrypt may have rate-limited or rejected the request." -ForegroundColor Yellow - Write-Host "================================================================" -ForegroundColor Yellow - + Write-Log INFO "New ACME certificate issued." + } + catch { + Write-Log ERROR "ACME issuance failed: $($_.Exception.Message)" if (-not $paCert) { - Write-Host "==================================================" -ForegroundColor Red - Write-Host "ERROR: No usable certificate available. Aborting." -ForegroundColor Red - Write-Host "==================================================" -ForegroundColor Red + Write-Host "No fallback certificate exists; aborting." -ForegroundColor Red 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 -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) -$keyPath = $paCert.KeyFile # private key -$chainPath = $paCert.ChainFile # issuing CA chain only (for Add-VITrustedCertificate) +$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 Posh-ACME cert folder: $certFolder" -Write-Log INFO "Using certificate files:" -Write-Log INFO " CERT : $certPath" -Write-Log INFO " KEY : $keyPath" -Write-Log INFO " CHAIN: $chainPath" +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)) { +foreach ($f in @($certPath,$keyPath,$chainPath)) { if (-not (Test-Path $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 } } # ---------------------------- -# Ensure CA chain is trusted in vCenter +# Add chain to trusted store # ---------------------------- try { - $trustedChainPem = Get-Content $chainPath -Raw - if ($trustedChainPem -and $trustedChainPem.Trim().Length -gt 0) { - Write-Log INFO "Adding/updating CA chain in vCenter trusted store (if not already present)..." - Add-VITrustedCertificate -PemCertificateOrChain $trustedChainPem -VCenterOnly -ErrorAction SilentlyContinue | Out-Null - } else { - Write-Log WARN "CHAIN file appears empty; skipping trusted root update." + 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 "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 -try { - $currentVcCert = Get-VIMachineCertificate -VCenterOnly -} catch { - Write-Log WARN "Get-VIMachineCertificate failed: $($_.Exception.Message). Will assume update is required." -} +$vcCert = $null +try { $vcCert = Get-VIMachineCertificate -VCenterOnly } +catch { Write-Log WARN "Unable to read current vCenter cert: $($_.Exception.Message)" } -$needPushToVc = $true +$needPush = $true -if ($currentVcCert) { - Write-Log INFO ("Current vCenter cert: Subject='{0}' NotAfter={1} Thumbprint={2}" -f ` - $currentVcCert.Subject, $currentVcCert.NotValidAfter, $currentVcCert.Thumbprint) - - if ($currentVcCert.Thumbprint -eq $paCert.Thumbprint) { - Write-Log INFO "vCenter is already using the same certificate as Posh-ACME. No update needed." - $needPushToVc = $false +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 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 { - 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) - $pemKey = Get-Content $keyPath -Raw # cert.key (private key) + Write-Log INFO "Applying certificate via Set-VIMachineCertificate..." + Set-VIMachineCertificate -PemCertificate $leafPem -PemKey $keyPem | Out-Null - if (-not $pemCert.Trim()) { - throw "PEM certificate content appears empty." - } - if (-not $pemKey.Trim()) { - throw "PEM key content appears empty." - } + Write-Host "===========================================================" + Write-Host "SUCCESS: vCenter Machine SSL certificate updated." -ForegroundColor Green + Write-Host "===========================================================" - # IMPORTANT: - # 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." + Write-Log INFO "Certificate applied successfully." } catch { - Write-Log ERROR ("Failed to apply certificate via Set-VIMachineCertificate: {0}" -f $_.Exception.Message) - Write-Host "=============================================================================================" -ForegroundColor Red - Write-Host "ERROR: Failed to apply certificate via Set-VIMachineCertificate." -ForegroundColor Red - Write-Host $_.Exception.Message -ForegroundColor Red - Write-Host "=============================================================================================" -ForegroundColor Red + 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 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 "INFO: Script complete. Log: $LogFile" -ForegroundColor Green -Write-Host "==========================================================" -ForegroundColor Green +Write-Host "=========================================================="