Files
NodeMgmt/inc/vCenter-SSL.ps1

419 lines
15 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 (Posh-ACME + PowerDNS + vCenter REST CertMgmt)
# - Uses ~/.config/Posh-ACME/LE_PROD/.../vcenter.scity.us
# - Files: cert.cer / cert.key / fullchain.cer
# - vCenter Cert API: /rest/vcenter/certificate-management/vcenter/tls
# - Auth: CIS REST session token (vmware-api-session-id)
# -----------------------------------------------------------------------------------
. /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 (PowerCLI)
# ----------------------------
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 $_ }
# Optional: show VMs just to confirm connectivity
try {
$vms = Get-VM
Write-Log INFO "Retrieved $($vms.Count) VMs from vCenter." Cyan
} catch {
Write-Log WARN "Unable to retrieve VM list, continuing..." Yellow
}
# ----------------------------
# 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 (we'll use this as 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 via Posh-ACME..." 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
# -------------------------------------------------------
# Create CIS REST session (vmware-api-session-id)
# -------------------------------------------------------
try {
$loginUri = "https://$VCENTERHOST/rest/com/vmware/cis/session"
$creds = "$VCENTERUSER`:$VCENTERPASS"
$authHeader = "Basic " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($creds))
$sessionResponse = Invoke-RestMethod -Uri $loginUri `
-Method Post `
-Headers @{ Authorization = $authHeader } `
-SkipCertificateCheck `
-ErrorAction Stop
$sessionToken = $sessionResponse.value
$restHeaders = @{ "vmware-api-session-id" = $sessionToken }
Write-Log INFO "CIS REST session established." Green
}
catch {
Show-Banner "Failed to create CIS REST session: $($_.Exception.Message)" ERROR
exit 1
}
# -------------------------------------------------------
# OPTIONAL: compare fingerprints using REST cert GET
# -------------------------------------------------------
$updateNeeded = $true
try {
$getUri = "https://$VCENTERHOST/rest/vcenter/certificate-management/vcenter/tls"
$resp = Invoke-RestMethod -Uri $getUri -Method Get -Headers $restHeaders -SkipCertificateCheck -ErrorAction Stop
$currentPem = $resp.value.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. No update needed." SUCCESS
$updateNeeded = $false
} else {
Write-Log INFO "vCenter certificate differs from Posh-ACME cert. Update required." Yellow
}
} else {
Write-Log WARN "vCenter cert GET did not return 'cert'. Assuming update required." Yellow
}
}
catch {
Write-Log WARN "Could not read vCenter certificate via REST; assuming update required. $($_.Exception.Message)" Yellow
}
# -------------------------------------------------------
# Upload + apply certificate via /rest/vcenter/certificate-management/vcenter/tls
# -------------------------------------------------------
$restartNeeded = $false
if ($updateNeeded) {
try {
$uploadUri = "https://$VCENTERHOST/rest/vcenter/certificate-management/vcenter/tls"
$applyUri = "https://$VCENTERHOST/rest/vcenter/certificate-management/vcenter/tls?action=apply"
$body = @{
cert = Get-Content -Raw $certPath
key = Get-Content -Raw $keyPath
chain = Get-Content -Raw $chainPath
}
Write-Log INFO "Uploading certificate to vCenter via /rest/vcenter/certificate-management..." Cyan
Invoke-RestMethod -Uri $uploadUri `
-Method Post `
-Headers $restHeaders `
-ContentType "application/json" `
-Body ($body | ConvertTo-Json -Compress) `
-SkipCertificateCheck `
-ErrorAction Stop
Write-Log INFO "Applying uploaded certificate..." Cyan
Invoke-RestMethod -Uri $applyUri `
-Method Post `
-Headers $restHeaders `
-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 (try REST, then show SSH fallback)
# -------------------------------------------------------
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 -Headers $restHeaders -SkipCertificateCheck -ErrorAction Stop
Write-Log INFO "Requesting vpxd restart via REST..." Cyan
Invoke-RestMethod -Uri $restartUri -Method Post -Headers $restHeaders -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 ($msg -like "*404*" -or $msg -like "*Not Found*") {
Write-Log WARN "vpxd REST endpoint not available (404). Falling 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-Host " ssh root@$VCENTERHOST 'service-control --stop vpxd; service-control --start vpxd'" -ForegroundColor Yellow
}
}
Show-Banner "Script complete. Log: $LogFile" INFO