#!/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) # ----------------------------------------------------------------------------------- . /opt/idssys/nodemgmt/conf/powerwall/settings.ps1 $ErrorActionPreference = 'Stop' # ---------------------------- # Config / constants # ---------------------------- $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 { 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 ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } Add-Content -Path $LogFile -Value $line } catch { Write-Host "[WARN] Failed to write to log file $LogFile : $($_.Exception.Message)" -ForegroundColor Yellow } } # ---------------------------- # 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 exit 1 } # ---------------------------- # Ensure modules # ---------------------------- 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 } 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 "Posh-ACME not found. Installing..." Install-Module -Name Posh-ACME -Force -Scope AllUsers } 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..." $null = Connect-VIServer -Server $VCENTERHOST -User $VCENTERUSER -Password $VCENTERPASS -Force Write-Host "========================================" Write-Host "SUCCESS: Connected to vCenter." Write-Host "========================================" } catch { Show-Failure $_ } # ---------------------------- # Optional: list VMs (sanity check) # ---------------------------- 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)" } # ---------------------------- # Posh-ACME: find existing cert for VCENTERHOST # ---------------------------- $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) } $paCert = $allPaCerts | 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)" } $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." } } else { Write-Log WARN "No existing Posh-ACME certificate found for $VCENTERHOST. Will request new certificate." $needNewCert = $true } # ---------------------------- # ACME / Posh-ACME certificate issuance (if needed) # ---------------------------- if ($needNewCert) { try { # Build PowerDNS plugin args $pArgs = @{ PowerDNSApiHost = $WDNSHOST PowerDNSApiKey = (if ($PDNSAPI -is [string]) { ConvertTo-SecureString $PDNSAPI -AsPlainText -Force } else { $PDNSAPI }) PowerDNSUseTLS = $true PowerDNSPort = 443 PowerDNSServerName = 'localhost' } Write-Log INFO "Requesting new ACME certificate via Posh-ACME..." New-PACertificate ` -MainDomain $VCENTERHOST ` -Plugin PowerDNS ` -PluginArgs $pArgs ` -Contact $ACMEEMAIL ` -AcceptTOS ` -DnsSleep $DnsSleep ` -Force ` -Verbose # 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 if (-not $paCert) { Write-Host "==================================================" -ForegroundColor Red Write-Host "ERROR: No usable certificate available. Aborting." -ForegroundColor Red Write-Host "==================================================" -ForegroundColor Red exit 1 } else { Write-Log WARN "Falling back to existing Posh-ACME certificate despite issuance failure." } } } # 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 # ---------------------------- $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) 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" 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 # ---------------------------- 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." } } catch { Write-Log WARN "Add-VITrustedCertificate failed (non-fatal): $($_.Exception.Message)" } # ---------------------------- # Compare current vCenter machine cert vs Posh-ACME cert # ---------------------------- $currentVcCert = $null try { $currentVcCert = Get-VIMachineCertificate -VCenterOnly } catch { Write-Log WARN "Get-VIMachineCertificate failed: $($_.Exception.Message). Will assume update is required." } $needPushToVc = $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 } else { Write-Log INFO "vCenter certificate differs from Posh-ACME cert. Update required." } } else { Write-Log WARN "Could not read current vCenter machine cert. Assuming update is required." } # ---------------------------- # Push certificate into vCenter via Set-VIMachineCertificate # ---------------------------- if ($needPushToVc) { try { Write-Log INFO "Uploading and applying certificate via Set-VIMachineCertificate..." $pemCert = Get-Content $certPath -Raw # fullchain.cer (leaf + chain) $pemKey = Get-Content $keyPath -Raw # cert.key (private key) if (-not $pemCert.Trim()) { throw "PEM certificate content appears empty." } if (-not $pemKey.Trim()) { throw "PEM key content appears empty." } # 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." } 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 exit 1 } } else { Write-Log INFO "Skipping vCenter certificate update because thumbprints already match." } # ---------------------------- # Done # ---------------------------- Write-Host "==========================================================" -ForegroundColor Green Write-Host "INFO: Script complete. Log: $LogFile" -ForegroundColor Green Write-Host "==========================================================" -ForegroundColor Green