Files
NodeMgmt/inc/vCenter-SSL.ps1

321 lines
12 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 + Posh-ACME + PowerCLI Machine SSL automation
# - Uses Let's Encrypt via Posh-ACME
# - Does NOT re-issue if existing cert has >30 days left
# - Still pushes cert to vCenter if thumbprint differs
# - Uses Set-VIMachineCertificate (no direct REST /rest/... calls)
# -----------------------------------------------------------------------------------
. /opt/idssys/nodemgmt/conf/powerwall/settings.ps1
$ErrorActionPreference = 'Stop'
# ----------------------------
# Config / constants
# ----------------------------
$LogFile = "/opt/idssys/nodemgmt/logs/vc-ssl.log"
$RenewalWindow = 30 # days: don't re-issue if cert still valid longer than this
$DnsSleep = 15 # seconds for Posh-ACME DNS propagation
# ----------------------------
# Logging helper
# ----------------------------
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 ($dir -and -not (Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
Add-Content -Path $LogFile -Value $line
} catch {
Write-Host "[WARN] Failed to write to log file $LogFile : $($_.Exception.Message)" -ForegroundColor Yellow
}
}
# ----------------------------
# Error handler
# ----------------------------
function Show-Failure {
param([System.Management.Automation.ErrorRecord]$ErrorRecord)
$msg = $ErrorRecord.Exception.Message
Write-Log -Level 'ERROR' -Message $msg
Write-Host "================================================================" -ForegroundColor Red
Write-Host "ERROR: A system exception was caught." -ForegroundColor Red
Write-Host $msg -ForegroundColor Red
Write-Host "================================================================" -ForegroundColor Red
exit 1
}
# ----------------------------
# Ensure modules
# ----------------------------
try {
if (-not (Get-Module -ListAvailable -Name VMware.PowerCLI)) {
Write-Log INFO "VMware.PowerCLI not found. Installing..."
Install-Module -Name VMware.PowerCLI -Force -Scope AllUsers -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 "Posh-ACME not found. Installing..."
Install-Module -Name Posh-ACME -Force -Scope AllUsers
}
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..."
$null = Connect-VIServer -Server $VCENTERHOST -User $VCENTERUSER -Password $VCENTERPASS -Force
Write-Host "========================================"
Write-Host "SUCCESS: Connected to vCenter."
Write-Host "========================================"
} catch {
Show-Failure $_
}
# ----------------------------
# Optional: list VMs (sanity check)
# ----------------------------
try {
$vms = Get-VM
Write-Log INFO "Retrieved $($vms.Count) VMs from vCenter."
} catch {
Write-Log WARN "Unable to retrieve VM list. Continuing anyway. Error: $($_.Exception.Message)"
}
# ----------------------------
# Posh-ACME: find existing cert for VCENTERHOST
# ----------------------------
$paCert = $null
try {
$allPaCerts = Get-PACertificate -List
if ($allPaCerts) {
Write-Log INFO "Found $($allPaCerts.Count) Posh-ACME cert(s)."
$allPaCerts |
ForEach-Object {
Write-Log INFO (" MainDomain={0} SANs={1} Exp={2}" -f $_.MainDomain, ($_.AllSANs -join ','), $_.NotAfter)
}
$paCert = $allPaCerts |
Where-Object {
$_.MainDomain -eq $VCENTERHOST -or ($_.AllSANs -contains $VCENTERHOST)
} |
Sort-Object NotAfter -Descending |
Select-Object -First 1
}
} catch {
Write-Log WARN "Get-PACertificate failed: $($_.Exception.Message)"
}
$needNewCert = $false
if ($paCert) {
$daysLeft = ($paCert.NotAfter - (Get-Date)).TotalDays
Write-Log INFO ("Existing cert expires {0} (~{1:N0} days)" -f $paCert.NotAfter, $daysLeft)
if ($daysLeft -le $RenewalWindow) {
Write-Log INFO "Existing cert within $RenewalWindow days of expiry. Will request new ACME certificate."
$needNewCert = $true
} else {
Write-Log INFO "Skipping ACME issuance: Existing certificate valid >$RenewalWindow days."
}
} else {
Write-Log WARN "No existing Posh-ACME certificate found for $VCENTERHOST. Will request new certificate."
$needNewCert = $true
}
# ----------------------------
# ACME / Posh-ACME certificate issuance (if needed)
# ----------------------------
if ($needNewCert) {
try {
# Build PowerDNS plugin args
$pArgs = @{
PowerDNSApiHost = $WDNSHOST
PowerDNSApiKey = (if ($PDNSAPI -is [string]) { ConvertTo-SecureString $PDNSAPI -AsPlainText -Force } else { $PDNSAPI })
PowerDNSUseTLS = $true
PowerDNSPort = 443
PowerDNSServerName = 'localhost'
}
Write-Log INFO "Requesting new ACME certificate via Posh-ACME..."
New-PACertificate `
-MainDomain $VCENTERHOST `
-Plugin PowerDNS `
-PluginArgs $pArgs `
-Contact $ACMEEMAIL `
-AcceptTOS `
-DnsSleep $DnsSleep `
-Force `
-Verbose
# Refresh cert info
$paCert = Get-PACertificate
Write-Log INFO ("New cert issued. NotAfter={0}" -f $paCert.NotAfter)
} catch {
Write-Log ERROR ("ACME request failed: {0}" -f $_.Exception.Message)
Write-Host "================================================================" -ForegroundColor Yellow
Write-Host "WARN: Lets Encrypt may have rate-limited or rejected the request." -ForegroundColor Yellow
Write-Host "================================================================" -ForegroundColor Yellow
if (-not $paCert) {
Write-Host "==================================================" -ForegroundColor Red
Write-Host "ERROR: No usable certificate available. Aborting." -ForegroundColor Red
Write-Host "==================================================" -ForegroundColor Red
exit 1
} else {
Write-Log WARN "Falling back to existing Posh-ACME certificate despite issuance failure."
}
}
}
# Ensure we have a cert object now
if (-not $paCert) {
Write-Log ERROR "No Posh-ACME certificate object available. Aborting."
exit 1
}
# ----------------------------
# Resolve cert file paths from Posh-ACME
# ----------------------------
$certFolder = Split-Path $paCert.CertFile -Parent
if (-not $certFolder) {
Write-Log ERROR "Unable to determine certificate folder from Posh-ACME. Aborting."
exit 1
}
$certPath = $paCert.FullChainFile # cert + chain (ideal for browsers)
$keyPath = $paCert.KeyFile # private key
$chainPath = $paCert.ChainFile # issuing CA chain only (for Add-VITrustedCertificate)
Write-Log INFO "Using Posh-ACME cert folder: $certFolder"
Write-Log INFO "Using certificate files:"
Write-Log INFO " CERT : $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"
Write-Host ""
Write-Host "================================================================" -ForegroundColor Red
Write-Host "ERROR: Missing certificate file: $f" -ForegroundColor Red
Write-Host "================================================================" -ForegroundColor Red
exit 1
}
}
# ----------------------------
# Ensure CA chain is trusted in vCenter
# ----------------------------
try {
$trustedChainPem = Get-Content $chainPath -Raw
if ($trustedChainPem -and $trustedChainPem.Trim().Length -gt 0) {
Write-Log INFO "Adding/updating CA chain in vCenter trusted store (if not already present)..."
Add-VITrustedCertificate -PemCertificateOrChain $trustedChainPem -VCenterOnly -ErrorAction SilentlyContinue | Out-Null
} else {
Write-Log WARN "CHAIN file appears empty; skipping trusted root update."
}
} catch {
Write-Log WARN "Add-VITrustedCertificate failed (non-fatal): $($_.Exception.Message)"
}
# ----------------------------
# Compare current vCenter machine cert vs Posh-ACME cert
# ----------------------------
$currentVcCert = $null
try {
$currentVcCert = Get-VIMachineCertificate -VCenterOnly
} catch {
Write-Log WARN "Get-VIMachineCertificate failed: $($_.Exception.Message). Will assume update is required."
}
$needPushToVc = $true
if ($currentVcCert) {
Write-Log INFO ("Current vCenter cert: Subject='{0}' NotAfter={1} Thumbprint={2}" -f `
$currentVcCert.Subject, $currentVcCert.NotValidAfter, $currentVcCert.Thumbprint)
if ($currentVcCert.Thumbprint -eq $paCert.Thumbprint) {
Write-Log INFO "vCenter is already using the same certificate as Posh-ACME. No update needed."
$needPushToVc = $false
} else {
Write-Log INFO "vCenter certificate differs from Posh-ACME cert. Update required."
}
} else {
Write-Log WARN "Could not read current vCenter machine cert. Assuming update is required."
}
# ----------------------------
# Push certificate into vCenter via Set-VIMachineCertificate
# ----------------------------
if ($needPushToVc) {
try {
Write-Log INFO "Uploading and applying certificate via Set-VIMachineCertificate..."
$pemCert = Get-Content $certPath -Raw # fullchain.cer (leaf + chain)
$pemKey = Get-Content $keyPath -Raw # cert.key (private key)
if (-not $pemCert.Trim()) {
throw "PEM certificate content appears empty."
}
if (-not $pemKey.Trim()) {
throw "PEM key content appears empty."
}
# IMPORTANT:
# Set-VIMachineCertificate understands a PEM cert + separate PEM private key.
# It wraps the vSphere /api/vcenter/certificate-management/vcenter/tls API for us.
Set-VIMachineCertificate -PemCertificate $pemCert -PemKey $pemKey | Out-Null
Write-Host "===================================================================="
Write-Host "SUCCESS: vCenter Machine SSL certificate has been updated." -ForegroundColor Green
Write-Host "===================================================================="
Write-Log INFO "Set-VIMachineCertificate completed successfully."
Write-Log INFO "Note: On vSphere 8.0 U2+ the swap is seamless; older versions may restart vCenter."
} catch {
Write-Log ERROR ("Failed to apply certificate via Set-VIMachineCertificate: {0}" -f $_.Exception.Message)
Write-Host "=============================================================================================" -ForegroundColor Red
Write-Host "ERROR: Failed to apply certificate via Set-VIMachineCertificate." -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
Write-Host "=============================================================================================" -ForegroundColor Red
exit 1
}
} else {
Write-Log INFO "Skipping vCenter certificate update because thumbprints already match."
}
# ----------------------------
# Done
# ----------------------------
Write-Host "==========================================================" -ForegroundColor Green
Write-Host "INFO: Script complete. Log: $LogFile" -ForegroundColor Green
Write-Host "==========================================================" -ForegroundColor Green