Files
NodeMgmt/inc/vCenter-SSL.ps1

251 lines
8.7 KiB
PowerShell

#!/usr/bin/env pwsh
# -----------------------------------------------------------------------------------
# vCenter Machine SSL automation for vSphere 8.0 U3
# - Uses Let's Encrypt via Posh-ACME (PowerDNS)
# - Skips issuance if cert >30 days
# - Pushes Posh-ACME cert into vCenter using Set-VIMachineCertificate
# - Works WITHOUT deprecated REST endpoints (no 404s)
# -----------------------------------------------------------------------------------
. /opt/idssys/nodemgmt/conf/powerwall/settings.ps1
$ErrorActionPreference = 'Stop'
# ----------------------------
# Logging
# ----------------------------
$LogFile = "/opt/idssys/nodemgmt/logs/vc-ssl.log"
function Write-Log {
param(
[Parameter(Mandatory)][ValidateSet('INFO','WARN','ERROR')]
[string]$Level,
[Parameter(Mandatory)]
[string]$Message
)
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$line = "[{0}] {1}: {2}" -f $ts,$Level,$Message
Write-Host $line
try {
$dir = Split-Path $LogFile -Parent
if (-not (Test-Path $dir)) {
New-Item -Path $dir -ItemType Directory -Force | Out-Null
}
Add-Content -Path $LogFile -Value $line
} catch {}
}
function Show-Failure {
param([System.Management.Automation.ErrorRecord]$ErrorRecord)
Write-Log ERROR $ErrorRecord.Exception.Message
Write-Host "======================================================" -ForegroundColor Red
Write-Host "ERROR: $($ErrorRecord.Exception.Message)" -ForegroundColor Red
Write-Host "======================================================" -ForegroundColor Red
exit 1
}
# ----------------------------
# Safety constants
# ----------------------------
$RenewalWindow = 30
$DnsSleep = 15
# ----------------------------
# Ensure PowerCLI + Posh-ACME
# ----------------------------
try {
if (-not (Get-Module -ListAvailable -Name VMware.PowerCLI)) {
Write-Log INFO "Installing VMware.PowerCLI..."
Install-Module -Name VMware.PowerCLI -Scope AllUsers -Force -AllowClobber
}
Import-Module VMware.PowerCLI -ErrorAction Stop
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null
Write-Log INFO "VMware.PowerCLI loaded."
} catch { Show-Failure $_ }
try {
if (-not (Get-Module -ListAvailable -Name Posh-ACME)) {
Write-Log INFO "Installing Posh-ACME..."
Install-Module -Name Posh-ACME -Scope AllUsers -Force
}
Import-Module Posh-ACME -ErrorAction Stop
Write-Log INFO "Posh-ACME loaded."
} catch { Show-Failure $_ }
# ----------------------------
# Connect to vCenter
# ----------------------------
try {
Write-Log INFO "Connecting to vCenter $VCENTERHOST..."
Connect-VIServer -Server $VCENTERHOST -User $VCENTERUSER -Password $VCENTERPASS -Force | Out-Null
Write-Host "========================================"
Write-Host "SUCCESS: Connected to vCenter."
Write-Host "========================================"
} catch { Show-Failure $_ }
# ----------------------------
# Get VM list (sanity)
# ----------------------------
try {
$vms = Get-VM
Write-Log INFO "Retrieved $($vms.Count) VMs from vCenter."
} catch {
Write-Log WARN "Could not retrieve VM list: $($_.Exception.Message)"
}
# ----------------------------
# Locate Posh-ACME cert object
# ----------------------------
$paCert = $null
try {
$allPaCerts = Get-PACertificate -List
if ($allPaCerts) {
Write-Log INFO "Found $($allPaCerts.Count) Posh-ACME cert(s)."
foreach ($c in $allPaCerts) {
Write-Log INFO (" MainDomain={0} Exp={1}" -f $c.MainDomain, $c.NotAfter)
}
$paCert = $allPaCerts |
Where-Object { $_.MainDomain -eq $VCENTERHOST -or ($_.AllSANs -contains $VCENTERHOST) } |
Sort-Object NotAfter -Descending |
Select-Object -First 1
}
} catch {
Write-Log WARN "Failed to query Posh-ACME certs: $($_.Exception.Message)"
}
$needNewCert = $false
if ($paCert) {
$daysLeft = ($paCert.NotAfter - (Get-Date)).TotalDays
Write-Log INFO "Existing cert expires $($paCert.NotAfter) (~$([math]::Round($daysLeft)) days)."
if ($daysLeft -le $RenewalWindow) { $needNewCert = $true }
else { Write-Log INFO "Skipping ACME issuance: certificate valid >30 days." }
} else {
Write-Log WARN "No existing Posh-ACME cert found. Will issue new."
$needNewCert = $true
}
# ----------------------------
# ACME issuance (if needed)
# ----------------------------
if ($needNewCert) {
try {
$pluginArgs = @{
PowerDNSApiHost = $WDNSHOST
PowerDNSApiKey = (ConvertTo-SecureString $PDNSAPI -AsPlainText -Force)
PowerDNSUseTLS = $true
PowerDNSPort = 443
PowerDNSServerName = "localhost"
}
Write-Log INFO "Requesting new ACME certificate..."
New-PACertificate `
-MainDomain $VCENTERHOST `
-Plugin PowerDNS `
-PluginArgs $pluginArgs `
-Contact $ACMEEMAIL `
-AcceptTOS `
-DnsSleep $DnsSleep `
-Verbose -Force
$paCert = Get-PACertificate
Write-Log INFO "New ACME certificate issued."
}
catch {
Write-Log ERROR "ACME issuance failed: $($_.Exception.Message)"
if (-not $paCert) {
Write-Host "No fallback certificate exists; aborting." -ForegroundColor Red
exit 1
}
Write-Log WARN "Falling back to existing certificate."
}
}
# ----------------------------
# Locate certificate files (LEAF ONLY!)
# ----------------------------
$certFolder = Split-Path $paCert.CertFile -Parent
$certPath = Join-Path $certFolder "cert.cer" # LEAF ONLY
$keyPath = Join-Path $certFolder "cert.key" # PRIVATE KEY
$chainPath = Join-Path $certFolder "chain.cer" # INTERMEDIATE
Write-Log INFO "Using cert folder: $certFolder"
Write-Log INFO " CERT(leaf) : $certPath"
Write-Log INFO " KEY : $keyPath"
Write-Log INFO " CHAIN : $chainPath"
foreach ($f in @($certPath,$keyPath,$chainPath)) {
if (-not (Test-Path $f)) {
Write-Log ERROR "Missing certificate file: $f"
exit 1
}
}
# ----------------------------
# Add chain to trusted store
# ----------------------------
try {
Write-Log INFO "Adding/updating CA chain to vCenter trusted store..."
$pemChain = Get-Content $chainPath -Raw
if ($pemChain.Trim().Length -gt 0) {
Add-VITrustedCertificate -PemCertificateOrChain $pemChain -VCenterOnly -ErrorAction SilentlyContinue | Out-Null
}
} catch {
Write-Log WARN "Failed to add trusted chain: $($_.Exception.Message)"
}
# ----------------------------
# Compare current vCenter cert
# ----------------------------
$vcCert = $null
try { $vcCert = Get-VIMachineCertificate -VCenterOnly }
catch { Write-Log WARN "Unable to read current vCenter cert: $($_.Exception.Message)" }
$needPush = $true
if ($vcCert) {
Write-Log INFO ("Current vCenter cert: Subject={0} NotAfter={1}" -f $vcCert.Subject,$vcCert.NotValidAfter)
if ($vcCert.Thumbprint -eq $paCert.Thumbprint) {
Write-Log INFO "vCenter already using the Posh-ACME certificate."
$needPush = $false
} else {
Write-Log INFO "vCenter cert differs; update required."
}
}
# ----------------------------
# Apply certificate (PowerCLI)
# ----------------------------
if ($needPush) {
try {
$leafPem = Get-Content $certPath -Raw # ONE cert block
$keyPem = Get-Content $keyPath -Raw # private key
Write-Log INFO "Applying certificate via Set-VIMachineCertificate..."
Set-VIMachineCertificate -PemCertificate $leafPem -PemKey $keyPem | Out-Null
Write-Host "==========================================================="
Write-Host "SUCCESS: vCenter Machine SSL certificate updated." -ForegroundColor Green
Write-Host "==========================================================="
Write-Log INFO "Certificate applied successfully."
} catch {
Write-Log ERROR "Failed updating vCenter cert: $($_.Exception.Message)"
Write-Host "===========================================================" -ForegroundColor Red
Write-Host "ERROR applying certificate." -ForegroundColor Red
Write-Host $($_.Exception.Message) -ForegroundColor Red
Write-Host "===========================================================" -ForegroundColor Red
exit 1
}
} else {
Write-Log INFO "Skipping cert update; already matched."
}
# ----------------------------
# Done
# ----------------------------
Write-Host "==========================================================" -ForegroundColor Green
Write-Host "INFO: Script complete. Log: $LogFile" -ForegroundColor Green
Write-Host "=========================================================="