Always pin the server, let the registry own pins, and survive a revoke mid-upgrade
Found in the Windows VM re-test. - A CA-flagged server certificate (common with a plain openssl self-signed cert) produced an installer package with no pin at all, so agent 1.20 refused the server. The package now always carries the SPKI pin of the certificate PCs receive; the CA flag only skips the trusted-root import. When an uploaded CA-flagged certificate differs from the server's own, both pins are included. The build response carries X-ShopDB-Build-Warning and the installer logs what older agents need. - Pins written into appsettings.json by the installer were merged with the registry, so removing a pin from HKLM never took effect. When the ServerPins value exists in the registry it is now authoritative, even when empty; appsettings pins apply only when it is absent. Set-ShopDBServerPins.ps1 -Clear deletes the value; new -None writes an empty value (no pins). - A script or package download sent with a credential revoked moments earlier got 401 CREDENTIAL_INVALID and failed the command. The agent now retries that download once with the shared key and records the fallback in the result.
This commit is contained in:
@@ -19,17 +19,27 @@
|
||||
requested set first, and nothing is written when they already match, so it
|
||||
is safe every cycle.
|
||||
|
||||
THE REGISTRY WINS. When ServerPins exists in either view, even empty, the
|
||||
agent uses it alone and ignores serverpins / servercertthumbprint in
|
||||
appsettings.json (the installer and store upgrade write those). Only when
|
||||
the value is absent in both views do the appsettings pins apply.
|
||||
|
||||
ROTATING THE SERVER KEY: pass the old pin AND the new pin until every PC has
|
||||
both, switch the server certificate, then drop the old pin. A PC holding
|
||||
only the old pin refuses the new certificate.
|
||||
both, switch the server certificate, then write ONLY the new pin to retire
|
||||
the old one. A PC holding only the old pin refuses the new certificate.
|
||||
|
||||
.PARAMETER Pins
|
||||
One or more pins separated by commas, semicolons or spaces. Colons and case
|
||||
are ignored. Every value must be 64 hex characters or nothing is written.
|
||||
|
||||
.PARAMETER Clear
|
||||
Remove ServerPins from both views. Pinning is then off and the normal
|
||||
Windows certificate checks apply.
|
||||
Delete ServerPins from both views. The agent then falls back to the pins
|
||||
in appsettings.json; with none there, normal Windows certificate checks.
|
||||
|
||||
.PARAMETER None
|
||||
Write an EMPTY ServerPins in both views. The registry stays authoritative
|
||||
with no pins, so appsettings.json pins are ignored and the normal Windows
|
||||
certificate checks apply.
|
||||
|
||||
.PARAMETER LogPath
|
||||
Append a transcript here as well as writing to the pipeline.
|
||||
@@ -42,6 +52,14 @@
|
||||
.\Set-ShopDBServerPins.ps1 -Pins "<oldpin>,<newpin>" -WhatIf
|
||||
Show what a key rotation would write without touching the registry.
|
||||
|
||||
.EXAMPLE
|
||||
.\Set-ShopDBServerPins.ps1 -Pins <newpin>
|
||||
After the server switched certificates: retire the old pin.
|
||||
|
||||
.EXAMPLE
|
||||
.\Set-ShopDBServerPins.ps1 -None
|
||||
Turn pinning off on a PC whose appsettings.json still carries pins.
|
||||
|
||||
.NOTES
|
||||
Exit codes:
|
||||
0 written, or already correct
|
||||
@@ -51,6 +69,7 @@
|
||||
param(
|
||||
[string]$Pins,
|
||||
[switch]$Clear,
|
||||
[switch]$None,
|
||||
[string]$LogPath
|
||||
)
|
||||
|
||||
@@ -69,16 +88,20 @@ function Log {
|
||||
}
|
||||
}
|
||||
|
||||
# Returns @{ Present; Pins }. Present = the value exists, even empty.
|
||||
function Get-CurrentPins {
|
||||
param([Microsoft.Win32.RegistryView]$View)
|
||||
$result = @{ Present = $false; Pins = @() }
|
||||
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $View)
|
||||
try {
|
||||
$key = $base.OpenSubKey($SubKey)
|
||||
if (-not $key) { return @() }
|
||||
if (-not $key) { return $result }
|
||||
try {
|
||||
$value = $key.GetValue($ValueName)
|
||||
if ($null -eq $value) { return @() }
|
||||
return @(@($value) | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ })
|
||||
if ($null -eq $value) { return $result }
|
||||
$result.Present = $true
|
||||
$result.Pins = @(@($value) | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ })
|
||||
return $result
|
||||
} finally { $key.Dispose() }
|
||||
} finally { $base.Dispose() }
|
||||
}
|
||||
@@ -97,9 +120,15 @@ function Test-SamePins {
|
||||
# --- 1. validate the request ------------------------------------------------
|
||||
|
||||
$requested = @()
|
||||
if (-not $Clear) {
|
||||
if (@(@($Clear.IsPresent, $None.IsPresent, [bool]$Pins) | Where-Object { $_ }).Count -gt 1) {
|
||||
Log 'ERROR use only one of -Pins, -Clear, -None'
|
||||
exit 1
|
||||
}
|
||||
if ($None) {
|
||||
Log 'requested an empty ServerPins (registry authoritative, no pins)'
|
||||
} elseif (-not $Clear) {
|
||||
if (-not $Pins -or -not $Pins.Trim()) {
|
||||
Log 'ERROR no -Pins given (use -Clear to remove pinning)'
|
||||
Log 'ERROR no -Pins given (-Clear deletes the value, -None writes it empty)'
|
||||
exit 1
|
||||
}
|
||||
foreach ($raw in ($Pins -split '[,;\s]+')) {
|
||||
@@ -128,10 +157,11 @@ $views = @(
|
||||
$failed = $false
|
||||
foreach ($entry in $views) {
|
||||
try {
|
||||
$current = @(Get-CurrentPins -View $entry.View)
|
||||
$state = Get-CurrentPins -View $entry.View
|
||||
$current = @($state.Pins)
|
||||
|
||||
if ($Clear) {
|
||||
if ($current.Count -eq 0) {
|
||||
if (-not $state.Present) {
|
||||
Log "$($entry.Name): no ServerPins; nothing to do"
|
||||
continue
|
||||
}
|
||||
@@ -143,12 +173,12 @@ foreach ($entry in $views) {
|
||||
try { $key.DeleteValue($ValueName, $false) } finally { $key.Dispose() }
|
||||
}
|
||||
} finally { $base.Dispose() }
|
||||
Log "$($entry.Name): removed ServerPins"
|
||||
Log "$($entry.Name): deleted ServerPins (appsettings.json pins apply)"
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (Test-SamePins -Left $current -Right $requested) {
|
||||
if ($state.Present -and (Test-SamePins -Left $current -Right $requested)) {
|
||||
Log "$($entry.Name): ServerPins already correct; nothing to do"
|
||||
continue
|
||||
}
|
||||
@@ -164,8 +194,8 @@ foreach ($entry in $views) {
|
||||
} finally { $base.Dispose() }
|
||||
|
||||
# Read back rather than trust SetValue.
|
||||
$after = @(Get-CurrentPins -View $entry.View)
|
||||
if (-not (Test-SamePins -Left $after -Right $requested)) {
|
||||
$after = Get-CurrentPins -View $entry.View
|
||||
if (-not $after.Present -or -not (Test-SamePins -Left @($after.Pins) -Right $requested)) {
|
||||
Log "ERROR $($entry.Name): ServerPins did not read back as written"
|
||||
$failed = $true
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user