Update vCenter-SSL.ps1

This commit is contained in:
2025-11-15 20:44:30 -06:00
parent 3eec3a66ec
commit d5a42efd89

View File

@@ -50,7 +50,6 @@ function Show-Banner {
Add-Content -Path $LogFile -Value $log Add-Content -Path $LogFile -Value $log
} }
# ---------------------------- # ----------------------------
# Global variables for troubleshooting # Global variables for troubleshooting
# ---------------------------- # ----------------------------
@@ -82,12 +81,15 @@ function Get-CertFingerprintFromPem {
) )
try { try {
$pem = $PemString -replace "\r","" -split "`n" | Where-Object { # Strip the BEGIN/END lines and blank lines
($_ -notlike "-----BEGIN*") -and ($_ -notlike "-----END*") -and ($_ -ne "") $pemBody = $PemString -replace "`r","" -split "`n" | Where-Object {
} | Out-String ($_ -notlike "-----BEGIN*") -and
$pem = $pem.Trim() ($_ -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) $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes)
$sha256 = [System.Security.Cryptography.SHA256]::Create() $sha256 = [System.Security.Cryptography.SHA256]::Create()
@@ -142,24 +144,26 @@ if (-not (Get-Module -ListAvailable -Name Posh-ACME)) {
Import-Module Posh-ACME -ErrorAction Stop 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 $certSuccess = $false
$certPath = $null $certPath = $null
$keyPath = $null $keyPath = $null
$chainPath = $null $chainPath = $null
# Get latest existing cert (Posh-ACME) # Get most recent Posh-ACME certificate for this domain
$existingPACert = Get-PACertificate -Domain $VCENTERHOST -ErrorAction SilentlyContinue | $existingPACert = Get-PACertificate |
Where-Object { $_.MainDomain -eq $VCENTERHOST } |
Sort-Object NotAfter -Descending | Sort-Object NotAfter -Descending |
Select-Object -First 1 Select-Object -First 1
$renewCert = $true $renewCert = $true
$skipReason = "" $skipReason = ""
# Rule 1: Skip if cert valid >30 days
if ($existingPACert) { if ($existingPACert) {
$daysLeft = ($existingPACert.NotAfter - (Get-Date)).TotalDays $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) { if ($daysLeft -gt 30) {
$renewCert = $false $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) { 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 $certPath = $existingPACert.CertificatePath
$keyPath = $existingPACert.PrivateKeyPath $keyPath = $existingPACert.PrivateKeyPath
$chainPath = $existingPACert.ChainPath $chainPath = $existingPACert.ChainPath
$certSuccess = $true $certSuccess = $true
} }
else { 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]) { if ($PDNSAPI -is [string]) {
$securePDNSAPI = ConvertTo-SecureString $PDNSAPI -AsPlainText -Force $securePDNSAPI = ConvertTo-SecureString $PDNSAPI -AsPlainText -Force
} else { } else {
@@ -191,11 +208,17 @@ else {
} }
try { try {
Write-Log -Level "INFO" -Message "Requesting new certificate via Posh-ACME..." -Color "Cyan" New-PACertificate -Domain $VCENTERHOST `
New-PACertificate -Domain $VCENTERHOST -DnsPlugin PowerDNS -PluginArgs $pArgs ` -DnsPlugin PowerDNS `
-Contact $ACMEEMAIL -AcceptTOS -Verbose -Force -DnsSleep 15 -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 | Sort-Object NotAfter -Descending |
Select-Object -First 1 Select-Object -First 1
@@ -206,24 +229,26 @@ else {
$certSuccess = $true $certSuccess = $true
Show-Banner -Text "New ACME certificate successfully created." -Type "SUCCESS" Show-Banner -Text "New ACME certificate successfully created." -Type "SUCCESS"
} else { } 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 { catch {
$msg = $_.Exception.Message $errorMessage = $_.Exception.Message
Write-Log -Level "ERROR" -Message "ACME certificate request failed: $msg" -Color "Red" Write-Log -Level "ERROR" -Message "ACME request failed: $errorMessage" -Color "Red"
$global:helpme = $msg $global:helpme = $errorMessage
# If we hit LE rate limit, fall back to existing cert (if any) # If LE rate-limit hit, try to fall back to existing cert
if ($msg -like "*too many certificates*") { if ($errorMessage -like "*too many certificates*") {
Show-Banner -Text "Lets Encrypt rate-limit reached. Using existing certificate if available." -Type "WARN" Show-Banner -Text "Lets Encrypt rate-limit reached. Using existing certificate if available." -Type "WARN"
if ($existingPACert) { if ($existingPACert) {
$certPath = $existingPACert.CertificatePath $certPath = $existingPACert.CertificatePath
$keyPath = $existingPACert.PrivateKeyPath $keyPath = $existingPACert.PrivateKeyPath
$chainPath = $existingPACert.ChainPath $chainPath = $existingPACert.ChainPath
$certSuccess = $true $certSuccess = $true
} else { } 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 $certSuccess = $false
} }
} else { } else {
@@ -232,9 +257,7 @@ else {
} }
} }
# ---------------------------- # Verify cert files exist
# Verify certificate files exist
# ----------------------------
if ($certSuccess) { if ($certSuccess) {
foreach ($f in @($certPath, $keyPath, $chainPath)) { foreach ($f in @($certPath, $keyPath, $chainPath)) {
if (-not (Test-Path $f)) { 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 = @{ $sessionHeaders = @{
'vmware-api-session-id' = $vCenterConn.ExtensionData.Content.SessionManager.SessionId 'vmware-api-session-id' = $vCenterConn.ExtensionData.Content.SessionManager.SessionId
} }
$vcenterCertUri = "https://$VCENTERHOST/rest/vcenter/certificate-management/vcenter/tls" $vcenterCertUri = "https://$VCENTERHOST/rest/vcenter/certificate-management/vcenter/tls"
$updateNeeded = $true
try { try {
Write-Log -Level "INFO" -Message "Querying current vCenter TLS certificate..." -Color "Cyan" Write-Log -Level "INFO" -Message "Querying current vCenter TLS certificate..." -Color "Cyan"
$vcResp = Invoke-RestMethod -Uri $vcenterCertUri -Method Get -Headers $sessionHeaders -SkipCertificateCheck -ErrorAction Stop $vcResp = Invoke-RestMethod -Uri $vcenterCertUri -Method Get -Headers $sessionHeaders -SkipCertificateCheck -ErrorAction Stop
# Try to find PEM in common shapes
$currentPem = $null $currentPem = $null
if ($vcResp.value -and $vcResp.value.cert) { if ($vcResp.value -and $vcResp.value.cert) {
$currentPem = $vcResp.value.cert $currentPem = $vcResp.value.cert
@@ -276,14 +299,14 @@ try {
$newFp = Get-CertFingerprintFromPem -PemString $newPem $newFp = Get-CertFingerprintFromPem -PemString $newPem
if ($currentFp -and $newFp -and ($currentFp -eq $newFp)) { 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 $updateNeeded = $false
} else { } 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 $updateNeeded = $true
} }
} else { } 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 $updateNeeded = $true
} }
} }
@@ -293,7 +316,7 @@ catch {
} }
# ---------------------------- # ----------------------------
# Upload and apply certificate via REST (only if needed) # Upload/apply cert if needed
# ---------------------------- # ----------------------------
$restartNeeded = $false $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) { if ($restartNeeded) {
$maxRetries = 20 $maxRetries = 20
@@ -357,7 +380,7 @@ if ($restartNeeded) {
if (-not $restartSucceeded) { if (-not $restartSucceeded) {
Show-Banner -Text "Automatic vpxd restart failed after $maxRetries attempts. Please restart manually via SSH." -Type "ERROR" 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 { } else {
Write-Log -Level "INFO" -Message "Skipping vpxd restart because no certificate changes were applied." -Color "Green" Write-Log -Level "INFO" -Message "Skipping vpxd restart because no certificate changes were applied." -Color "Green"