From db2c181fc340f5d26e2e49b8bf2ba25c8df21e9f Mon Sep 17 00:00:00 2001 From: David Schroeder Date: Sat, 15 Nov 2025 23:01:45 -0600 Subject: [PATCH] Update vCenter-SSL.ps1 --- inc/vCenter-SSL.ps1 | 193 +++++++++++++++++++++++++------------------- 1 file changed, 112 insertions(+), 81 deletions(-) diff --git a/inc/vCenter-SSL.ps1 b/inc/vCenter-SSL.ps1 index 6866bbce..7f3ad765 100644 --- a/inc/vCenter-SSL.ps1 +++ b/inc/vCenter-SSL.ps1 @@ -1,8 +1,8 @@ #!/usr/bin/env pwsh # ----------------------------------------------------------------------------------- -# vCenter + Posh-ACME SSL Automation Script -# Fully idempotent, rate-limit safe, fingerprint matching, full logging -# Compatible with Posh-ACME 4.30.0 +# vCenter SSL Automation (with Posh-ACME 4.30 and XDG path support) +# Fully idempotent, rate-limit safe, uses existing cert if valid >30 days +# Auto-discovers cert files under ~/.config/Posh-ACME # ----------------------------------------------------------------------------------- . /opt/idssys/nodemgmt/conf/powerwall/settings.ps1 @@ -50,14 +50,13 @@ function Show-Failure { } # ---------------------------- -# CERT FINGERPRINT FUNCTION +# CERT FINGERPRINT # ---------------------------- function Get-CertFingerprintFromPem { param([string]$PemString) try { $pemBody = $PemString -replace "`r","" -split "`n" | Where-Object {$_ -notmatch "^-----" -and $_ -ne ""} - $bytes = [Convert]::FromBase64String(($pemBody -join "")) $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes) $sha256 = [System.Security.Cryptography.SHA256]::Create() @@ -65,11 +64,39 @@ function Get-CertFingerprintFromPem { return ($hash | ForEach-Object { $_.ToString("X2") }) -join "" } catch { - Write-Log -Level "WARN" -Message "Fingerprint calc error: $($_.Exception.Message)" -Color Yellow + Write-Log WARN "Fingerprint calculation error: $($_.Exception.Message)" Yellow return $null } } +# ---------------------------- +# Posh-ACME CERT FOLDER DISCOVERY (XDG FIX) +# ---------------------------- +function Find-LocalPACertFolder { + param([string]$HostName) + + # Correct Linux path for Posh-ACME 4.x + $paRoot = Join-Path $HOME ".config/Posh-ACME" + + if (-not (Test-Path $paRoot)) { + Write-Log WARN "Posh-ACME config folder not found: $paRoot" Yellow + return $null + } + + # Look for folders named exactly after the hostname + $folders = Get-ChildItem -Path $paRoot -Recurse -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -eq $HostName } + + if (-not $folders) { + Write-Log WARN "No hostname-matched folders found under $paRoot" Yellow + return $null + } + + $folder = $folders | Select-Object -First 1 + Write-Log INFO "Using Posh-ACME cert folder: $($folder.FullName)" Gray + return $folder.FullName +} + # ---------------------------- # LOAD POWERCLI # ---------------------------- @@ -83,9 +110,9 @@ Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out # CONNECT TO VCENTER # ---------------------------- try { - Write-Log -Level "INFO" -Message "Connecting to vCenter $VCENTERHOST..." -Color Cyan + Write-Log INFO "Connecting to vCenter $VCENTERHOST..." Cyan $vCenterConn = Connect-VIServer -Server $VCENTERHOST -User $VCENTERUSER -Password $VCENTERPASS -Force - Show-Banner -Text "Connected to vCenter." -Type SUCCESS + Show-Banner "Connected to vCenter." SUCCESS } catch { Show-Failure $_ } # ---------------------------- @@ -104,13 +131,13 @@ $certPath = $keyPath = $chainPath = $null $allPACerts = Get-PACertificate -ErrorAction SilentlyContinue if ($allPACerts) { - Write-Log INFO "Found $($allPACerts.Count) Posh-ACME certs." Gray + Write-Log INFO "Found $($allPACerts.Count) Posh-ACME cert(s)." Gray foreach ($c in $allPACerts) { - Write-Log INFO (" Cert: MainDomain={0} SANs={1} Exp={2}" -f $c.MainDomain, ($c.AllSANs -join ","), $c.NotAfter) Gray + Write-Log INFO " MainDomain=$($c.MainDomain) SANs=$($c.AllSANs -join ',') Exp=$($c.NotAfter)" Gray } } -# Match by MainDomain OR SANs +# Match certificate for vCenter $existingPACert = $allPACerts | Where-Object { $_.MainDomain -eq $VCENTERHOST -or ($_.AllSANs -contains $VCENTERHOST) } | Sort-Object NotAfter -Descending | @@ -119,34 +146,36 @@ $existingPACert = $allPACerts | $renewCert = $true $skipReason = "" -# RULE 1: Existing cert valid >30 days → skip +# RULE: If existing cert valid >30 days → skip ACME if ($existingPACert) { $daysLeft = ($existingPACert.NotAfter - (Get-Date)).TotalDays - Write-Log INFO "Existing cert expires $($existingPACert.NotAfter) (~$([math]::Round($daysLeft)) days)." Gray - + Write-Log INFO "Existing cert expires $($existingPACert.NotAfter) (~$([math]::Round($daysLeft)) days)" Gray if ($daysLeft -gt 30) { - $renewCert = $false - $skipReason = "Existing certificate still valid >30 days." + $renewCert = $false + $skipReason = "Existing certificate valid >30 days." } } -# RULE 2: Rate-limit safety (168 hours) +# RULE: Rate-limit safety if ($renewCert -and $existingPACert) { $hoursSince = ((Get-Date) - $existingPACert.Created).TotalHours if ($hoursSince -lt 168) { $renewCert = $false - $skipReason = "Last certificate issued $([math]::Round($hoursSince)) hours ago (<168h)." + $skipReason = "Rate-limit: cert issued $([math]::Round($hoursSince)) hours ago." } } -# If skipping issuance +# ------------------------------------------------------- +# USE EXISTING CERT FILES (XDG-FIXED) +# ------------------------------------------------------- if (-not $renewCert -and $existingPACert) { Write-Log INFO "Skipping ACME issuance: $skipReason" Yellow - # --- FIX: DERIVE CERT FOLDER FROM PFXPATH --- - Write-Log INFO "Applying fix: deriving certificate folder from PfxPath." Gray - $certFolder = Split-Path $existingPACert.PfxPath -Parent - Write-Log INFO "Cert folder resolved to: $certFolder" Gray + $certFolder = Find-LocalPACertFolder -HostName $VCENTERHOST + if (-not $certFolder) { + Show-Banner "Could not locate certificate files on disk. Aborting." ERROR + exit 1 + } $certPath = Join-Path $certFolder "cert.pem" $keyPath = Join-Path $certFolder "privkey.pem" @@ -155,7 +184,9 @@ if (-not $renewCert -and $existingPACert) { $certSuccess = $true } -# --- OTHERWISE REQUEST NEW CERT --- +# ------------------------------------------------------- +# REQUEST NEW ACME CERT IF NEEDED +# ------------------------------------------------------- if ($renewCert) { Write-Log INFO "Requesting new ACME certificate..." Cyan @@ -172,50 +203,54 @@ if ($renewCert) { } try { - New-PACertificate -Domain $VCENTERHOST -DnsPlugin PowerDNS -PluginArgs $pArgs ` - -Contact $ACMEEMAIL -AcceptTOS -DnsSleep 15 -Force -Verbose + New-PACertificate -Domain $VCENTERHOST ` + -DnsPlugin PowerDNS ` + -PluginArgs $pArgs ` + -Contact $ACMEEMAIL ` + -AcceptTOS ` + -DnsSleep 15 ` + -Force ` + -Verbose - $newCert = Get-PACertificate | - Where-Object { $_.MainDomain -eq $VCENTERHOST -or ($_.AllSANs -contains $VCENTERHOST) } | - Sort-Object NotAfter -Descending | - Select-Object -First 1 + # Find new cert folder via XDG + $certFolder = Find-LocalPACertFolder -HostName $VCENTERHOST + if (-not $certFolder) { Show-Banner "ACME succeeded but no files found." ERROR; exit 1 } - if ($newCert) { - $certFolder = Split-Path $newCert.PfxPath -Parent - $certPath = Join-Path $certFolder "cert.pem" - $keyPath = Join-Path $certFolder "privkey.pem" - $chainPath = Join-Path $certFolder "chain.pem" - $certSuccess = $true - Show-Banner "New ACME cert created." SUCCESS - } else { - Write-Log ERROR "ACME succeeded but no cert found." Red - exit 1 - } + $certPath = Join-Path $certFolder "cert.pem" + $keyPath = Join-Path $certFolder "privkey.pem" + $chainPath = Join-Path $certFolder "chain.pem" + $certSuccess = $true + Show-Banner "New ACME certificate created." SUCCESS } catch { $msg = $_.Exception.Message - Write-Log ERROR "ACME request failed: $msg" Red + Write-Log ERROR "ACME failed: $msg" Red $global:helpme = $msg if ($msg -like "*too many certificates*") { - Show-Banner "Rate-limit hit. Falling back to existing cert." WARN + Show-Banner "Rate-limit hit! Falling back to existing cert." WARN if ($existingPACert) { - $certFolder = Split-Path $existingPACert.PfxPath -Parent - $certPath = Join-Path $certFolder "cert.pem" - $keyPath = Join-Path $certFolder "privkey.pem" - $chainPath = Join-Path $certFolder "chain.pem" - $certSuccess = $true + $certFolder = Find-LocalPACertFolder -HostName $VCENTERHOST + if ($certFolder) { + $certPath = Join-Path $certFolder "cert.pem" + $keyPath = Join-Path $certFolder "privkey.pem" + $chainPath = Join-Path $certFolder "chain.pem" + $certSuccess = $true + } else { + Show-Banner "Cannot locate fallback cert files. Aborting." ERROR + exit 1 + } } - else { - Show-Banner "No fallback cert exists. Aborting." ERROR - exit 1 - } - } else { exit 1 } + else { Show-Banner "No fallback cert exists." ERROR; exit 1 } + } + else { exit 1 } } } -# Validate final cert files +# ------------------------------------------------------- +# VERIFY CERT FILES EXIST +# ------------------------------------------------------- foreach ($f in @($certPath,$keyPath,$chainPath)) { if (-not (Test-Path $f)) { Write-Log ERROR "Missing certificate file: $f" Red @@ -223,9 +258,9 @@ foreach ($f in @($certPath,$keyPath,$chainPath)) { } } -# ---------------------------- -# RETRIEVE VCENTER CURRENT CERT + COMPARE FINGERPRINTS -# ---------------------------- +# ------------------------------------------------------- +# CHECK VCENTER CURRENT CERT & COMPARE FINGERPRINTS +# ------------------------------------------------------- $sessionHeaders = @{ 'vmware-api-session-id' = $vCenterConn.ExtensionData.Content.SessionManager.SessionId } @@ -237,60 +272,56 @@ try { $vcResp = Invoke-RestMethod -Uri $vcenterCertUri -Method Get -Headers $sessionHeaders -SkipCertificateCheck $currentPem = $vcResp.value.cert - $currentFp = Get-CertFingerprintFromPem -PemString $currentPem - $newPem = Get-Content -Raw $certPath - $newFp = Get-CertFingerprintFromPem -PemString $newPem + $currentFp = Get-CertFingerprintFromPem $currentPem + $newFp = Get-CertFingerprintFromPem (Get-Content -Raw $certPath) if ($currentFp -and $newFp -and ($currentFp -eq $newFp)) { - Show-Banner "vCenter SSL certificate already up-to-date." SUCCESS + Show-Banner "vCenter certificate already up-to-date." SUCCESS $updateNeeded = $false } else { - Write-Log INFO "Certificate fingerprint mismatch. vCenter update required." Yellow + Write-Log INFO "Fingerprint mismatch → update required." Yellow } } catch { - Write-Log WARN "Unable to read vCenter cert; assuming update needed." Yellow + Write-Log WARN "Could not read vCenter certificate; assuming update needed." Yellow } -# ---------------------------- -# UPLOAD + APPLY CERT IF NEEDED -# ---------------------------- +# ------------------------------------------------------- +# UPLOAD / APPLY NEW CERT +# ------------------------------------------------------- $restartNeeded = $false if ($updateNeeded) { try { - Write-Log INFO "Uploading new TLS certificate..." Cyan - $body = @{ cert = Get-Content -Raw $certPath key = Get-Content -Raw $keyPath chain = Get-Content -Raw $chainPath } - Invoke-RestMethod -Uri $vcenterCertUri -Method Post ` - -Headers $sessionHeaders -ContentType "application/json" ` - -Body ($body | ConvertTo-Json -Compress) -SkipCertificateCheck + Write-Log INFO "Uploading new cert..." Cyan + Invoke-RestMethod -Uri $vcenterCertUri -Method Post -Headers $sessionHeaders ` + -ContentType "application/json" -Body ($body | ConvertTo-Json -Compress) -SkipCertificateCheck - Write-Log INFO "Applying new TLS certificate..." Cyan - - Invoke-RestMethod -Uri "$vcenterCertUri?action=apply" ` - -Method Post -Headers $sessionHeaders -SkipCertificateCheck + Write-Log INFO "Applying certificate..." Cyan + Invoke-RestMethod -Uri "$vcenterCertUri?action=apply" -Method Post ` + -Headers $sessionHeaders -SkipCertificateCheck Show-Banner "Certificate applied to vCenter." SUCCESS $restartNeeded = $true } catch { - Show-Banner "Failed to upload/apply certificate: $($_.Exception.Message)" ERROR + Show-Banner "Failed to apply certificate: $($_.Exception.Message)" ERROR exit 1 } } else { - Write-Log INFO "Skipping upload/apply (not needed)." Green + Write-Log INFO "Certificate update not required." Green } -# ---------------------------- -# RESTART VPXD VIA REST -# ---------------------------- +# ------------------------------------------------------- +# RESTART VPXD (IF NEEDED) +# ------------------------------------------------------- if ($restartNeeded) { $maxRetries = 20 for ($i=1; $i -le $maxRetries; $i++) { @@ -305,7 +336,7 @@ if ($restartNeeded) { break } catch { - Write-Log WARN "vpxd REST not ready; retry $i/$maxRetries..." Yellow + Write-Log WARN "vpxd not ready, retrying $i/$maxRetries..." Yellow Start-Sleep -Seconds 15 } }