scripts: vsql certificate request uses the legacy SChannel CSP with an exchange key, validates before binding, scopes key access, saves bindings for rollback, resumes pending requests
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
@@ -1,83 +1,207 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Requests a SQL Server TLS certificate for vsql from PESCO-PSYLOCKE-CA and installs it on the instances.
|
||||
Requests a SQL Server TLS certificate for vsql from PESCO-PSYLOCKE-CA and binds it to 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:
|
||||
domain account that may enroll from the chosen template. Steps:
|
||||
|
||||
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).
|
||||
1. Resolve the SQL Server instances to configure and their service accounts. Unknown names
|
||||
in -Instances are an error, nothing is touched.
|
||||
2. Write a certreq INF with what SQL Server requires (Microsoft certificate requirements):
|
||||
a legacy CryptoAPI provider, Microsoft RSA SChannel Cryptographic Provider, with KeySpec
|
||||
AT_KEYEXCHANGE; RSA 2048; SHA-256; Server Authentication EKU; key usage digital signature
|
||||
and key encipherment; subject CN=vsql.pesco.com; subject alternative names vsql.pesco.com,
|
||||
vsql, the host FQDN and short name; machine store; exportable.
|
||||
3. Submit to the CA and install the issued certificate. A pending request is saved in a state
|
||||
file; issue it on the CA, then rerun with -CerFile (or -Thumbprint once installed).
|
||||
4. Validate the installed certificate before anything is bound: private key present and a
|
||||
CSP exchange key, 2048 bits or more, SHA-256 family signature, valid dates, Server
|
||||
Authentication EKU, every required SAN.
|
||||
5. Grant the selected instances' service accounts read on the private key. A failure stops
|
||||
the run before any instance is changed.
|
||||
6. Save each instance's current Certificate value to the state file, then bind the new
|
||||
thumbprint (the registry value SQL Server Configuration Manager sets). ForceEncryption is
|
||||
left as it is. A failure restores the values already changed.
|
||||
7. Print the restart commands, or restart with -RestartInstances (a short VISUAL outage).
|
||||
|
||||
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.
|
||||
-WhatIf previews every step, instance bindings and restarts included, without requesting.
|
||||
-Rollback restores the Certificate values saved by the last run.
|
||||
|
||||
.PARAMETER CertificateAuthority
|
||||
The CA config string, host\name. Default DC-02.PESCO.COM\PESCO-PSYLOCKE-CA.
|
||||
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.
|
||||
Certificate template name. Default WebServer. The template must allow Server Authentication
|
||||
and must not force a Key Storage Provider.
|
||||
|
||||
.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).
|
||||
Instance names to configure (MSSQLSERVER for the default instance, DEV for vsql\DEV).
|
||||
Default: every instance on this host.
|
||||
|
||||
.PARAMETER CerFile
|
||||
Continue from an issued certificate file (after a pending request was issued and retrieved).
|
||||
|
||||
.PARAMETER Thumbprint
|
||||
Continue from a certificate already in the machine store; skips the request.
|
||||
|
||||
.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.
|
||||
.PARAMETER Rollback
|
||||
Restore the Certificate values saved by the last run and stop.
|
||||
|
||||
.EXAMPLE
|
||||
.\Request-VsqlServerCertificate.ps1 -WhatIf
|
||||
Shows what would happen without changing anything.
|
||||
.EXAMPLE
|
||||
.\Request-VsqlServerCertificate.ps1 -Instances DEV
|
||||
.EXAMPLE
|
||||
.\Request-VsqlServerCertificate.ps1 -CerFile C:\ProgramData\PacketStation\vsql-cert\vsql-20261018-090000.cer -Instances DEV -RestartInstances
|
||||
.EXAMPLE
|
||||
.\Request-VsqlServerCertificate.ps1 -Rollback
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
[CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Request')]
|
||||
param(
|
||||
[string]$CertificateAuthority = 'DC-02.PESCO.COM\PESCO-PSYLOCKE-CA',
|
||||
[string]$Template = 'WebServer',
|
||||
[string[]]$DnsNames = @('vsql.pesco.com', 'vsql'),
|
||||
[string[]]$Instances,
|
||||
[switch]$RestartInstances
|
||||
[Parameter(ParameterSetName = 'Cer')][string]$CerFile,
|
||||
[Parameter(ParameterSetName = 'Thumb')][string]$Thumbprint,
|
||||
[switch]$RestartInstances,
|
||||
[Parameter(ParameterSetName = 'Rollback')][switch]$Rollback
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$acceptedSignatures = @('sha256RSA', 'sha384RSA', 'sha512RSA')
|
||||
$work = Join-Path $env:ProgramData 'PacketStation\vsql-cert'
|
||||
$stateFile = Join-Path $work 'state.json'
|
||||
$instanceRoot = 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server'
|
||||
|
||||
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.'
|
||||
function Test-Admin {
|
||||
([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function Read-State {
|
||||
if (Test-Path $stateFile) { Get-Content $stateFile -Raw | ConvertFrom-Json } else { [pscustomobject]@{} }
|
||||
}
|
||||
|
||||
function Write-State([object]$state) {
|
||||
New-Item -ItemType Directory -Force -Path $work | Out-Null
|
||||
$state | ConvertTo-Json -Depth 5 | Set-Content -Path $stateFile -Encoding UTF8
|
||||
}
|
||||
|
||||
function Get-SqlInstances([string[]]$wanted) {
|
||||
$map = Get-ItemProperty "$instanceRoot\Instance Names\SQL" -ErrorAction SilentlyContinue
|
||||
if (-not $map) { throw 'No SQL Server Database Engine instance is registered on this host. Is this the vsql machine?' }
|
||||
$all = @($map.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' } | ForEach-Object {
|
||||
$name = $_.Name
|
||||
$svc = if ($name -eq 'MSSQLSERVER') { 'MSSQLSERVER' } else { "MSSQL`$$name" }
|
||||
$service = Get-CimInstance Win32_Service -Filter "Name='$($svc.Replace('$','\$'))'" -ErrorAction SilentlyContinue
|
||||
[pscustomobject]@{
|
||||
Name = $name
|
||||
Id = $_.Value
|
||||
Service = $svc
|
||||
Account = if ($service) { $service.StartName } else { $null }
|
||||
RegKey = "$instanceRoot\$($_.Value)\MSSQLServer\SuperSocketNetLib"
|
||||
}
|
||||
})
|
||||
if ($wanted) {
|
||||
$unknown = @($wanted | Where-Object { $_ -notin $all.Name })
|
||||
if ($unknown) { throw "Unknown instance(s): $($unknown -join ', '). Found on this host: $($all.Name -join ', ')" }
|
||||
$all = @($all | Where-Object { $wanted -contains $_.Name })
|
||||
}
|
||||
foreach ($i in $all) {
|
||||
if (-not $i.Account) { throw "Service $($i.Service) for instance $($i.Name) was not found." }
|
||||
if (-not (Test-Path $i.RegKey)) { throw "Registry key for instance $($i.Name) not found: $($i.RegKey)" }
|
||||
}
|
||||
return $all
|
||||
}
|
||||
|
||||
function Test-ServerCertificate([System.Security.Cryptography.X509Certificates.X509Certificate2]$cert, [string[]]$requiredNames) {
|
||||
$problems = New-Object System.Collections.Generic.List[string]
|
||||
if (-not $cert.HasPrivateKey) { $problems.Add('no private key in the machine store') }
|
||||
$csp = $null
|
||||
if ($cert.HasPrivateKey) {
|
||||
$pk = $cert.PrivateKey
|
||||
if ($pk -is [System.Security.Cryptography.RSACryptoServiceProvider]) {
|
||||
$csp = $pk.CspKeyContainerInfo
|
||||
if ($csp.KeyNumber -ne [System.Security.Cryptography.KeyNumber]::Exchange) { $problems.Add("KeySpec is $($csp.KeyNumber), SQL Server needs AT_KEYEXCHANGE") }
|
||||
if ($csp.ProviderType -ne 12) { $problems.Add("provider type $($csp.ProviderType) ($($csp.ProviderName)); expected 12, Microsoft RSA SChannel Cryptographic Provider") }
|
||||
}
|
||||
else {
|
||||
$problems.Add('the private key is not a CryptoAPI (CSP) key; a KSP/CNG key is not supported by SQL Server')
|
||||
}
|
||||
}
|
||||
$keySize = $cert.PublicKey.Key.KeySize
|
||||
if ($keySize -lt 2048) { $problems.Add("key size $keySize is below 2048") }
|
||||
$sig = $cert.SignatureAlgorithm.FriendlyName
|
||||
if ($sig -notin $acceptedSignatures) { $problems.Add("signature algorithm $sig is not one of $($acceptedSignatures -join ', ')") }
|
||||
$now = Get-Date
|
||||
if ($cert.NotBefore -gt $now -or $cert.NotAfter -lt $now.AddDays(30)) { $problems.Add("validity $($cert.NotBefore.ToString('yyyy-MM-dd')) to $($cert.NotAfter.ToString('yyyy-MM-dd')) is not usable") }
|
||||
$eku = $cert.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.37' }
|
||||
if (-not $eku -or -not ($eku.EnhancedKeyUsages | Where-Object { $_.Value -eq '1.3.6.1.5.5.7.3.1' })) { $problems.Add('Server Authentication EKU missing') }
|
||||
$ku = $cert.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.15' }
|
||||
if ($ku -and -not ($ku.KeyUsages -band [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::KeyEncipherment)) { $problems.Add('key usage lacks key encipherment') }
|
||||
$san = $cert.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.17' }
|
||||
$sanText = if ($san) { $san.Format($false) } else { '' }
|
||||
foreach ($n in $requiredNames) {
|
||||
if ($sanText -notmatch [regex]::Escape("DNS Name=$n")) { $problems.Add("subject alternative name $n missing") }
|
||||
}
|
||||
return [pscustomobject]@{ Problems = $problems; Csp = $csp; KeySize = $keySize; Signature = $sig }
|
||||
}
|
||||
|
||||
# --- rollback --------------------------------------------------------------------------------
|
||||
if (Test-Admin -eq $false) { throw 'Run this in an elevated PowerShell on the SQL Server host.' }
|
||||
|
||||
if ($Rollback) {
|
||||
$state = Read-State
|
||||
if (-not $state.previous) { throw "No saved bindings in $stateFile." }
|
||||
foreach ($p in $state.previous.PSObject.Properties) {
|
||||
$inst = $p.Name; $value = $p.Value.value; $key = $p.Value.regKey
|
||||
if ($PSCmdlet.ShouldProcess("instance $inst", "restore Certificate='$value'")) {
|
||||
if ([string]::IsNullOrEmpty($value)) { Remove-ItemProperty -Path $key -Name Certificate -ErrorAction SilentlyContinue }
|
||||
else { Set-ItemProperty -Path $key -Name Certificate -Value $value }
|
||||
Write-Host "Restored $inst to '$value'"
|
||||
}
|
||||
}
|
||||
Write-Host 'Restart the instances for the rollback to take effect.'
|
||||
return
|
||||
}
|
||||
|
||||
# --- 1. instances ------------------------------------------------------------------------------
|
||||
$targets = Get-SqlInstances $Instances
|
||||
$fqdn = [System.Net.Dns]::GetHostEntry($env:COMPUTERNAME).HostName
|
||||
$short = $env:COMPUTERNAME
|
||||
$names = @($DnsNames + $fqdn + $short) | Select-Object -Unique
|
||||
$names = @($DnsNames + $fqdn + $env:COMPUTERNAME) | Select-Object -Unique
|
||||
$primary = $DnsNames[0]
|
||||
Write-Host "Host $fqdn. Certificate for CN=$primary with SANs: $($names -join ', ')"
|
||||
Write-Host "Host $fqdn. Instances: $(($targets | ForEach-Object { "$($_.Name) [$($_.Account)]" }) -join ', ')"
|
||||
Write-Host "Certificate CN=$primary, SANs: $($names -join ', ')"
|
||||
|
||||
# --- 1. INF for certreq ---------------------------------------------------------------------
|
||||
$work = Join-Path $env:ProgramData 'PacketStation\vsql-cert'
|
||||
New-Item -ItemType Directory -Force -Path $work | Out-Null
|
||||
$cert = $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"
|
||||
New-Item -ItemType Directory -Force -Path $work | Out-Null
|
||||
|
||||
$sanLines = ($names | ForEach-Object { "_continue_ = `"dns=$_&`"" }) -join "`r`n"
|
||||
@"
|
||||
if ($PSCmdlet.ParameterSetName -eq 'Thumb') {
|
||||
$cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Thumbprint -eq $Thumbprint.ToUpper() }
|
||||
if (-not $cert) { throw "Certificate $Thumbprint is not in the machine store." }
|
||||
}
|
||||
elseif ($PSCmdlet.ParameterSetName -eq 'Cer') {
|
||||
if (-not (Test-Path $CerFile)) { throw "Certificate file not found: $CerFile" }
|
||||
$issued = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $CerFile
|
||||
if ($PSCmdlet.ShouldProcess($CerFile, 'certreq -accept')) {
|
||||
& certreq -accept -q $CerFile | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) { throw "certreq -accept failed ($LASTEXITCODE). The request that made this certificate must exist on this host." }
|
||||
}
|
||||
$cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Thumbprint -eq $issued.Thumbprint }
|
||||
if (-not $cert -and -not $WhatIfPreference) { throw "Issued certificate $($issued.Thumbprint) not found in the machine store after accept." }
|
||||
}
|
||||
else {
|
||||
# --- 2. INF ---------------------------------------------------------------------------------
|
||||
$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"
|
||||
$infText = @"
|
||||
[Version]
|
||||
Signature = "`$Windows NT`$"
|
||||
|
||||
@@ -86,8 +210,11 @@ Subject = "CN=$primary"
|
||||
KeyLength = 2048
|
||||
KeyAlgorithm = RSA
|
||||
HashAlgorithm = SHA256
|
||||
ProviderName = "Microsoft Software Key Storage Provider"
|
||||
KeySpec = 0
|
||||
; SQL Server needs a legacy CryptoAPI provider and an exchange key (KeySpec = AT_KEYEXCHANGE).
|
||||
ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
|
||||
ProviderType = 12
|
||||
KeySpec = 1
|
||||
; digital signature + key encipherment
|
||||
KeyUsage = 0xA0
|
||||
Exportable = TRUE
|
||||
MachineKeySet = TRUE
|
||||
@@ -103,79 +230,104 @@ $sanLines
|
||||
|
||||
[RequestAttributes]
|
||||
CertificateTemplate = $Template
|
||||
"@ | Set-Content -Path $inf -Encoding ASCII
|
||||
"@
|
||||
Write-Host "INF: $inf"
|
||||
if ($WhatIfPreference) {
|
||||
Write-Host $infText
|
||||
Write-Host "WhatIf: would run certreq -new, -submit to $CertificateAuthority (template $Template) and -accept."
|
||||
}
|
||||
else {
|
||||
$infText | Set-Content -Path $inf -Encoding ASCII
|
||||
# --- 3. request, submit, accept -------------------------------------------------------
|
||||
& certreq -new -q $inf $csr | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) { throw "certreq -new failed ($LASTEXITCODE)" }
|
||||
$submit = & certreq -submit -q -config $CertificateAuthority $csr $cer 2>&1
|
||||
$submit | Out-Host
|
||||
$reqId = ($submit | Select-String -Pattern 'RequestId:\s*"?(\d+)' | ForEach-Object { $_.Matches[0].Groups[1].Value } | Select-Object -First 1)
|
||||
$state = Read-State
|
||||
$state | Add-Member -NotePropertyName request -NotePropertyValue ([pscustomobject]@{ id = $reqId; ca = $CertificateAuthority; cer = $cer; inf = $inf; at = (Get-Date).ToString('s') }) -Force
|
||||
Write-State $state
|
||||
if ($LASTEXITCODE -ne 0 -or -not (Test-Path $cer)) {
|
||||
throw "The request was not issued (certreq exit $LASTEXITCODE, request id $reqId). If it is pending, have the CA admin issue it, then on this host run: certreq -retrieve -config `"$CertificateAuthority`" $reqId `"$cer`" and rerun this script with -CerFile `"$cer`"$(if ($Instances) { " -Instances $($Instances -join ',')" })."
|
||||
}
|
||||
& 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." }
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "INF written: $inf"
|
||||
if ($WhatIfPreference -and -not $cert) {
|
||||
Write-Host "WhatIf: would validate the certificate, grant private key read to $(($targets.Account | Select-Object -Unique) -join ', '), bind it to $(($targets.Name) -join ', ') and $(if ($RestartInstances) { 'restart' } else { 'print restart commands for' }) $(($targets.Service) -join ', ')."
|
||||
return
|
||||
}
|
||||
|
||||
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." }
|
||||
# --- 4. validate ------------------------------------------------------------------------------
|
||||
$thumb = $cert.Thumbprint.ToLower()
|
||||
$check = Test-ServerCertificate $cert $names
|
||||
Write-Host "Certificate $($cert.Subject) thumbprint $thumb, $($check.KeySize)-bit, $($check.Signature), expires $($cert.NotAfter.ToString('yyyy-MM-dd'))"
|
||||
if ($check.Problems.Count) {
|
||||
throw "The certificate is not usable by SQL Server:`n - $($check.Problems -join "`n - ")`nNothing was bound. Check the template ($Template) on the CA; it must issue through a legacy CSP with an exchange key."
|
||||
}
|
||||
|
||||
$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))" }
|
||||
# --- 5. private key access, per selected instance account ------------------------------------
|
||||
$keyName = $check.Csp.UniqueKeyContainerName
|
||||
$keyFile = Join-Path $env:ProgramData "Microsoft\Crypto\RSA\MachineKeys\$keyName"
|
||||
if (-not (Test-Path $keyFile)) { throw "Private key container file not found: $keyFile" }
|
||||
foreach ($acct in ($targets.Account | Select-Object -Unique)) {
|
||||
if ($PSCmdlet.ShouldProcess($keyFile, "grant read to $acct")) {
|
||||
$out = & icacls $keyFile /grant "${acct}:(R)" 2>&1
|
||||
if ($LASTEXITCODE -ne 0) { throw "Could not grant $acct read on the private key ($out). Nothing was bound." }
|
||||
Write-Host "Private key read granted to $acct"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 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 ', ')" }
|
||||
# --- 6. bind, with saved previous values ---------------------------------------------------------
|
||||
$state = Read-State
|
||||
$previous = [ordered]@{}
|
||||
foreach ($i in $targets) {
|
||||
$cur = (Get-ItemProperty $i.RegKey -Name Certificate -ErrorAction SilentlyContinue).Certificate
|
||||
$previous[$i.Name] = [pscustomobject]@{ regKey = $i.RegKey; value = $cur }
|
||||
}
|
||||
$state | Add-Member -NotePropertyName previous -NotePropertyValue ([pscustomobject]$previous) -Force
|
||||
$state | Add-Member -NotePropertyName bound -NotePropertyValue ([pscustomobject]@{ thumbprint = $thumb; at = (Get-Date).ToString('s'); instances = @($targets.Name) }) -Force
|
||||
Write-State $state
|
||||
Write-Host "Previous bindings saved to $stateFile (rollback: .\Request-VsqlServerCertificate.ps1 -Rollback)"
|
||||
|
||||
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)"
|
||||
$done = @()
|
||||
try {
|
||||
foreach ($i in $targets) {
|
||||
if ($PSCmdlet.ShouldProcess("instance $($i.Name)", "set Certificate=$thumb")) {
|
||||
Set-ItemProperty -Path $i.RegKey -Name Certificate -Value $thumb
|
||||
$done += $i
|
||||
$force = (Get-ItemProperty $i.RegKey -Name ForceEncryption -ErrorAction SilentlyContinue).ForceEncryption
|
||||
Write-Host "Instance $($i.Name): Certificate bound (ForceEncryption=$force, unchanged)"
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
foreach ($i in $done) {
|
||||
$old = $previous[$i.Name].value
|
||||
if ([string]::IsNullOrEmpty($old)) { Remove-ItemProperty -Path $i.RegKey -Name Certificate -ErrorAction SilentlyContinue } else { Set-ItemProperty -Path $i.RegKey -Name Certificate -Value $old }
|
||||
Write-Warning "Restored $($i.Name) to its previous binding."
|
||||
}
|
||||
throw
|
||||
}
|
||||
|
||||
# --- 5. restart ------------------------------------------------------------------------------
|
||||
$toRestart = $all | ForEach-Object { if ($_.Name -eq 'MSSQLSERVER') { 'MSSQLSERVER' } else { "MSSQL`$$($_.Name)" } }
|
||||
# --- 7. restart ------------------------------------------------------------------------------
|
||||
if ($RestartInstances) {
|
||||
foreach ($s in $toRestart) {
|
||||
if ($PSCmdlet.ShouldProcess($s, 'restart service')) { Restart-Service -Name $s -Force; Write-Host "Restarted $s" }
|
||||
foreach ($i in $targets) {
|
||||
if ($PSCmdlet.ShouldProcess($i.Service, 'restart service')) { Restart-Service -Name $i.Service -Force; Write-Host "Restarted $($i.Service)" }
|
||||
}
|
||||
} else {
|
||||
}
|
||||
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" }
|
||||
$targets | ForEach-Object { Write-Host " Restart-Service -Name '$($_.Service)' -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 "Done. Thumbprint $thumb. After the restart the SQL Server error log should read 'The certificate [Cn = $primary] was successfully loaded for encryption'."
|
||||
Write-Host 'If an instance fails to start, run this script with -Rollback and restart it, then check the private key access and the provider.'
|
||||
Write-Host 'Then tell Packet Station: the stacks switch to VISUAL_ENCRYPT=yes (Gitea issue 5).'
|
||||
|
||||
Reference in New Issue
Block a user