diff --git a/inc/vCenter-SSL.ps1 b/inc/vCenter-SSL.ps1 index 106a89c6..80fdd261 100644 --- a/inc/vCenter-SSL.ps1 +++ b/inc/vCenter-SSL.ps1 @@ -50,7 +50,6 @@ function Show-Banner { Add-Content -Path $LogFile -Value $log } - # ---------------------------- # Global variables for troubleshooting # ---------------------------- @@ -82,12 +81,15 @@ function Get-CertFingerprintFromPem { ) try { - $pem = $PemString -replace "\r","" -split "`n" | Where-Object { - ($_ -notlike "-----BEGIN*") -and ($_ -notlike "-----END*") -and ($_ -ne "") - } | Out-String - $pem = $pem.Trim() + # Strip the BEGIN/END lines and blank lines + $pemBody = $PemString -replace "`r","" -split "`n" | Where-Object { + ($_ -notlike "-----BEGIN*") -and + ($_ -notlike "-----END*") -and + ($_ -ne "") + } + $pemBody = ($pemBody -join "") # single base64 string - $bytes = [Convert]::FromBase64String($pem) + $bytes = [Convert]::FromBase64String($pemBody) $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes) $sha256 = [System.Security.Cryptography.SHA256]::Create() @@ -142,24 +144,26 @@ if (-not (Get-Module -ListAvailable -Name Posh-ACME)) { Import-Module Posh-ACME -ErrorAction Stop # ---------------------------- -# ACME / PowerDNS certificate request with 30-day + rate-limit handling +# ACME / Posh-ACME certificate logic (v4.30-compatible) # ---------------------------- $certSuccess = $false -$certPath = $null -$keyPath = $null -$chainPath = $null +$certPath = $null +$keyPath = $null +$chainPath = $null -# Get latest existing cert (Posh-ACME) -$existingPACert = Get-PACertificate -Domain $VCENTERHOST -ErrorAction SilentlyContinue | +# Get most recent Posh-ACME certificate for this domain +$existingPACert = Get-PACertificate | + Where-Object { $_.MainDomain -eq $VCENTERHOST } | Sort-Object NotAfter -Descending | Select-Object -First 1 $renewCert = $true $skipReason = "" +# Rule 1: Skip if cert valid >30 days if ($existingPACert) { $daysLeft = ($existingPACert.NotAfter - (Get-Date)).TotalDays - Write-Log -Level "INFO" -Message "Existing Posh-ACME cert expires $($existingPACert.NotAfter) (~$([math]::Round($daysLeft)) days left)." -Color "Gray" + Write-Log -Level "INFO" -Message "Existing cert expires $($existingPACert.NotAfter) (~$([math]::Round($daysLeft)) days left)." -Color "Gray" if ($daysLeft -gt 30) { $renewCert = $false @@ -167,15 +171,28 @@ if ($existingPACert) { } } +# Rule 2: LE rate-limit safety (don't request if last issuance < 168h) +if ($renewCert -and $existingPACert) { + $hoursSinceIssued = ((Get-Date) - $existingPACert.Created).TotalHours + if ($hoursSinceIssued -lt 168) { + $renewCert = $false + $skipReason = "LE rate-limit safety: last cert issued $([math]::Round($hoursSinceIssued)) hours ago (must wait 168h)." + } +} + if (-not $renewCert -and $existingPACert) { - Write-Log -Level "INFO" -Message "Skipping ACME request: $skipReason" -Color "Green" + Write-Log -Level "INFO" -Message "Skipping ACME issuance: $skipReason" -Color "Yellow" + $certPath = $existingPACert.CertificatePath $keyPath = $existingPACert.PrivateKeyPath $chainPath = $existingPACert.ChainPath $certSuccess = $true } else { - # Need or want to renew + # We either have no existing cert, it's near expiry, or outside LE safety window + + Write-Log -Level "INFO" -Message "Requesting new ACME certificate via Posh-ACME..." -Color "Cyan" + if ($PDNSAPI -is [string]) { $securePDNSAPI = ConvertTo-SecureString $PDNSAPI -AsPlainText -Force } else { @@ -191,11 +208,17 @@ else { } try { - Write-Log -Level "INFO" -Message "Requesting new certificate via Posh-ACME..." -Color "Cyan" - New-PACertificate -Domain $VCENTERHOST -DnsPlugin PowerDNS -PluginArgs $pArgs ` - -Contact $ACMEEMAIL -AcceptTOS -Verbose -Force -DnsSleep 15 + New-PACertificate -Domain $VCENTERHOST ` + -DnsPlugin PowerDNS ` + -PluginArgs $pArgs ` + -Contact $ACMEEMAIL ` + -AcceptTOS ` + -DnsSleep 15 ` + -Force ` + -Verbose - $newCert = Get-PACertificate -Domain $VCENTERHOST | + $newCert = Get-PACertificate | + Where-Object { $_.MainDomain -eq $VCENTERHOST } | Sort-Object NotAfter -Descending | Select-Object -First 1 @@ -206,24 +229,26 @@ else { $certSuccess = $true Show-Banner -Text "New ACME certificate successfully created." -Type "SUCCESS" } else { - Write-Log -Level "ERROR" -Message "New-PACertificate succeeded but Get-PACertificate returned nothing." -Color "Red" + Write-Log -Level "ERROR" -Message "ACME issuance succeeded but no certificate object found from Get-PACertificate." -Color "Red" + $certSuccess = $false } } catch { - $msg = $_.Exception.Message - Write-Log -Level "ERROR" -Message "ACME certificate request failed: $msg" -Color "Red" - $global:helpme = $msg + $errorMessage = $_.Exception.Message + Write-Log -Level "ERROR" -Message "ACME request failed: $errorMessage" -Color "Red" + $global:helpme = $errorMessage - # If we hit LE rate limit, fall back to existing cert (if any) - if ($msg -like "*too many certificates*") { + # If LE rate-limit hit, try to fall back to existing cert + if ($errorMessage -like "*too many certificates*") { Show-Banner -Text "Let’s Encrypt rate-limit reached. Using existing certificate if available." -Type "WARN" + if ($existingPACert) { $certPath = $existingPACert.CertificatePath $keyPath = $existingPACert.PrivateKeyPath $chainPath = $existingPACert.ChainPath $certSuccess = $true } else { - Write-Log -Level "ERROR" -Message "No existing certificate available to fall back to." -Color "Red" + Show-Banner -Text "No existing certificate available to fall back to!" -Type "ERROR" $certSuccess = $false } } else { @@ -232,9 +257,7 @@ else { } } -# ---------------------------- -# Verify certificate files exist -# ---------------------------- +# Verify cert files exist if ($certSuccess) { foreach ($f in @($certPath, $keyPath, $chainPath)) { if (-not (Test-Path $f)) { @@ -250,19 +273,19 @@ if (-not $certSuccess) { } # ---------------------------- -# Compare fingerprints with current vCenter cert +# vCenter REST: Compare fingerprints and update if needed # ---------------------------- -$updateNeeded = $true $sessionHeaders = @{ 'vmware-api-session-id' = $vCenterConn.ExtensionData.Content.SessionManager.SessionId } $vcenterCertUri = "https://$VCENTERHOST/rest/vcenter/certificate-management/vcenter/tls" +$updateNeeded = $true + try { Write-Log -Level "INFO" -Message "Querying current vCenter TLS certificate..." -Color "Cyan" $vcResp = Invoke-RestMethod -Uri $vcenterCertUri -Method Get -Headers $sessionHeaders -SkipCertificateCheck -ErrorAction Stop - # Try to find PEM in common shapes $currentPem = $null if ($vcResp.value -and $vcResp.value.cert) { $currentPem = $vcResp.value.cert @@ -276,14 +299,14 @@ try { $newFp = Get-CertFingerprintFromPem -PemString $newPem if ($currentFp -and $newFp -and ($currentFp -eq $newFp)) { - Write-Log -Level "INFO" -Message "vCenter already has the same certificate (SHA-256 fingerprint match)." -Color "Green" + Write-Log -Level "INFO" -Message "vCenter already has the same certificate (fingerprint match)." -Color "Green" $updateNeeded = $false } else { - Write-Log -Level "INFO" -Message "vCenter certificate fingerprint differs. Update is required." -Color "Yellow" + Write-Log -Level "INFO" -Message "vCenter certificate differs from Posh-ACME cert. Update is required." -Color "Yellow" $updateNeeded = $true } } else { - Write-Log -Level "WARN" -Message "Could not find existing vCenter certificate in REST response. Assuming update is required." -Color "Yellow" + Write-Log -Level "WARN" -Message "Could not parse existing vCenter certificate from REST response. Assuming update required." -Color "Yellow" $updateNeeded = $true } } @@ -293,7 +316,7 @@ catch { } # ---------------------------- -# Upload and apply certificate via REST (only if needed) +# Upload/apply cert if needed # ---------------------------- $restartNeeded = $false @@ -329,7 +352,7 @@ if ($updateNeeded) { } # ---------------------------- -# Automatic vpxd restart via REST (only if we changed the cert) +# vpxd restart via REST (only if cert changed) # ---------------------------- if ($restartNeeded) { $maxRetries = 20 @@ -357,7 +380,7 @@ if ($restartNeeded) { if (-not $restartSucceeded) { Show-Banner -Text "Automatic vpxd restart failed after $maxRetries attempts. Please restart manually via SSH." -Type "ERROR" - Write-Log -Level "ERROR" -Message "Manual restart command: ssh root@$VCENTERHOST 'service-control --stop vpxd; service-control --start vpxd'" -Color "Red" + Write-Log -Level "ERROR" -Message "Manual restart: ssh root@$VCENTERHOST 'service-control --stop vpxd; service-control --start vpxd'" -Color "Red" } } else { Write-Log -Level "INFO" -Message "Skipping vpxd restart because no certificate changes were applied." -Color "Green"