diff --git a/inc/vCenter-SSL.ps1 b/inc/vCenter-SSL.ps1 index 80fdd261..281986e9 100644 --- a/inc/vCenter-SSL.ps1 +++ b/inc/vCenter-SSL.ps1 @@ -1,13 +1,14 @@ #!/usr/bin/env pwsh # ----------------------------------------------------------------------------------- -# vCenter + Posh-ACME Script -# Fully automated, idempotent, with logging and fingerprint comparison +# vCenter + Posh-ACME SSL Automation Script +# Fully idempotent, rate-limit safe, fingerprint matching, full logging +# Compatible with: Posh-ACME 4.30.0 # ----------------------------------------------------------------------------------- . /opt/idssys/nodemgmt/conf/powerwall/settings.ps1 # ---------------------------- -# Logging setup +# LOGGING # ---------------------------- $LogFile = "/opt/idssys/nodemgmt/logs/vc-ssl.log" $logDir = Split-Path -Path $LogFile -Parent @@ -16,188 +17,148 @@ if (-not (Test-Path $logDir)) { } function Write-Log { - param( - [string]$Level, - [string]$Message, - [string]$Color = "White" - ) - $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss" - $line = "[{0}] {1}: {2}" -f $ts, $Level.ToUpper(), $Message + param([string]$Level,[string]$Message,[string]$Color="White") + $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $line = "[{0}] {1}: {2}" -f $ts,$Level.ToUpper(),$Message Write-Host $line -ForegroundColor $Color Add-Content -Path $LogFile -Value $line } function Show-Banner { - param( - [string]$Text, - [string]$Type = "INFO" - ) + param([string]$Text,[string]$Type="INFO") switch ($Type.ToUpper()) { - "SUCCESS" { $color = "Green"; $level = "SUCCESS" } - "ERROR" { $color = "Red"; $level = "ERROR" } - "WARN" { $color = "Yellow"; $level = "WARN" } - default { $color = "Cyan"; $level = "INFO" } + "SUCCESS" { $color="Green"; $level="SUCCESS" } + "ERROR" { $color="Red"; $level="ERROR" } + "WARN" { $color="Yellow"; $level="WARN" } + default { $color="Cyan"; $level="INFO" } } - - $line = ("=" * [Math]::Max($Text.Length, 40)) - + $line = "=" * [math]::Max($Text.Length,40) Write-Host $line -ForegroundColor $color Write-Host "${level}: $Text" -ForegroundColor $color Write-Host $line -ForegroundColor $color - - $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss" - $log = "[{0}] {1}: {2}" -f $ts, $level, $Text - Add-Content -Path $LogFile -Value $log + Add-Content -Path $LogFile -Value ("[{0}] {1}: {2}" -f (Get-Date),$level,$Text) } # ---------------------------- -# Global variables for troubleshooting +# ERROR HANDLING # ---------------------------- $global:helpme = $null -$global:responseBody = $null - -# ---------------------------- -# Error handler -# ---------------------------- function Show-Failure { param([System.Management.Automation.ErrorRecord]$ErrorRecord) - - $global:responseBody = $ErrorRecord.Exception.Message - $global:helpme = $global:responseBody - + $global:helpme = $ErrorRecord.Exception.Message Show-Banner -Text $ErrorRecord.Exception.Message -Type "ERROR" - Write-Log -Level "ERROR" -Message "Exception: $($ErrorRecord | Out-String)" -Color "Red" - + Write-Log -Level "ERROR" -Message $ErrorRecord | Out-Null exit 1 } # ---------------------------- -# Certificate fingerprint helper (SHA-256) +# SHA-256 CERT FINGERPRINT # ---------------------------- function Get-CertFingerprintFromPem { - param( - [Parameter(Mandatory = $true)] - [string]$PemString - ) + param([string]$PemString) try { - # 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 - + $pemBody = $PemString -replace "`r","" -split "`n" | + Where-Object {$_ -notmatch "^-----"} | + Where-Object {$_ -ne ""} + $pemBody = ($pemBody -join "") $bytes = [Convert]::FromBase64String($pemBody) $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes) - $sha256 = [System.Security.Cryptography.SHA256]::Create() $hash = $sha256.ComputeHash($cert.RawData) - ($hash | ForEach-Object { $_.ToString("X2") }) -join "" - } - catch { - Write-Log -Level "WARN" -Message "Failed to compute fingerprint: $($_.Exception.Message)" -Color "Yellow" + return ($hash | ForEach-Object { $_.ToString("X2") }) -join "" + } catch { + Write-Log -Level "WARN" -Message "Fingerprint calc error: $($_.Exception.Message)" -Color Yellow return $null } } # ---------------------------- -# Ensure PowerCLI module +# LOAD POWERCLI # ---------------------------- -if (-not (Get-Module -ListAvailable -Name VMware.PowerCLI)) { - Write-Log -Level "INFO" -Message "VMware.PowerCLI not found. Installing..." -Color "Yellow" - Install-Module -Name VMware.PowerCLI -Force -Scope AllUsers +if (-not (Get-Module -ListAvailable VMware.PowerCLI)) { + Install-Module VMware.PowerCLI -Force -Scope AllUsers } Import-Module VMware.PowerCLI -ErrorAction Stop Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null # ---------------------------- -# Connect to vCenter +# CONNECT TO VCENTER # ---------------------------- try { - Write-Log -Level "INFO" -Message "Connecting to vCenter at $VCENTERHOST ..." -Color "Cyan" + Write-Log -Level "INFO" -Message "Connecting to vCenter $VCENTERHOST..." -Color Cyan $vCenterConn = Connect-VIServer -Server $VCENTERHOST -User $VCENTERUSER -Password $VCENTERPASS -Force - Show-Banner -Text "Connected to vCenter API. Session established." -Type "SUCCESS" -} catch { - Show-Failure -ErrorRecord $_ -} + Show-Banner -Text "Connected to vCenter." -Type SUCCESS +} catch { Show-Failure $_ } # ---------------------------- -# Retrieve VM list (optional) +# LOAD POSH-ACME # ---------------------------- -try { - $vms = Get-VM - Write-Log -Level "INFO" -Message "Retrieved VM list ($($vms.Count) VMs)." -Color "Cyan" - $vms | ForEach-Object { Write-Log -Level "INFO" -Message "VM: $($_.Name)" -Color "Gray" } -} catch { - Write-Log -Level "WARN" -Message "Unable to retrieve VM list, continuing..." -Color "Yellow" -} - -# ---------------------------- -# Ensure Posh-ACME module -# ---------------------------- -if (-not (Get-Module -ListAvailable -Name Posh-ACME)) { - Write-Log -Level "INFO" -Message "Posh-ACME not found. Installing..." -Color "Yellow" - Install-Module -Name Posh-ACME -Force -Scope AllUsers +if (-not (Get-Module -ListAvailable Posh-ACME)) { + Install-Module Posh-ACME -Force -Scope AllUsers } Import-Module Posh-ACME -ErrorAction Stop # ---------------------------- -# ACME / Posh-ACME certificate logic (v4.30-compatible) +# ACME CERTIFICATE LOGIC (Posh-ACME 4.30) # ---------------------------- $certSuccess = $false -$certPath = $null -$keyPath = $null -$chainPath = $null +$certPath = $keyPath = $chainPath = $null -# Get most recent Posh-ACME certificate for this domain -$existingPACert = Get-PACertificate | - Where-Object { $_.MainDomain -eq $VCENTERHOST } | +# Log all certs first +$allPACerts = Get-PACertificate -ErrorAction SilentlyContinue +if ($allPACerts) { + Write-Log INFO "Found $($allPACerts.Count) Posh-ACME certs." Gray + foreach ($c in $allPACerts) { + Write-Log INFO (" Cert: MainDomain={0} SANs={1} Exp={2}" -f $c.MainDomain, ($c.AllSANs -join ","), $c.NotAfter) Gray + } +} else { + Write-Log INFO "No Posh-ACME certs found." Yellow +} + +# Find cert matching VCENTERHOST +$existingPACert = $allPACerts | + Where-Object {$_.MainDomain -eq $VCENTERHOST -or ($_.AllSANs -contains $VCENTERHOST)} | Sort-Object NotAfter -Descending | Select-Object -First 1 -$renewCert = $true +$renewCert = $true $skipReason = "" -# Rule 1: Skip if cert valid >30 days +# RULE 1: Existing cert valid >30 days → skip if ($existingPACert) { $daysLeft = ($existingPACert.NotAfter - (Get-Date)).TotalDays - Write-Log -Level "INFO" -Message "Existing cert expires $($existingPACert.NotAfter) (~$([math]::Round($daysLeft)) days left)." -Color "Gray" - + Write-Log INFO "Existing cert expires $($existingPACert.NotAfter) (~$([math]::Round($daysLeft)) days)." Gray if ($daysLeft -gt 30) { - $renewCert = $false - $skipReason = "Existing certificate is valid for more than 30 days." + $renewCert = $false + $skipReason = "Existing certificate still valid >30 days." } } -# Rule 2: LE rate-limit safety (don't request if last issuance < 168h) +# RULE 2: LE rate-limit safety (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)." + $renewCert = $false + $skipReason = "LE rate-limit safety: last cert was $([math]::Round($hoursSinceIssued)) hours ago." } } +# If skipping ACME issuance if (-not $renewCert -and $existingPACert) { - Write-Log -Level "INFO" -Message "Skipping ACME issuance: $skipReason" -Color "Yellow" - + Write-Log INFO "Skipping new ACME issuance: $skipReason" Yellow $certPath = $existingPACert.CertificatePath $keyPath = $existingPACert.PrivateKeyPath $chainPath = $existingPACert.ChainPath $certSuccess = $true } else { - # We either have no existing cert, it's near expiry, or outside LE safety window + # NEED NEW CERTIFICATE + Write-Log INFO "Requesting new ACME certificate..." Cyan - 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 { - $securePDNSAPI = $PDNSAPI - } + $securePDNSAPI = if ($PDNSAPI -is [string]) { + ConvertTo-SecureString $PDNSAPI -AsPlainText -Force + } else { $PDNSAPI } $pArgs = @{ PowerDNSApiHost = $WDNSHOST @@ -208,17 +169,11 @@ else { } 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 } | + Where-Object {$_.MainDomain -eq $VCENTERHOST -or ($_.AllSANs -contains $VCENTERHOST)} | Sort-Object NotAfter -Descending | Select-Object -First 1 @@ -227,166 +182,128 @@ else { $keyPath = $newCert.PrivateKeyPath $chainPath = $newCert.ChainPath $certSuccess = $true - Show-Banner -Text "New ACME certificate successfully created." -Type "SUCCESS" + Show-Banner "New ACME certificate created." SUCCESS } else { - Write-Log -Level "ERROR" -Message "ACME issuance succeeded but no certificate object found from Get-PACertificate." -Color "Red" + Write-Log ERROR "ACME succeeded but no certificate found." Red $certSuccess = $false } } catch { - $errorMessage = $_.Exception.Message - Write-Log -Level "ERROR" -Message "ACME request failed: $errorMessage" -Color "Red" - $global:helpme = $errorMessage + $msg = $_.Exception.Message + Write-Log ERROR "ACME request failed: $msg" Red + $global:helpme = $msg - # 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 ($msg -like "*too many certificates*") { + Show-Banner "Rate-limit hit. Trying fallback..." WARN if ($existingPACert) { $certPath = $existingPACert.CertificatePath $keyPath = $existingPACert.PrivateKeyPath $chainPath = $existingPACert.ChainPath $certSuccess = $true - } else { - Show-Banner -Text "No existing certificate available to fall back to!" -Type "ERROR" - $certSuccess = $false } - } else { - $certSuccess = $false + else { + Show-Banner "No fallback cert exists. Aborting." ERROR + exit 1 + } } + else { exit 1 } } } -# Verify cert files exist -if ($certSuccess) { - foreach ($f in @($certPath, $keyPath, $chainPath)) { - if (-not (Test-Path $f)) { - Write-Log -Level "ERROR" -Message "Certificate file missing: $f" -Color "Red" - $certSuccess = $false - } +# Validate final cert files +foreach ($f in @($certPath,$keyPath,$chainPath)) { + if (-not (Test-Path $f)) { + Write-Log ERROR "Missing certificate file: $f" Red + exit 1 } } -if (-not $certSuccess) { - Show-Banner -Text "No usable certificate available. Aborting before vCenter update." -Type "ERROR" - exit 1 -} - # ---------------------------- -# vCenter REST: Compare fingerprints and update if needed +# CHECK VCENTER CERT & FINGERPRINT MATCH # ---------------------------- $sessionHeaders = @{ '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 { - 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 + $currentPem = $vcResp.value.cert - $currentPem = $null - if ($vcResp.value -and $vcResp.value.cert) { - $currentPem = $vcResp.value.cert - } elseif ($vcResp.certificate) { - $currentPem = $vcResp.certificate - } + $currentFp = Get-CertFingerprintFromPem -PemString $currentPem + $newPem = Get-Content -Raw $certPath + $newFp = Get-CertFingerprintFromPem -PemString $newPem - if ($currentPem) { - $currentFp = Get-CertFingerprintFromPem -PemString $currentPem - $newPem = Get-Content -Path $certPath -Raw - $newFp = Get-CertFingerprintFromPem -PemString $newPem - - if ($currentFp -and $newFp -and ($currentFp -eq $newFp)) { - 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 differs from Posh-ACME cert. Update is required." -Color "Yellow" - $updateNeeded = $true - } + if ($currentFp -and $newFp -and ($currentFp -eq $newFp)) { + Show-Banner "vCenter certificate already up-to-date." SUCCESS + $updateNeeded = $false } else { - Write-Log -Level "WARN" -Message "Could not parse existing vCenter certificate from REST response. Assuming update required." -Color "Yellow" - $updateNeeded = $true + Write-Log INFO "Certificate fingerprints differ. Update required." Yellow } } catch { - Write-Log -Level "WARN" -Message "Failed to query current vCenter certificate: $($_.Exception.Message). Proceeding with update." -Color "Yellow" + Write-Log WARN "Cannot read vCenter cert, assuming update needed." Yellow $updateNeeded = $true } # ---------------------------- -# Upload/apply cert if needed +# UPLOAD + APPLY NEW CERT # ---------------------------- $restartNeeded = $false if ($updateNeeded) { try { $body = @{ - cert = Get-Content -Path $certPath -Raw - key = Get-Content -Path $keyPath -Raw - chain = Get-Content -Path $chainPath -Raw + cert = Get-Content -Raw $certPath + key = Get-Content -Raw $keyPath + chain = Get-Content -Raw $chainPath } - Write-Log -Level "INFO" -Message "Uploading TLS certificate to vCenter..." -Color "Cyan" - Invoke-RestMethod -Uri $vcenterCertUri -Method Post -Body ($body | ConvertTo-Json -Compress) ` - -ContentType 'application/json' -Headers $sessionHeaders -SkipCertificateCheck - Show-Banner -Text "Certificate uploaded to vCenter." -Type "SUCCESS" + Write-Log INFO "Uploading new TLS cert to vCenter..." Cyan + Invoke-RestMethod -Uri $vcenterCertUri -Method Post ` + -Headers $sessionHeaders -ContentType "application/json" ` + -Body ($body | ConvertTo-Json -Compress) -SkipCertificateCheck - $uriApply = "https://$VCENTERHOST/rest/vcenter/certificate-management/vcenter/tls?action=apply" - Write-Log -Level "INFO" -Message "Applying TLS certificate to vCenter..." -Color "Cyan" - Invoke-RestMethod -Uri $uriApply -Method Post -Headers $sessionHeaders -SkipCertificateCheck - Show-Banner -Text "Certificate applied to vCenter." -Type "SUCCESS" + Write-Log INFO "Applying new TLS cert..." Cyan + Invoke-RestMethod -Uri "$vcenterCertUri?action=apply" ` + -Method Post -Headers $sessionHeaders -SkipCertificateCheck + Show-Banner "Certificate applied to vCenter." SUCCESS $restartNeeded = $true } catch { - Write-Log -Level "ERROR" -Message "Certificate upload/apply failed: $($_.Exception.Message)" -Color "Red" - $global:helpme = $_.Exception.Message - Show-Banner -Text "Failed to upload/apply certificate to vCenter." -Type "ERROR" - $restartNeeded = $false + Show-Banner "Failed to upload/apply certificate: $($_.Exception.Message)" ERROR + exit 1 } -} else { - Show-Banner -Text "vCenter certificate is already up to date. No upload/apply needed." -Type "SUCCESS" - $restartNeeded = $false +} +else { + Write-Log INFO "Skipping cert upload/apply." Green } # ---------------------------- -# vpxd restart via REST (only if cert changed) +# RESTART VPXD IF NEEDED # ---------------------------- if ($restartNeeded) { $maxRetries = 20 - $retryCount = 0 - $restartSucceeded = $false - - while ($retryCount -lt $maxRetries -and -not $restartSucceeded) { + for ($i=1; $i -le $maxRetries; $i++) { try { - $healthUri = "https://$VCENTERHOST/rest/appliance/health/system" - Write-Log -Level "INFO" -Message "Checking vCenter REST health endpoint..." -Color "Cyan" - Invoke-RestMethod -Uri $healthUri -Method Get -SkipCertificateCheck -ErrorAction Stop + Invoke-RestMethod -Uri "https://$VCENTERHOST/rest/appliance/health/system" ` + -Method Get -SkipCertificateCheck -ErrorAction Stop - $restartUri = "https://$VCENTERHOST/rest/appliance/system/services/vpxd?action=restart" - Write-Log -Level "INFO" -Message "Requesting vpxd service restart via REST..." -Color "Cyan" - Invoke-RestMethod -Uri $restartUri -Method Post -SkipCertificateCheck -ErrorAction Stop - Show-Banner -Text "vpxd service restart requested successfully." -Type "SUCCESS" - $restartSucceeded = $true + Invoke-RestMethod -Uri "https://$VCENTERHOST/rest/appliance/system/services/vpxd?action=restart" ` + -Method Post -SkipCertificateCheck -ErrorAction Stop + + Show-Banner "vpxd restarted successfully." SUCCESS + break } catch { - Write-Log -Level "WARN" -Message "vpxd REST endpoint not ready, retrying in 15 seconds... (Attempt $($retryCount+1)/$maxRetries)" -Color "Yellow" + Write-Log WARN "vpxd REST not ready, retry $i/$maxRetries..." Yellow Start-Sleep -Seconds 15 - $retryCount++ } } - - 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: 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" } -# ---------------------------- -# Completion message -# ---------------------------- -Show-Banner -Text "Script completed. Check $LogFile and `$global:helpme for details if needed." -Type "INFO" +Show-Banner "Script complete. Log: $LogFile" INFO