scripts: Request-VsqlServerCertificate.ps1 requests and installs a SQL Server TLS certificate from PESCO-PSYLOCKE-CA
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Requests a SQL Server TLS certificate for vsql from PESCO-PSYLOCKE-CA and installs it on the instances.
|
||||
|
||||
.DESCRIPTION
|
||||
Run on the SQL Server host (the machine behind vsql.pesco.com) in an elevated PowerShell, as a
|
||||
domain account that may enroll from the chosen template. The script:
|
||||
|
||||
1. Writes a certreq INF with the requirements Packet Station needs: RSA 2048, SHA-256,
|
||||
Server Authentication EKU, subject CN=vsql.pesco.com, subject alternative names
|
||||
vsql.pesco.com, vsql, the host's own FQDN and short name, key in the Microsoft Software
|
||||
Key Storage Provider, exportable, in the machine store.
|
||||
2. Submits the request to the CA and installs the issued certificate.
|
||||
3. Grants the SQL Server service account(s) read access to the private key.
|
||||
4. Points each SQL Server instance at the certificate (the same registry value that SQL Server
|
||||
Configuration Manager sets). ForceEncryption is left as it is.
|
||||
5. Prints the thumbprint and what to restart. With -RestartInstances it restarts the services
|
||||
(a short VISUAL outage; run off-hours).
|
||||
|
||||
Afterwards, from docker-1:
|
||||
docker compose ... run --rm --no-deps -e VISUAL_ENCRYPT=yes worker doctor --skip mfiles,db,volume,smtp,clamav,ldap
|
||||
should show "visual: Connect PASS" once the image trusts the PESCO CA.
|
||||
|
||||
.PARAMETER CertificateAuthority
|
||||
The CA config string, host\name. Default DC-02.PESCO.COM\PESCO-PSYLOCKE-CA.
|
||||
|
||||
.PARAMETER Template
|
||||
The certificate template name. Default WebServer. Use the template IT prefers for servers; it
|
||||
must allow Server Authentication and an exportable private key is not required but is set.
|
||||
|
||||
.PARAMETER Instances
|
||||
SQL Server instance names to configure. Default: every instance found on this host
|
||||
(MSSQLSERVER for the default instance, DEV for vsql\DEV, and so on).
|
||||
|
||||
.PARAMETER RestartInstances
|
||||
Restart each configured instance so the certificate takes effect.
|
||||
|
||||
.EXAMPLE
|
||||
.\Request-VsqlServerCertificate.ps1
|
||||
Requests and installs the certificate, configures every instance, prints restart instructions.
|
||||
|
||||
.EXAMPLE
|
||||
.\Request-VsqlServerCertificate.ps1 -Instances DEV -RestartInstances
|
||||
Configures only the DEV instance and restarts it.
|
||||
|
||||
.EXAMPLE
|
||||
.\Request-VsqlServerCertificate.ps1 -WhatIf
|
||||
Shows what would happen without changing anything.
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[string]$CertificateAuthority = 'DC-02.PESCO.COM\PESCO-PSYLOCKE-CA',
|
||||
[string]$Template = 'WebServer',
|
||||
[string[]]$DnsNames = @('vsql.pesco.com', 'vsql'),
|
||||
[string[]]$Instances,
|
||||
[switch]$RestartInstances
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
throw 'Run this in an elevated PowerShell on the SQL Server host.'
|
||||
}
|
||||
|
||||
$fqdn = [System.Net.Dns]::GetHostEntry($env:COMPUTERNAME).HostName
|
||||
$short = $env:COMPUTERNAME
|
||||
$names = @($DnsNames + $fqdn + $short) | Select-Object -Unique
|
||||
$primary = $DnsNames[0]
|
||||
Write-Host "Host $fqdn. Certificate for CN=$primary with SANs: $($names -join ', ')"
|
||||
|
||||
# --- 1. INF for certreq ---------------------------------------------------------------------
|
||||
$work = Join-Path $env:ProgramData 'PacketStation\vsql-cert'
|
||||
New-Item -ItemType Directory -Force -Path $work | Out-Null
|
||||
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$inf = Join-Path $work "vsql-$stamp.inf"
|
||||
$csr = Join-Path $work "vsql-$stamp.req"
|
||||
$cer = Join-Path $work "vsql-$stamp.cer"
|
||||
|
||||
$sanLines = ($names | ForEach-Object { "_continue_ = `"dns=$_&`"" }) -join "`r`n"
|
||||
@"
|
||||
[Version]
|
||||
Signature = "`$Windows NT`$"
|
||||
|
||||
[NewRequest]
|
||||
Subject = "CN=$primary"
|
||||
KeyLength = 2048
|
||||
KeyAlgorithm = RSA
|
||||
HashAlgorithm = SHA256
|
||||
ProviderName = "Microsoft Software Key Storage Provider"
|
||||
KeySpec = 0
|
||||
KeyUsage = 0xA0
|
||||
Exportable = TRUE
|
||||
MachineKeySet = TRUE
|
||||
RequestType = PKCS10
|
||||
FriendlyName = "SQL Server TLS $primary ($stamp)"
|
||||
|
||||
[EnhancedKeyUsageExtension]
|
||||
OID = 1.3.6.1.5.5.7.3.1
|
||||
|
||||
[Extensions]
|
||||
2.5.29.17 = "{text}"
|
||||
$sanLines
|
||||
|
||||
[RequestAttributes]
|
||||
CertificateTemplate = $Template
|
||||
"@ | Set-Content -Path $inf -Encoding ASCII
|
||||
|
||||
Write-Host "INF written: $inf"
|
||||
|
||||
if (-not $PSCmdlet.ShouldProcess($CertificateAuthority, "request certificate from template $Template")) { return }
|
||||
|
||||
# --- 2. request, submit, accept -------------------------------------------------------------
|
||||
& certreq -new -q $inf $csr | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) { throw "certreq -new failed ($LASTEXITCODE)" }
|
||||
|
||||
& certreq -submit -q -config $CertificateAuthority $csr $cer | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) { throw "certreq -submit failed ($LASTEXITCODE). If the request is pending, have the CA admin issue it, then run: certreq -retrieve <RequestId> `"$cer`" and certreq -accept `"$cer`"" }
|
||||
|
||||
& certreq -accept -q $cer | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) { throw "certreq -accept failed ($LASTEXITCODE)" }
|
||||
|
||||
$issued = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $cer
|
||||
$cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Thumbprint -eq $issued.Thumbprint }
|
||||
if (-not $cert) { throw "Issued certificate $($issued.Thumbprint) not found in the machine store." }
|
||||
$thumb = $cert.Thumbprint.ToLower()
|
||||
|
||||
$keySize = $cert.PublicKey.Key.KeySize
|
||||
$sig = $cert.SignatureAlgorithm.FriendlyName
|
||||
Write-Host "Installed: $($cert.Subject) thumbprint $thumb, $keySize-bit, $sig, expires $($cert.NotAfter.ToString('yyyy-MM-dd'))"
|
||||
if ($keySize -lt 2048 -or $sig -notmatch 'sha256') { Write-Warning 'The CA issued something weaker than requested. Check the template and the CA hash setting.' }
|
||||
|
||||
# --- 3. private key read access for the SQL Server service accounts -------------------------
|
||||
$services = Get-CimInstance Win32_Service | Where-Object { $_.Name -eq 'MSSQLSERVER' -or $_.Name -like 'MSSQL$*' }
|
||||
if (-not $services) { Write-Warning 'No SQL Server service found on this host. Is this the vsql machine?' }
|
||||
|
||||
$rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
|
||||
$keyName = $rsa.Key.UniqueName
|
||||
$keyFile = Get-ChildItem -Path "$env:ProgramData\Microsoft\Crypto\Keys", "$env:ProgramData\Microsoft\Crypto\RSA\MachineKeys" -Filter $keyName -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $keyFile) { throw "Private key file $keyName not found under ProgramData\Microsoft\Crypto." }
|
||||
|
||||
foreach ($svc in $services) {
|
||||
$acct = $svc.StartName
|
||||
if ($PSCmdlet.ShouldProcess($keyFile.FullName, "grant read to $acct")) {
|
||||
& icacls $keyFile.FullName /grant "${acct}:(R)" | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { Write-Warning "icacls failed for $acct; grant Read on the private key by hand (certlm.msc, Manage Private Keys)." }
|
||||
else { Write-Host "Private key read granted to $acct ($($svc.Name))" }
|
||||
}
|
||||
}
|
||||
|
||||
# --- 4. point each instance at the certificate -----------------------------------------------
|
||||
$instanceRoot = 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server'
|
||||
$instanceMap = Get-ItemProperty "$instanceRoot\Instance Names\SQL" -ErrorAction SilentlyContinue
|
||||
if (-not $instanceMap) { throw 'No SQL Server instances are registered on this host.' }
|
||||
$all = $instanceMap.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' } | ForEach-Object { @{ Name = $_.Name; Id = $_.Value } }
|
||||
if ($Instances) { $all = $all | Where-Object { $Instances -contains $_.Name } }
|
||||
if (-not $all) { throw "None of the requested instances ($($Instances -join ', ')) exist here. Found: $(($instanceMap.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' } | ForEach-Object Name) -join ', ')" }
|
||||
|
||||
foreach ($inst in $all) {
|
||||
$key = "$instanceRoot\$($inst.Id)\MSSQLServer\SuperSocketNetLib"
|
||||
if ($PSCmdlet.ShouldProcess("$($inst.Name) ($key)", "set Certificate=$thumb")) {
|
||||
Set-ItemProperty -Path $key -Name Certificate -Value $thumb
|
||||
$force = (Get-ItemProperty $key -Name ForceEncryption -ErrorAction SilentlyContinue).ForceEncryption
|
||||
Write-Host "Instance $($inst.Name): Certificate set (ForceEncryption=$force, unchanged)"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 5. restart ------------------------------------------------------------------------------
|
||||
$toRestart = $all | ForEach-Object { if ($_.Name -eq 'MSSQLSERVER') { 'MSSQLSERVER' } else { "MSSQL`$$($_.Name)" } }
|
||||
if ($RestartInstances) {
|
||||
foreach ($s in $toRestart) {
|
||||
if ($PSCmdlet.ShouldProcess($s, 'restart service')) { Restart-Service -Name $s -Force; Write-Host "Restarted $s" }
|
||||
}
|
||||
} else {
|
||||
Write-Host ''
|
||||
Write-Host 'Restart the instances for the certificate to take effect (off-hours; VISUAL users lose their connection):'
|
||||
$toRestart | ForEach-Object { Write-Host " Restart-Service -Name '$_' -Force" }
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host "Done. Thumbprint $thumb. The SQL Server error log should show 'The certificate [Cn = $primary] was successfully loaded for encryption' after the restart."
|
||||
Write-Host 'Then tell Packet Station: the stacks switch to VISUAL_ENCRYPT=yes (Gitea issue 5).'
|
||||
Reference in New Issue
Block a user