#!/usr/bin/env pwsh # ----------------------------------------------------------------------------------- # vCenter Machine SSL Automation Script (vSphere 8.0 U3) # - Uses Let's Encrypt via Posh-ACME and PowerDNS # - Skips issuance if existing cert has >30 days # - Pushes cert into vCenter using Set-VIMachineCertificate # - Adds CA chain to trusted store # - Restarts vpxd (Restart-VIApplianceService) # - Performs Veeam Rescan-VBREntity when certificate changes # - Fully non-interactive (no prompts) # ----------------------------------------------------------------------------------- . /opt/idssys/nodemgmt/conf/powerwall/settings.ps1 $ErrorActionPreference = 'Stop' $ConfirmPreference = 'None' # Disable all PowerCLI confirmations # ---------------------------- # Logging # ---------------------------- $LogFile = "/opt/idssys/nodemgmt/logs/vc-ssl.log" function Write-Log { param( [ValidateSet('INFO','WARN','ERROR')] [string]$Level, [string]$Message, [string]$ForegroundColor ) $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $line = "[$ts] $Level : $Message" if ($ForegroundColor) { Write-Host $line -ForegroundColor $ForegroundColor } else { Write-Host $line } try { $dir = Split-Path $LogFile if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } Add-Content -Path $LogFile -Value $line } catch {} } function Show-Failure { param([System.Management.Automation.ErrorRecord]$ErrorRecord) $msg = $ErrorRecord.Exception.Message Write-Log ERROR $msg Red Write-Host "======================================================" -ForegroundColor Red Write-Host "ERROR: $msg" -ForegroundColor Red Write-Host "======================================================" -ForegroundColor Red exit 1 } # ---------------------------- # Constants # ---------------------------- $RenewalWindow = 30 $DnsSleep = 15 # ---------------------------- # Load PowerCLI + Posh-ACME # ---------------------------- Write-Log INFO "Loading Modules..." try { if (-not (Get-Module -ListAvailable -Name VCF.PowerCLI)) { Install-Module VCF.PowerCLI -Force -Scope AllUsers -AllowClobber } Import-Module VCF.PowerCLI -ErrorAction Stop *>$null Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false ` -ParticipateInCEIP:$false -DisplayDeprecationWarnings:$false | Out-Null Write-Log INFO "VCF.PowerCLI loaded." } catch { Show-Failure $_ } try { if (-not (Get-Module -ListAvailable -Name Posh-ACME)) { Install-Module Posh-ACME -Force -Scope AllUsers } Import-Module Posh-ACME -ErrorAction Stop *>$null 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 "========================================" -ForegroundColor Green Write-Host "SUCCESS: Connected to vCenter." -ForegroundColor Green Write-Host "========================================" -ForegroundColor Green } catch { Show-Failure $_ } # ---------------------------- # Sanity check: list VMs # ---------------------------- try { $vms = Get-VM Write-Log INFO "Retrieved $($vms.Count) VMs from vCenter." } catch { Write-Log WARN "Failed to enumerate VMs: $($_.Exception.Message)" Orange } # ---------------------------- # Detect existing Posh-ACME cert # ---------------------------- $paCert = $null $needNewCert = $false try { $allCerts = Get-PACertificate -List if ($allCerts) { Write-Log INFO "Found $($allCerts.Count) Posh-ACME cert(s)." $paCert = $allCerts | 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 certificates: $($_.Exception.Message)" Orange } 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. ACME issuance required." $needNewCert = $true } else { Write-Log INFO "Skipping issuance — certificate valid >$RenewalWindow days." $needNewCert = $false } } else { Write-Log WARN "No existing cert found — issuance required." Orange $needNewCert = $true } # ---------------------------- # ACME issuance (only if needed) # ---------------------------- if ($needNewCert) { try { Write-Log INFO "Requesting new ACME certificate via Posh-ACME..." $pluginArgs = @{ PowerDNSApiHost = $WDNSHOST PowerDNSApiKey = (ConvertTo-SecureString $PDNSAPI -AsPlainText -Force) PowerDNSUseTLS = $true PowerDNSPort = 443 PowerDNSServerName = 'localhost' } # Posh-ACME v4 syntax: New-PACertificate ` -Domain $VCENTERHOST ` -Plugin PowerDNS ` -PluginArgs $pluginArgs ` -Contact $ACMEEMAIL ` -AcceptTOS ` -DnsSleep $DnsSleep ` -Verbose ` -Force $paCert = Get-PACertificate Write-Log INFO ("New certificate issued: NotAfter={0}" -f $paCert.NotAfter) } catch { Write-Log ERROR ("ACME issuance failed: {0}" -f $_.Exception.Message) Red if (-not $paCert) { Write-Log ERROR "No fallback certificate exists — aborting." Red exit 1 } Write-Log WARN "Using existing Posh-ACME certificate." Orange } } if (-not $paCert) { Write-Log ERROR "No usable certificate available — aborting." Red exit 1 } if ($needNewCert) { # # ---------------------------- # # Certificate file resolution # # ---------------------------- $certFolder = Split-Path $paCert.CertFile -Parent $certPath = Join-Path $certFolder "cert.cer" $keyPath = Join-Path $certFolder "cert.key" $chainPath = Join-Path $certFolder "chain.cer" Write-Log INFO "Using cert folder: $certFolder" Write-Log INFO " CERT : $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 cert file: $f" Red exit 1 } } # ---------------------------- # Add CA chain to trusted store (remove duplicates) # ---------------------------- try { Write-Log INFO "Cleaning old CA trust entries..." $issuer = ($paCert.Issuer) $existingCA = Get-VITrustedCertificate | Where-Object { $_.Subject -eq $issuer } foreach ($ca in $existingCA) { Remove-VITrustedCertificate -Certificate $ca -Confirm:$false -ErrorAction SilentlyContinue } $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 { Write-Log WARN "Failed to manage CA trust entries: $($_.Exception.Message)" -FvoregroundColor Orange } # ---------------------------- # Compare current vCenter cert # ---------------------------- $needPush = $true try { $vcCert = Get-VIMachineCertificate -VCenterOnly -ErrorAction Stop 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 this certificate." $needPush = $false } } catch { Write-Log WARN "Unable to read vCenter cert, assuming update required." Orange } # ---------------------------- # Apply new certificate # ---------------------------- if ($needPush) { Write-Log INFO "Applying new Machine SSL certificate..." $leafPem = Get-Content $certPath -Raw $keyPem = Get-Content $keyPath -Raw try { Set-VIMachineCertificate -PemCertificate $leafPem -PemKey $keyPem -Confirm:$false | Out-Null Write-Host "===========================================================" -ForegroundColor Green Write-Host "SUCCESS: vCenter Machine SSL certificate updated." ForegroundColor Green Write-Host "===========================================================" -ForegroundColor Green Write-Log INFO "Certificate updated successfully." # ---------------------------- # Restart vpxd service # ---------------------------- try { Write-Log INFO "Restarting vpxd via Restart-VIApplianceService..." $svc = Get-VIApplianceService -Name 'vpxd' -ErrorAction Stop $null = $svc | Restart-VIApplianceService -Confirm:$false Write-Log INFO "vpxd restarted successfully." } catch { Write-Log WARN "vpxd restart failed: $($_.Exception.Message)" Orange } # ---------------------------- # 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)" Orange } } } catch { Show-Failure $_ } } else { Write-Log INFO "No certificate update needed. Skipping vpxd restart + Veeam rescan." } } # ---------------------------- # Script Complete # ---------------------------- Write-Host "==========================================================" -ForegroundColor Green Write-Host "INFO: Script complete. Log: $LogFile" -ForegroundColor Green Write-Host "==========================================================" -ForegroundColor Green