Files
NodeMgmt/inc/vCenter-SSL.ps1

393 lines
14 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env pwsh
# -----------------------------------------------------------------------------------
# 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
# ----------------------------
# Logging
# ----------------------------
$LogFile = "/opt/idssys/nodemgmt/logs/vc-ssl.log"
$logDir = Split-Path -Path $LogFile -Parent
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }
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
Write-Host $line -ForegroundColor $Color
Add-Content -Path $LogFile -Value $line
}
function Show-Banner {
param([string]$Text,[string]$Type="INFO")
switch ($Type.ToUpper()) {
"SUCCESS" { $C="Green"; $L="SUCCESS" }
"ERROR" { $C="Red"; $L="ERROR" }
"WARN" { $C="Yellow"; $L="WARN" }
default { $C="Cyan"; $L="INFO" }
}
$line = "=" * [math]::Max($Text.Length,40)
Write-Host $line -ForegroundColor $C
Write-Host "${L}: $Text" -ForegroundColor $C
Write-Host $line -ForegroundColor $C
Add-Content -Path $LogFile -Value ("[{0}] {1}: {2}" -f (Get-Date),$L,$Text)
}
# ----------------------------
# Error handling
# ----------------------------
$global:helpme = $null
function Show-Failure {
param([System.Management.Automation.ErrorRecord]$ErrorRecord)
$global:helpme = $ErrorRecord.Exception.Message
Show-Banner -Text $ErrorRecord.Exception.Message -Type "ERROR"
exit 1
}
# ----------------------------
# Cert fingerprint helper
# ----------------------------
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()
$hash = $sha256.ComputeHash($cert.RawData)
return ($hash | ForEach-Object { $_.ToString("X2") }) -join ""
}
catch { return $null }
}
# ----------------------------
# Discover Posh-ACME cert folder (Linux/XDG)
# ----------------------------
function Find-LocalPACertFolder {
param([string]$HostName)
$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 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) {
Write-Log WARN "No matching cert folders found under $paRoot for $HostName" Yellow
return $null
}
$folder = $folders | Select-Object -First 1
Write-Log INFO "Using Posh-ACME cert folder: $($folder.FullName)" Gray
return $folder.FullName
}
# ----------------------------
# Load VMware PowerCLI
# ----------------------------
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
# ----------------------------
try {
Write-Log INFO "Connecting to vCenter $VCENTERHOST..." Cyan
$vCenterConn = Connect-VIServer -Server $VCENTERHOST -User $VCENTERUSER -Password $VCENTERPASS -Force
Show-Banner "Connected to vCenter." SUCCESS
} catch { Show-Failure $_ }
# ----------------------------
# Load Posh-ACME
# ----------------------------
if (-not (Get-Module -ListAvailable Posh-ACME)) {
Install-Module Posh-ACME -Force -Scope AllUsers
}
Import-Module Posh-ACME -ErrorAction Stop
# ----------------------------
# ACME / Certificate selection logic
# ----------------------------
$certSuccess = $false
$certPath = $keyPath = $chainPath = $null
$allPACerts = Get-PACertificate -ErrorAction SilentlyContinue
if ($allPACerts) {
Write-Log INFO "Found $($allPACerts.Count) Posh-ACME cert(s)." Gray
foreach ($c in $allPACerts) {
Write-Log INFO " MainDomain=$($c.MainDomain) SANs=$($c.AllSANs -join ',') Exp=$($c.NotAfter)" Gray
}
}
# Select cert object for VCENTERHOST
$existingPACert = $allPACerts |
Where-Object { $_.MainDomain -eq $VCENTERHOST -or ($_.AllSANs -contains $VCENTERHOST) } |
Sort-Object NotAfter -Descending |
Select-Object -First 1
$renewCert = $true
$skipReason = ""
# 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
if ($daysLeft -gt 30) {
$renewCert = $false
$skipReason = "Existing certificate valid >30 days."
}
}
# 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 safety: cert issued $([math]::Round($hoursSince)) hours ago (<168h)."
}
}
# -------------------------------------------------------
# Use existing cert files (no ACME issuance)
# -------------------------------------------------------
if (-not $renewCert -and $existingPACert) {
Write-Log INFO "Skipping ACME issuance: $skipReason" Yellow
$certFolder = Find-LocalPACertFolder -HostName $VCENTERHOST
if (-not $certFolder) {
Show-Banner "Could not locate certificate files on disk. Aborting." ERROR
exit 1
}
# 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"
foreach ($f in @($certPath,$keyPath,$chainPath)) {
if (-not (Test-Path $f)) {
Write-Log ERROR "Missing certificate file: $f" Red
exit 1
}
}
$certSuccess = $true
}
# -------------------------------------------------------
# Request new ACME certificate if needed
# -------------------------------------------------------
if ($renewCert) {
Write-Log INFO "Requesting new ACME certificate..." Cyan
$securePDNSAPI = if ($PDNSAPI -is [string]) {
ConvertTo-SecureString $PDNSAPI -AsPlainText -Force
} else { $PDNSAPI }
$pArgs = @{
PowerDNSApiHost = $WDNSHOST
PowerDNSApiKey = $securePDNSAPI
PowerDNSUseTLS = $true
PowerDNSPort = 443
PowerDNSServerName = "localhost"
}
try {
New-PACertificate `
-Domain $VCENTERHOST `
-DnsPlugin PowerDNS `
-PluginArgs $pArgs `
-Contact $ACMEEMAIL `
-AcceptTOS `
-DnsSleep 15 `
-Force `
-Verbose
$certFolder = Find-LocalPACertFolder -HostName $VCENTERHOST
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
}
catch {
$msg = $_.Exception.Message
Write-Log ERROR "ACME failed: $msg" Red
$global:helpme = $msg
if ($msg -like "*too many certificates*") {
Show-Banner "Lets Encrypt rate limit hit. Using existing cert if possible." WARN
if (-not $existingPACert) {
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
}
}
}
# -------------------------------------------------------
# Cert sanity check
# -------------------------------------------------------
foreach ($f in @($certPath,$keyPath,$chainPath)) {
if (-not (Test-Path $f)) {
Write-Log ERROR "Missing certificate file: $f" Red
exit 1
}
}
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
# -------------------------------------------------------
# vCenter 8u3 certificate API (/api/vcenter/certificate-management/vcenter/tls)
# -------------------------------------------------------
$sessionHeaders = @{
'vmware-api-session-id' = $vCenterConn.ExtensionData.Content.SessionManager.SessionId
}
$vcCertBaseUri = "https://$VCENTERHOST/api/vcenter/certificate-management/vcenter/tls"
$vcCertGetUri = $vcCertBaseUri
$vcCertApplyUri = "$vcCertBaseUri?action=apply"
$updateNeeded = $true
try {
$vcResp = Invoke-RestMethod -Uri $vcCertGetUri -Method Get -Headers $sessionHeaders -SkipCertificateCheck -ErrorAction Stop
$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 via /api endpoint; assuming update required. $($_.Exception.Message)" Yellow
}
# -------------------------------------------------------
# Upload + apply certificate (vCenter 8u3 API)
# -------------------------------------------------------
$restartNeeded = $false
if ($updateNeeded) {
try {
$body = @{
cert = Get-Content -Raw $certPath
key = Get-Content -Raw $keyPath
chain = Get-Content -Raw $chainPath
}
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 uploaded certificate..." Cyan
Invoke-RestMethod -Uri $vcCertApplyUri -Method Post -Headers $sessionHeaders `
-SkipCertificateCheck -ErrorAction Stop
Show-Banner "Certificate successfully uploaded and applied to vCenter." SUCCESS
$restartNeeded = $true
}
catch {
Show-Banner "Failed to upload/apply certificate: $($_.Exception.Message)" ERROR
$restartNeeded = $false
}
}
else {
Write-Log INFO "Skipping certificate upload/apply (already up to date)." Green
}
# -------------------------------------------------------
# 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 -and -not $restRestartSucceeded; $i++) {
try {
Invoke-RestMethod -Uri $healthUri -Method Get -SkipCertificateCheck -ErrorAction Stop
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 {
$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