Update vCenter-SSL.ps1

This commit is contained in:
2025-11-16 00:41:14 -06:00
parent f9c7996c4d
commit fb3dba1f3b

View File

@@ -1,18 +1,19 @@
#!/usr/bin/env pwsh #!/usr/bin/env pwsh
# ----------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------
# vCenter Machine SSL automation for vSphere 8.0 U3 # vCenter Machine SSL Automation Script (vSphere 8.0 U3)
# - Uses Let's Encrypt via Posh-ACME (PowerDNS) # - Uses Let's Encrypt via Posh-ACME and PowerDNS
# - Skips issuance if existing cert has >30 days remaining # - Skips issuance if existing cert has >30 days
# - Pushes Posh-ACME cert into vCenter using Set-VIMachineCertificate # - Pushes cert into vCenter using Set-VIMachineCertificate
# - Adds CA chain with Add-VITrustedCertificate # - Adds CA chain to trusted store
# - Automatically restarts vpxd via Restart-VIApplianceService # - Restarts vpxd (Restart-VIApplianceService)
# - Runs completely non-interactive (no prompts) # - Performs Veeam Rescan-VBREntity when certificate changes
# - Fully non-interactive (no prompts)
# ----------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------
. /opt/idssys/nodemgmt/conf/powerwall/settings.ps1 . /opt/idssys/nodemgmt/conf/powerwall/settings.ps1
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$ConfirmPreference = 'None' # Disable all confirmation prompts $ConfirmPreference = 'None' # Disable all PowerCLI confirmations
# ---------------------------- # ----------------------------
# Logging # Logging
@@ -21,32 +22,29 @@ $LogFile = "/opt/idssys/nodemgmt/logs/vc-ssl.log"
function Write-Log { function Write-Log {
param( param(
[Parameter(Mandatory)][ValidateSet('INFO','WARN','ERROR')] [ValidateSet('INFO','WARN','ERROR')]
[string]$Level, [string]$Level,
[Parameter(Mandatory)]
[string]$Message [string]$Message
) )
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$line = "[{0}] {1}: {2}" -f $ts, $Level, $Message $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$line = "[$ts] $Level: $Message"
Write-Host $line Write-Host $line
try { try {
$dir = Split-Path $LogFile -Parent $dir = Split-Path $LogFile
if ($dir -and -not (Test-Path $dir)) { if (-not (Test-Path $dir)) {
New-Item -Path $dir -ItemType Directory -Force | Out-Null New-Item -ItemType Directory -Path $dir -Force | Out-Null
} }
Add-Content -Path $LogFile -Value $line Add-Content -Path $LogFile -Value $line
} catch { } catch {}
# Logging failure is non-fatal
}
} }
function Show-Failure { function Show-Failure {
param([System.Management.Automation.ErrorRecord]$ErrorRecord) param([System.Management.Automation.ErrorRecord]$ErrorRecord)
$msg = $ErrorRecord.Exception.Message $msg = $ErrorRecord.Exception.Message
Write-Log ERROR $msg
Write-Log ERROR $msg
Write-Host "======================================================" -ForegroundColor Red Write-Host "======================================================" -ForegroundColor Red
Write-Host "ERROR: $msg" -ForegroundColor Red Write-Host "ERROR: $msg" -ForegroundColor Red
Write-Host "======================================================" -ForegroundColor Red Write-Host "======================================================" -ForegroundColor Red
@@ -54,35 +52,29 @@ function Show-Failure {
} }
# ---------------------------- # ----------------------------
# Safety constants # Constants
# ---------------------------- # ----------------------------
$RenewalWindow = 30 # Don't re-issue if cert has > 30 days left $RenewalWindow = 30
$DnsSleep = 15 # Seconds for DNS propagation for ACME DNS-01 $DnsSleep = 15
# ---------------------------- # ----------------------------
# Ensure PowerCLI + Posh-ACME # Load PowerCLI + Posh-ACME
# ---------------------------- # ----------------------------
try { try {
if (-not (Get-Module -ListAvailable -Name VMware.PowerCLI)) { if (-not (Get-Module -ListAvailable -Name VMware.PowerCLI)) {
Write-Log INFO "Installing VMware.PowerCLI..." Install-Module VMware.PowerCLI -Force -Scope AllUsers -AllowClobber
Install-Module -Name VMware.PowerCLI -Scope AllUsers -Force -AllowClobber
} }
Import-Module VMware.PowerCLI -ErrorAction Stop
# Disable all confirmations and cert warnings from PowerCLI Import-Module VMware.PowerCLI -ErrorAction Stop
Set-PowerCLIConfiguration -Scope AllUsers ` Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false `
-InvalidCertificateAction Ignore ` -ParticipateInCEIP:$false -DisplayDeprecationWarnings:$false | Out-Null
-Confirm:$false `
-ParticipateInCEIP:$false `
-DisplayDeprecationWarnings:$false | Out-Null
Write-Log INFO "VMware.PowerCLI loaded." Write-Log INFO "VMware.PowerCLI loaded."
} catch { Show-Failure $_ } } catch { Show-Failure $_ }
try { try {
if (-not (Get-Module -ListAvailable -Name Posh-ACME)) { if (-not (Get-Module -ListAvailable -Name Posh-ACME)) {
Write-Log INFO "Installing Posh-ACME..." Install-Module 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."
@@ -101,36 +93,32 @@ try {
} catch { Show-Failure $_ } } catch { Show-Failure $_ }
# ---------------------------- # ----------------------------
# Get VM list (sanity check) # Sanity check: list VMs
# ---------------------------- # ----------------------------
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 "Could not retrieve VM list: $($_.Exception.Message)" Write-Log WARN "Failed to enumerate VMs: $($_.Exception.Message)"
} }
# ---------------------------- # ----------------------------
# Locate existing Posh-ACME cert for VCENTERHOST # Detect existing Posh-ACME cert
# ---------------------------- # ----------------------------
$paCert = $null $paCert = $null
$needNewCert = $false $needNewCert = $false
try { try {
$allPaCerts = Get-PACertificate -List $allCerts = Get-PACertificate -List
if ($allPaCerts) { if ($allCerts) {
Write-Log INFO "Found $($allPaCerts.Count) Posh-ACME cert(s)." Write-Log INFO "Found $($allCerts.Count) Posh-ACME cert(s)."
foreach ($c in $allPaCerts) { $paCert = $allCerts |
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 | Sort-Object NotAfter -Descending |
Select-Object -First 1 Select-Object -First 1
} }
} catch { } catch {
Write-Log WARN "Failed to query Posh-ACME certs: $($_.Exception.Message)" Write-Log WARN "Failed to query Posh-ACME certificates: $($_.Exception.Message)"
} }
if ($paCert) { if ($paCert) {
@@ -138,21 +126,23 @@ if ($paCert) {
Write-Log INFO ("Existing cert expires {0} (~{1:N0} days)." -f $paCert.NotAfter, $daysLeft) Write-Log INFO ("Existing cert expires {0} (~{1:N0} days)." -f $paCert.NotAfter, $daysLeft)
if ($daysLeft -le $RenewalWindow) { if ($daysLeft -le $RenewalWindow) {
Write-Log INFO "Existing cert within $RenewalWindow days of expiry. Will request new ACME certificate." Write-Log INFO "Existing cert within $RenewalWindow days. ACME issuance required."
$needNewCert = $true $needNewCert = $true
} else { } else {
Write-Log INFO "Skipping ACME issuance: certificate valid > $RenewalWindow days." Write-Log INFO "Skipping issuance certificate valid >$RenewalWindow days."
} }
} else { } else {
Write-Log WARN "No existing Posh-ACME cert found for $VCENTERHOST. Will request new certificate." Write-Log WARN "No existing cert found — issuance required."
$needNewCert = $true $needNewCert = $true
} }
# ---------------------------- # ----------------------------
# ACME issuance (if needed) # ACME issuance (only if needed)
# ---------------------------- # ----------------------------
if ($needNewCert) { if ($needNewCert) {
try { try {
Write-Log INFO "Requesting new ACME certificate via Posh-ACME..."
$pluginArgs = @{ $pluginArgs = @{
PowerDNSApiHost = $WDNSHOST PowerDNSApiHost = $WDNSHOST
PowerDNSApiKey = (ConvertTo-SecureString $PDNSAPI -AsPlainText -Force) PowerDNSApiKey = (ConvertTo-SecureString $PDNSAPI -AsPlainText -Force)
@@ -161,9 +151,9 @@ if ($needNewCert) {
PowerDNSServerName = 'localhost' PowerDNSServerName = 'localhost'
} }
Write-Log INFO "Requesting new ACME certificate via Posh-ACME..." # Posh-ACME v4 syntax:
New-PACertificate ` New-PACertificate `
-MainDomain $VCENTERHOST ` -Domain $VCENTERHOST `
-Plugin PowerDNS ` -Plugin PowerDNS `
-PluginArgs $pluginArgs ` -PluginArgs $pluginArgs `
-Contact $ACMEEMAIL ` -Contact $ACMEEMAIL `
@@ -173,137 +163,134 @@ if ($needNewCert) {
-Force -Force
$paCert = Get-PACertificate $paCert = Get-PACertificate
Write-Log INFO ("New ACME certificate issued. NotAfter={0}" -f $paCert.NotAfter) Write-Log INFO ("New certificate issued: NotAfter={0}" -f $paCert.NotAfter)
} catch { } catch {
Write-Log ERROR ("ACME issuance failed: {0}" -f $_.Exception.Message) Write-Log ERROR ("ACME issuance failed: {0}" -f $_.Exception.Message)
if (-not $paCert) { if (-not $paCert) {
Write-Host "No fallback certificate exists; aborting." -ForegroundColor Red Write-Log ERROR "No fallback certificate exists aborting."
exit 1 exit 1
} }
Write-Log WARN "Falling back to existing certificate." Write-Log WARN "Using existing Posh-ACME certificate."
} }
} }
# Ensure we have a certificate at this point
if (-not $paCert) { if (-not $paCert) {
Write-Log ERROR "No usable Posh-ACME certificate available. Aborting." Write-Log ERROR "No usable certificate available — aborting."
exit 1 exit 1
} }
# ---------------------------- # ----------------------------
# Locate certificate files (LEAF ONLY for vCenter) # Certificate file resolution
# ---------------------------- # ----------------------------
$certFolder = Split-Path $paCert.CertFile -Parent $certFolder = Split-Path $paCert.CertFile -Parent
if (-not $certFolder) { $certPath = Join-Path $certFolder "cert.cer"
Write-Log ERROR "Unable to determine certificate folder from Posh-ACME. Aborting." $keyPath = Join-Path $certFolder "cert.key"
exit 1 $chainPath = Join-Path $certFolder "chain.cer"
}
# Posh-ACME outputs:
# cert.cer = leaf certificate
# cert.key = private key
# chain.cer = intermediate chain
# fullchain.cer = leaf + chain (NOT used by Set-VIMachineCertificate)
$certPath = Join-Path $certFolder "cert.cer" # leaf only
$keyPath = Join-Path $certFolder "cert.key" # private key
$chainPath = Join-Path $certFolder "chain.cer" # intermediate CA(s)
Write-Log INFO "Using cert folder: $certFolder" Write-Log INFO "Using cert folder: $certFolder"
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 cert file: $f"
exit 1 exit 1
} }
} }
# ---------------------------- # ----------------------------
# Add CA chain to vCenter trusted store # Add CA chain to trusted store (remove duplicates)
# ---------------------------- # ----------------------------
try { try {
Write-Log INFO "Adding/updating CA chain in vCenter trusted store..." Write-Log INFO "Cleaning old CA trust entries..."
$pemChain = Get-Content $chainPath -Raw $issuer = ($paCert.Issuer)
if ($pemChain.Trim().Length -gt 0) { $existingCA = Get-VITrustedCertificate | Where-Object { $_.Subject -eq $issuer }
Add-VITrustedCertificate -PemCertificateOrChain $pemChain -VCenterOnly -ErrorAction SilentlyContinue | Out-Null foreach ($ca in $existingCA) {
} else { Remove-VITrustedCertificate -Certificate $ca -Confirm:$false -ErrorAction SilentlyContinue
Write-Log WARN "CHAIN file appears empty; skipping trusted chain update."
} }
$pemChain = Get-Content $chainPath -Raw
Write-Log INFO "Adding CA chain to vCenter trust store..."
Add-VITrustedCertificate -PemCertificateOrChain $pemChain -VCenterOnly -Confirm:$false | Out-Null
} catch { } catch {
Write-Log WARN "Failed to add trusted chain (non-fatal): $($_.Exception.Message)" Write-Log WARN "Failed to manage CA trust entries: $($_.Exception.Message)"
} }
# ---------------------------- # ----------------------------
# Compare current vCenter Machine SSL cert # Compare current vCenter cert
# ---------------------------- # ----------------------------
$vcCert = $null
$needPush = $true $needPush = $true
try { try {
$vcCert = Get-VIMachineCertificate -VCenterOnly $vcCert = Get-VIMachineCertificate -VCenterOnly -ErrorAction Stop
} catch {
Write-Log WARN "Unable to read current vCenter certificate: $($_.Exception.Message). Assuming update required."
}
if ($vcCert) {
Write-Log INFO ("Current vCenter cert: Subject={0} NotAfter={1}" -f $vcCert.Subject, $vcCert.NotValidAfter) Write-Log INFO ("Current vCenter cert: Subject={0} NotAfter={1}" -f $vcCert.Subject, $vcCert.NotValidAfter)
if ($vcCert.Thumbprint -eq $paCert.Thumbprint) { if ($vcCert.Thumbprint -eq $paCert.Thumbprint) {
Write-Log INFO "vCenter already using the same certificate as Posh-ACME. No update necessary." Write-Log INFO "vCenter already using this certificate."
$needPush = $false $needPush = $false
} else {
Write-Log INFO "vCenter certificate differs from Posh-ACME cert. Update required."
} }
} catch {
Write-Log WARN "Unable to read vCenter cert, assuming update required."
} }
# ---------------------------- # ----------------------------
# Apply certificate via Set-VIMachineCertificate # Apply new certificate
# ---------------------------- # ----------------------------
if ($needPush) { if ($needPush) {
Write-Log INFO "Applying new Machine SSL certificate..."
$leafPem = Get-Content $certPath -Raw
$keyPem = Get-Content $keyPath -Raw
try { try {
$leafPem = Get-Content $certPath -Raw Set-VIMachineCertificate -PemCertificate $leafPem -PemKey $keyPem -Confirm:$false | Out-Null
$keyPem = Get-Content $keyPath -Raw
if (-not $leafPem.Trim()) { throw "Leaf certificate PEM appears empty." }
if (-not $keyPem.Trim()) { throw "Private key PEM appears empty." }
Write-Log INFO "Applying certificate via Set-VIMachineCertificate..."
Set-VIMachineCertificate -PemCertificate $leafPem -PemKey $keyPem | Out-Null
Write-Host "===========================================================" Write-Host "==========================================================="
Write-Host "SUCCESS: vCenter Machine SSL certificate updated." -ForegroundColor Green Write-Host "SUCCESS: vCenter Machine SSL certificate updated." -ForegroundColor Green
Write-Host "===========================================================" Write-Host "==========================================================="
Write-Log INFO "Certificate applied successfully."
Write-Log INFO "Certificate updated successfully."
# ---------------------------- # ----------------------------
# Restart vpxd service via appliance APIs # Restart vpxd service
# ---------------------------- # ----------------------------
try { try {
Write-Log INFO "Restarting vpxd service via Restart-VIApplianceService..." Write-Log INFO "Restarting vpxd via Restart-VIApplianceService..."
# vpxd service name is typically 'vpxd' $svc = Get-VIApplianceService -Name 'vpxd' -ErrorAction Stop
$vpxdSvc = Get-VIApplianceService -Name 'vpxd' -ErrorAction Stop $null = $svc | Restart-VIApplianceService -Confirm:$false
if ($vpxdSvc) { Write-Log INFO "vpxd restarted successfully."
$null = $vpxdSvc | Restart-VIApplianceService -Confirm:$false
Write-Log INFO "vpxd restart requested successfully."
} else {
Write-Log WARN "vpxd service not found via Get-VIApplianceService. Please check manually."
}
} catch { } catch {
Write-Log WARN "Automatic vpxd restart failed: $($_.Exception.Message). Please restart vCenter services manually if required." Write-Log WARN "vpxd restart failed: $($_.Exception.Message)"
}
# ----------------------------
# Trigger Veeam rescan
# ----------------------------
if ($VEEAMHOSTSSH) {
try {
Write-Log INFO "Triggering Veeam host rescan on $VEEAMHOSTSSH..."
$veeamCmd = "Rescan-VBREntity -AllHosts"
$sshCmd = "ssh -tq -o ConnectTimeout=3 -o ConnectionAttempts=1 $VEEAMHOSTSSH '$veeamCmd'"
$result = bash -c $sshCmd
Write-Log INFO "Veeam rescan result: $result"
} catch {
Write-Log WARN "Veeam rescan failed: $($_.Exception.Message)"
}
} }
} catch { } catch {
Show-Failure $_ Show-Failure $_
} }
} else { } else {
Write-Log INFO "Skipping vCenter certificate update; thumbprints already match." Write-Log INFO "No certificate update needed. Skipping vpxd restart + Veeam rescan."
} }
# ---------------------------- # ----------------------------
# Done # Script Complete
# ---------------------------- # ----------------------------
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