diff --git a/inc/vCenter-SSL.ps1 b/inc/vCenter-SSL.ps1 index 4a54ca99..647cd343 100644 --- a/inc/vCenter-SSL.ps1 +++ b/inc/vCenter-SSL.ps1 @@ -1,8 +1,11 @@ #!/usr/bin/env pwsh # ----------------------------------------------------------------------------------- -# vCenter SSL Automation (Posh-ACME 4.30, Linux/XDG compatible) -# Uses cert.cer / cert.key / fullchain.cer layout -# Rate-limit safe, fingerprint-based update, 30-day renewal window +# vCenter SSL Automation (vCenter 8u3 + Posh-ACME 4.30, Linux/XDG) +# Uses cert.cer / cert.key / fullchain.cer from ~/.config/Posh-ACME +# - Skips ACME if existing cert valid >30 days +# - Rate-limit safe +# - Fingerprint-based vCenter update +# - vpxd restart: try REST, fall back to SSH instructions # ----------------------------------------------------------------------------------- . /opt/idssys/nodemgmt/conf/powerwall/settings.ps1 @@ -73,14 +76,23 @@ function Find-LocalPACertFolder { $paRoot = Join-Path $HOME ".config/Posh-ACME" - if (-not (Test-Path $paRoot)) { return $null } + if (-not (Test-Path $paRoot)) { + Write-Log WARN "Posh-ACME config folder not found: $paRoot" Yellow + return $null + } + # Look for folder named exactly like the hostname (e.g. vcenter.scity.us) $folders = Get-ChildItem -Path $paRoot -Recurse -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq $HostName } - if (-not $folders) { return $null } + if (-not $folders) { + Write-Log WARN "No matching cert folders found under $paRoot for $HostName" Yellow + return $null + } - return ($folders | Select-Object -First 1).FullName + $folder = $folders | Select-Object -First 1 + Write-Log INFO "Using Posh-ACME cert folder: $($folder.FullName)" Gray + return $folder.FullName } # ---------------------------- @@ -110,7 +122,7 @@ if (-not (Get-Module -ListAvailable Posh-ACME)) { Import-Module Posh-ACME -ErrorAction Stop # ---------------------------- -# ACME logic +# ACME / Certificate selection logic # ---------------------------- $certSuccess = $false $certPath = $keyPath = $chainPath = $null @@ -123,7 +135,7 @@ if ($allPACerts) { } } -# Select cert for vcenter.scity.us +# Select cert object for VCENTERHOST $existingPACert = $allPACerts | Where-Object { $_.MainDomain -eq $VCENTERHOST -or ($_.AllSANs -contains $VCENTERHOST) } | Sort-Object NotAfter -Descending | @@ -132,7 +144,7 @@ $existingPACert = $allPACerts | $renewCert = $true $skipReason = "" -# If valid for >30 days, skip issuance +# 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 @@ -142,17 +154,17 @@ if ($existingPACert) { } } -# Rate-limit safety: don't issue if created <168 hours (LE window) +# Rate-limit safety guard based on Created time if ($renewCert -and $existingPACert) { $hoursSince = ((Get-Date) - $existingPACert.Created).TotalHours if ($hoursSince -lt 168) { $renewCert = $false - $skipReason = "Rate-limit: cert issued $([math]::Round($hoursSince)) hours ago." + $skipReason = "Rate-limit safety: cert issued $([math]::Round($hoursSince)) hours ago (<168h)." } } # ------------------------------------------------------- -# USE EXISTING CERT (no ACME issuance) +# Use existing cert files (no ACME issuance) # ------------------------------------------------------- if (-not $renewCert -and $existingPACert) { Write-Log INFO "Skipping ACME issuance: $skipReason" Yellow @@ -163,7 +175,10 @@ if (-not $renewCert -and $existingPACert) { exit 1 } - # Posh-ACME Linux filenames: + # Posh-ACME Linux file layout: + # cert.cer - leaf cert + # cert.key - private key + # fullchain.cer - cert + chain $certPath = Join-Path $certFolder "cert.cer" $keyPath = Join-Path $certFolder "cert.key" $chainPath = Join-Path $certFolder "fullchain.cer" @@ -179,7 +194,7 @@ if (-not $renewCert -and $existingPACert) { } # ------------------------------------------------------- -# NEW ACME CERTIFICATE +# Request new ACME certificate if needed # ------------------------------------------------------- if ($renewCert) { Write-Log INFO "Requesting new ACME certificate..." Cyan @@ -208,12 +223,14 @@ if ($renewCert) { -Verbose $certFolder = Find-LocalPACertFolder -HostName $VCENTERHOST - if (-not $certFolder) { Show-Banner "ACME OK but no files found!" ERROR; exit 1 } + if (-not $certFolder) { + Show-Banner "ACME succeeded but no cert folder found!" ERROR + exit 1 + } $certPath = Join-Path $certFolder "cert.cer" $keyPath = Join-Path $certFolder "cert.key" $chainPath = Join-Path $certFolder "fullchain.cer" - $certSuccess = $true Show-Banner "New ACME certificate created." SUCCESS } @@ -223,18 +240,29 @@ if ($renewCert) { $global:helpme = $msg if ($msg -like "*too many certificates*") { - Show-Banner "Rate limit hit! Using existing cert." WARN + Show-Banner "Let’s Encrypt rate limit hit. Using existing cert if possible." WARN if (-not $existingPACert) { - Show-Banner "No fallback cert exists! Aborting." ERROR + Show-Banner "No fallback cert exists. Aborting." ERROR exit 1 } + $certFolder = Find-LocalPACertFolder -HostName $VCENTERHOST + if (-not $certFolder) { + Show-Banner "Fallback cert folder missing. Aborting." ERROR + exit 1 + } + $certPath = Join-Path $certFolder "cert.cer" + $keyPath = Join-Path $certFolder "cert.key" + $chainPath = Join-Path $certFolder "fullchain.cer" + $certSuccess = $true + } + else { + exit 1 } - else { exit 1 } } } # ------------------------------------------------------- -# CERT SANITY CHECK +# Cert sanity check # ------------------------------------------------------- foreach ($f in @($certPath,$keyPath,$chainPath)) { if (-not (Test-Path $f)) { @@ -243,34 +271,57 @@ foreach ($f in @($certPath,$keyPath,$chainPath)) { } } +Write-Log INFO "Using certificate files:" Green +Write-Log INFO " CERT : $certPath" Green +Write-Log INFO " KEY : $keyPath" Green +Write-Log INFO " CHAIN: $chainPath" Green + # ------------------------------------------------------- -# COMPARE CURRENT VCENTER CERT +# vCenter 8u3 certificate API (/api/vcenter/certificate-management/vcenter/tls) # ------------------------------------------------------- $sessionHeaders = @{ 'vmware-api-session-id' = $vCenterConn.ExtensionData.Content.SessionManager.SessionId } -$vcenterCertUri = "https://$VCENTERHOST/rest/vcenter/certificate-management/vcenter/tls" +$vcCertBaseUri = "https://$VCENTERHOST/api/vcenter/certificate-management/vcenter/tls" +$vcCertGetUri = $vcCertBaseUri +$vcCertApplyUri = "$vcCertBaseUri?action=apply" + $updateNeeded = $true try { - $vcCert = Invoke-RestMethod -Uri $vcenterCertUri -Method Get -Headers $sessionHeaders -SkipCertificateCheck - $currentPem = $vcCert.value.cert - $currentFp = Get-CertFingerprintFromPem $currentPem - $newFp = Get-CertFingerprintFromPem (Get-Content -Raw $certPath) + $vcResp = Invoke-RestMethod -Uri $vcCertGetUri -Method Get -Headers $sessionHeaders -SkipCertificateCheck -ErrorAction Stop - if ($currentFp -and $newFp -and ($currentFp -eq $newFp)) { - Show-Banner "vCenter certificate already up-to-date." SUCCESS - $updateNeeded = $false + $currentPem = $null + if ($vcResp.value -and $vcResp.value.cert) { + $currentPem = $vcResp.value.cert + } elseif ($vcResp.cert) { + $currentPem = $vcResp.cert + } + + if ($currentPem) { + $currentFp = Get-CertFingerprintFromPem $currentPem + $newFp = Get-CertFingerprintFromPem (Get-Content -Raw $certPath) + + if ($currentFp -and $newFp -and ($currentFp -eq $newFp)) { + Show-Banner "vCenter certificate already matches Posh-ACME certificate." SUCCESS + $updateNeeded = $false + } else { + Write-Log INFO "vCenter certificate differs from Posh-ACME cert. Update required." Yellow + } + } else { + Write-Log WARN "vCenter cert API did not return a 'cert' field. Assuming update required." Yellow } } catch { - Write-Log WARN "Could not read vCenter certificate; assuming update needed." Yellow + Write-Log WARN "Could not read vCenter certificate via /api endpoint; assuming update required. $($_.Exception.Message)" Yellow } # ------------------------------------------------------- -# UPLOAD + APPLY CERTIFICATE +# Upload + apply certificate (vCenter 8u3 API) # ------------------------------------------------------- +$restartNeeded = $false + if ($updateNeeded) { try { $body = @{ @@ -279,48 +330,63 @@ if ($updateNeeded) { chain = Get-Content -Raw $chainPath } - Write-Log INFO "Uploading certificate..." Cyan - Invoke-RestMethod -Uri $vcenterCertUri -Method Post -Headers $sessionHeaders ` - -ContentType "application/json" -Body ($body | ConvertTo-Json -Compress) -SkipCertificateCheck + Write-Log INFO "Uploading certificate to vCenter via /api/vcenter/certificate-management..." Cyan + Invoke-RestMethod -Uri $vcCertBaseUri -Method Post -Headers $sessionHeaders ` + -ContentType "application/json" -Body ($body | ConvertTo-Json -Compress) -SkipCertificateCheck -ErrorAction Stop - Write-Log INFO "Applying certificate..." Cyan - Invoke-RestMethod -Uri "$vcenterCertUri?action=apply" ` - -Method Post -Headers $sessionHeaders -SkipCertificateCheck + Write-Log INFO "Applying uploaded certificate..." Cyan + Invoke-RestMethod -Uri $vcCertApplyUri -Method Post -Headers $sessionHeaders ` + -SkipCertificateCheck -ErrorAction Stop - Show-Banner "Certificate applied to vCenter." SUCCESS + Show-Banner "Certificate successfully uploaded and applied to vCenter." SUCCESS $restartNeeded = $true } catch { - Show-Banner "Failed to apply certificate: $($_.Exception.Message)" ERROR - exit 1 + Show-Banner "Failed to upload/apply certificate: $($_.Exception.Message)" ERROR + $restartNeeded = $false } } else { - Write-Log INFO "Certificate update not required." Green + Write-Log INFO "Skipping certificate upload/apply (already up to date)." Green } # ------------------------------------------------------- -# Restart vpxd (optional, REST) +# Restart vpxd (Option #3: try REST, fallback to SSH instructions) # ------------------------------------------------------- if ($restartNeeded) { $maxRetries = 20 + $healthUri = "https://$VCENTERHOST/rest/appliance/health/system" + $restartUri = "https://$VCENTERHOST/rest/appliance/system/services/vpxd?action=restart" + $restRestartSucceeded = $false - for ($i=1; $i -le $maxRetries; $i++) { + for ($i=1; $i -le $maxRetries -and -not $restRestartSucceeded; $i++) { try { - Invoke-RestMethod -Uri "https://$VCENTERHOST/rest/appliance/health/system" ` - -Method Get -SkipCertificateCheck -ErrorAction Stop + Invoke-RestMethod -Uri $healthUri -Method Get -SkipCertificateCheck -ErrorAction Stop - Invoke-RestMethod -Uri "https://$VCENTERHOST/rest/appliance/system/services/vpxd?action=restart" ` - -Method Post -SkipCertificateCheck -ErrorAction Stop - - Show-Banner "vpxd restarted successfully." SUCCESS - break + Write-Log INFO "Requesting vpxd restart via REST..." Cyan + Invoke-RestMethod -Uri $restartUri -Method Post -SkipCertificateCheck -ErrorAction Stop + Show-Banner "vpxd restart requested successfully via REST." SUCCESS + $restRestartSucceeded = $true } catch { - Write-Log WARN "vpxd not ready, retrying $i/$maxRetries..." Yellow + $msg = $_.Exception.Message + Write-Log WARN "vpxd REST restart attempt $i/$maxRetries failed: $msg" Yellow + + # If 404 or obvious unsupported, no point retrying REST + if ($msg -like "*404*" -or $msg -like "*Not Found*") { + Write-Log WARN "vpxd REST endpoint not available (404). Will fall back to SSH instructions." Yellow + break + } + Start-Sleep -Seconds 15 } } + + if (-not $restRestartSucceeded) { + Show-Banner "Automatic vpxd restart not confirmed via REST. Please restart manually via SSH." WARN + Write-Log WARN "Manual restart command:" Yellow + Write-Log WARN " ssh root@$VCENTERHOST 'service-control --stop vpxd; service-control --start vpxd'" Yellow + } } Show-Banner "Script complete. Log: $LogFile" INFO