diff --git a/inc/vCenter-SSL.ps1 b/inc/vCenter-SSL.ps1 index 7d7a9910..4f029f24 100644 --- a/inc/vCenter-SSL.ps1 +++ b/inc/vCenter-SSL.ps1 @@ -1,18 +1,19 @@ #!/usr/bin/env pwsh # ----------------------------------------------------------------------------------- -# vCenter Machine SSL automation for vSphere 8.0 U3 -# - Uses Let's Encrypt via Posh-ACME (PowerDNS) -# - Skips issuance if existing cert has >30 days remaining -# - Pushes Posh-ACME cert into vCenter using Set-VIMachineCertificate -# - Adds CA chain with Add-VITrustedCertificate -# - Automatically restarts vpxd via Restart-VIApplianceService -# - Runs completely non-interactive (no prompts) +# vCenter Machine SSL Automation Script (vSphere 8.0 U3) +# - Uses Let's Encrypt via Posh-ACME and PowerDNS +# - Skips issuance if existing cert has >30 days +# - Pushes cert into vCenter using Set-VIMachineCertificate +# - Adds CA chain to trusted store +# - Restarts vpxd (Restart-VIApplianceService) +# - Performs Veeam Rescan-VBREntity when certificate changes +# - Fully non-interactive (no prompts) # ----------------------------------------------------------------------------------- . /opt/idssys/nodemgmt/conf/powerwall/settings.ps1 $ErrorActionPreference = 'Stop' -$ConfirmPreference = 'None' # Disable all confirmation prompts +$ConfirmPreference = 'None' # Disable all PowerCLI confirmations # ---------------------------- # Logging @@ -21,32 +22,29 @@ $LogFile = "/opt/idssys/nodemgmt/logs/vc-ssl.log" function Write-Log { param( - [Parameter(Mandatory)][ValidateSet('INFO','WARN','ERROR')] + [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 + + $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + $line = "[$ts] $Level: $Message" Write-Host $line try { - $dir = Split-Path $LogFile -Parent - if ($dir -and -not (Test-Path $dir)) { - New-Item -Path $dir -ItemType Directory -Force | Out-Null + $dir = Split-Path $LogFile + if (-not (Test-Path $dir)) { + New-Item -ItemType Directory -Path $dir -Force | Out-Null } Add-Content -Path $LogFile -Value $line - } catch { - # Logging failure is non-fatal - } + } catch {} } function Show-Failure { param([System.Management.Automation.ErrorRecord]$ErrorRecord) - $msg = $ErrorRecord.Exception.Message - Write-Log ERROR $msg + Write-Log ERROR $msg Write-Host "======================================================" -ForegroundColor Red Write-Host "ERROR: $msg" -ForegroundColor Red Write-Host "======================================================" -ForegroundColor Red @@ -54,35 +52,29 @@ function Show-Failure { } # ---------------------------- -# Safety constants +# Constants # ---------------------------- -$RenewalWindow = 30 # Don't re-issue if cert has > 30 days left -$DnsSleep = 15 # Seconds for DNS propagation for ACME DNS-01 +$RenewalWindow = 30 +$DnsSleep = 15 # ---------------------------- -# Ensure PowerCLI + Posh-ACME +# Load 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 + Install-Module VMware.PowerCLI -Force -Scope AllUsers -AllowClobber } - Import-Module VMware.PowerCLI -ErrorAction Stop - # Disable all confirmations and cert warnings from PowerCLI - Set-PowerCLIConfiguration -Scope AllUsers ` - -InvalidCertificateAction Ignore ` - -Confirm:$false ` - -ParticipateInCEIP:$false ` - -DisplayDeprecationWarnings:$false | Out-Null + Import-Module VMware.PowerCLI -ErrorAction Stop + Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false ` + -ParticipateInCEIP:$false -DisplayDeprecationWarnings:$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 + Install-Module Posh-ACME -Force -Scope AllUsers } Import-Module Posh-ACME -ErrorAction Stop Write-Log INFO "Posh-ACME loaded." @@ -101,36 +93,32 @@ try { } catch { Show-Failure $_ } # ---------------------------- -# Get VM list (sanity check) +# Sanity check: list VMs # ---------------------------- try { $vms = Get-VM Write-Log INFO "Retrieved $($vms.Count) VMs from vCenter." } catch { - Write-Log WARN "Could not retrieve VM list: $($_.Exception.Message)" + Write-Log WARN "Failed to enumerate VMs: $($_.Exception.Message)" } # ---------------------------- -# Locate existing Posh-ACME cert for VCENTERHOST +# Detect existing Posh-ACME cert # ---------------------------- -$paCert = $null +$paCert = $null $needNewCert = $false 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 | + $allCerts = Get-PACertificate -List + if ($allCerts) { + Write-Log INFO "Found $($allCerts.Count) Posh-ACME cert(s)." + $paCert = $allCerts | 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)" + Write-Log WARN "Failed to query Posh-ACME certificates: $($_.Exception.Message)" } if ($paCert) { @@ -138,21 +126,23 @@ if ($paCert) { 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." + Write-Log INFO "Existing cert within $RenewalWindow days. ACME issuance required." $needNewCert = $true } else { - Write-Log INFO "Skipping ACME issuance: certificate valid > $RenewalWindow days." + Write-Log INFO "Skipping issuance — certificate valid >$RenewalWindow days." } } else { - Write-Log WARN "No existing Posh-ACME cert found for $VCENTERHOST. Will request new certificate." + Write-Log WARN "No existing cert found — issuance required." $needNewCert = $true } # ---------------------------- -# ACME issuance (if needed) +# ACME issuance (only if needed) # ---------------------------- if ($needNewCert) { try { + Write-Log INFO "Requesting new ACME certificate via Posh-ACME..." + $pluginArgs = @{ PowerDNSApiHost = $WDNSHOST PowerDNSApiKey = (ConvertTo-SecureString $PDNSAPI -AsPlainText -Force) @@ -161,9 +151,9 @@ if ($needNewCert) { PowerDNSServerName = 'localhost' } - Write-Log INFO "Requesting new ACME certificate via Posh-ACME..." + # Posh-ACME v4 syntax: New-PACertificate ` - -MainDomain $VCENTERHOST ` + -Domain $VCENTERHOST ` -Plugin PowerDNS ` -PluginArgs $pluginArgs ` -Contact $ACMEEMAIL ` @@ -173,137 +163,134 @@ if ($needNewCert) { -Force $paCert = Get-PACertificate - Write-Log INFO ("New ACME certificate issued. NotAfter={0}" -f $paCert.NotAfter) + Write-Log INFO ("New certificate issued: NotAfter={0}" -f $paCert.NotAfter) } catch { Write-Log ERROR ("ACME issuance failed: {0}" -f $_.Exception.Message) if (-not $paCert) { - Write-Host "No fallback certificate exists; aborting." -ForegroundColor Red + Write-Log ERROR "No fallback certificate exists — aborting." exit 1 } - Write-Log WARN "Falling back to existing certificate." + Write-Log WARN "Using existing Posh-ACME certificate." } } -# Ensure we have a certificate at this point if (-not $paCert) { - Write-Log ERROR "No usable Posh-ACME certificate available. Aborting." + Write-Log ERROR "No usable certificate available — aborting." exit 1 } # ---------------------------- -# Locate certificate files (LEAF ONLY for vCenter) +# Certificate file resolution # ---------------------------- $certFolder = Split-Path $paCert.CertFile -Parent -if (-not $certFolder) { - Write-Log ERROR "Unable to determine certificate folder from Posh-ACME. Aborting." - exit 1 -} - -# Posh-ACME outputs: -# cert.cer = leaf certificate -# cert.key = private key -# chain.cer = intermediate chain -# fullchain.cer = leaf + chain (NOT used by Set-VIMachineCertificate) -$certPath = Join-Path $certFolder "cert.cer" # leaf only -$keyPath = Join-Path $certFolder "cert.key" # private key -$chainPath = Join-Path $certFolder "chain.cer" # intermediate CA(s) +$certPath = Join-Path $certFolder "cert.cer" +$keyPath = Join-Path $certFolder "cert.key" +$chainPath = Join-Path $certFolder "chain.cer" Write-Log INFO "Using cert folder: $certFolder" -Write-Log INFO " CERT(leaf) : $certPath" -Write-Log INFO " KEY : $keyPath" -Write-Log INFO " CHAIN : $chainPath" +Write-Log INFO " CERT : $certPath" +Write-Log INFO " KEY : $keyPath" +Write-Log INFO " CHAIN: $chainPath" -foreach ($f in @($certPath, $keyPath, $chainPath)) { +foreach ($f in @($certPath,$keyPath,$chainPath)) { if (-not (Test-Path $f)) { - Write-Log ERROR "Missing certificate file: $f" + Write-Log ERROR "Missing cert file: $f" exit 1 } } # ---------------------------- -# Add CA chain to vCenter trusted store +# Add CA chain to trusted store (remove duplicates) # ---------------------------- try { - Write-Log INFO "Adding/updating CA chain in vCenter trusted store..." - $pemChain = Get-Content $chainPath -Raw - if ($pemChain.Trim().Length -gt 0) { - Add-VITrustedCertificate -PemCertificateOrChain $pemChain -VCenterOnly -ErrorAction SilentlyContinue | Out-Null - } else { - Write-Log WARN "CHAIN file appears empty; skipping trusted chain update." + Write-Log INFO "Cleaning old CA trust entries..." + $issuer = ($paCert.Issuer) + $existingCA = Get-VITrustedCertificate | Where-Object { $_.Subject -eq $issuer } + foreach ($ca in $existingCA) { + Remove-VITrustedCertificate -Certificate $ca -Confirm:$false -ErrorAction SilentlyContinue } + + $pemChain = Get-Content $chainPath -Raw + Write-Log INFO "Adding CA chain to vCenter trust store..." + Add-VITrustedCertificate -PemCertificateOrChain $pemChain -VCenterOnly -Confirm:$false | Out-Null + } catch { - Write-Log WARN "Failed to add trusted chain (non-fatal): $($_.Exception.Message)" + Write-Log WARN "Failed to manage CA trust entries: $($_.Exception.Message)" } # ---------------------------- -# Compare current vCenter Machine SSL cert +# Compare current vCenter cert # ---------------------------- -$vcCert = $null $needPush = $true - try { - $vcCert = Get-VIMachineCertificate -VCenterOnly -} catch { - Write-Log WARN "Unable to read current vCenter certificate: $($_.Exception.Message). Assuming update required." -} - -if ($vcCert) { + $vcCert = Get-VIMachineCertificate -VCenterOnly -ErrorAction Stop 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 same certificate as Posh-ACME. No update necessary." + Write-Log INFO "vCenter already using this certificate." $needPush = $false - } else { - Write-Log INFO "vCenter certificate differs from Posh-ACME cert. Update required." } +} catch { + Write-Log WARN "Unable to read vCenter cert, assuming update required." } # ---------------------------- -# Apply certificate via Set-VIMachineCertificate +# Apply new certificate # ---------------------------- if ($needPush) { + + Write-Log INFO "Applying new Machine SSL certificate..." + + $leafPem = Get-Content $certPath -Raw + $keyPem = Get-Content $keyPath -Raw + try { - $leafPem = Get-Content $certPath -Raw - $keyPem = Get-Content $keyPath -Raw - - if (-not $leafPem.Trim()) { throw "Leaf certificate PEM appears empty." } - if (-not $keyPem.Trim()) { throw "Private key PEM appears empty." } - - Write-Log INFO "Applying certificate via Set-VIMachineCertificate..." - Set-VIMachineCertificate -PemCertificate $leafPem -PemKey $keyPem | Out-Null + Set-VIMachineCertificate -PemCertificate $leafPem -PemKey $keyPem -Confirm:$false | Out-Null Write-Host "===========================================================" Write-Host "SUCCESS: vCenter Machine SSL certificate updated." -ForegroundColor Green Write-Host "===========================================================" - Write-Log INFO "Certificate applied successfully." + + Write-Log INFO "Certificate updated successfully." # ---------------------------- - # Restart vpxd service via appliance APIs + # Restart vpxd service # ---------------------------- try { - Write-Log INFO "Restarting vpxd service via Restart-VIApplianceService..." - # vpxd service name is typically 'vpxd' - $vpxdSvc = Get-VIApplianceService -Name 'vpxd' -ErrorAction Stop - if ($vpxdSvc) { - $null = $vpxdSvc | Restart-VIApplianceService -Confirm:$false - Write-Log INFO "vpxd restart requested successfully." - } else { - Write-Log WARN "vpxd service not found via Get-VIApplianceService. Please check manually." - } + Write-Log INFO "Restarting vpxd via Restart-VIApplianceService..." + $svc = Get-VIApplianceService -Name 'vpxd' -ErrorAction Stop + $null = $svc | Restart-VIApplianceService -Confirm:$false + Write-Log INFO "vpxd restarted successfully." } catch { - Write-Log WARN "Automatic vpxd restart failed: $($_.Exception.Message). Please restart vCenter services manually if required." + Write-Log WARN "vpxd restart failed: $($_.Exception.Message)" + } + + # ---------------------------- + # Trigger Veeam rescan + # ---------------------------- + if ($VEEAMHOSTSSH) { + try { + Write-Log INFO "Triggering Veeam host rescan on $VEEAMHOSTSSH..." + $veeamCmd = "Rescan-VBREntity -AllHosts" + $sshCmd = "ssh -tq -o ConnectTimeout=3 -o ConnectionAttempts=1 $VEEAMHOSTSSH '$veeamCmd'" + $result = bash -c $sshCmd + Write-Log INFO "Veeam rescan result: $result" + } catch { + Write-Log WARN "Veeam rescan failed: $($_.Exception.Message)" + } } } catch { Show-Failure $_ } + } else { - Write-Log INFO "Skipping vCenter certificate update; thumbprints already match." + Write-Log INFO "No certificate update needed. Skipping vpxd restart + Veeam rescan." } # ---------------------------- -# Done +# Script Complete # ---------------------------- Write-Host "==========================================================" -ForegroundColor Green Write-Host "INFO: Script complete. Log: $LogFile" -ForegroundColor Green