# Set-ShopdbCollectorKey.ps1 # # Delivers the ShopDB collector credential to every shopfloor PC by writing # HKLM:\SOFTWARE\GE\ShopDB (BaseUrl + CollectorKey), the same contract the # display kiosks already use. Anything that posts to /api/collector/* reads it # from there - today Backup-NtlarsSettings.ps1, tomorrow whatever else reports. # # WHY A KEY IS NEEDED AT ALL: # The GE-Enforce manifest/payload FETCH honours an IP allowlist, so a bay on a # trusted subnet pulls config with no token. Collector INGEST does not: it # accepts only a collector-scoped token. That asymmetry is deliberate - fetch # reads config we already publish, ingest WRITES asset data, and an allowlist # alone would let anything on the subnet post revisions. # # WHERE THE SECRET LIVES: # NOT in manifest.json, and not in this script. It is read from a sibling file # on the share (see $KEYCONFIG below), so: # - the manifest stays free of secrets and safe to read/diff # - rotating the token is replacing ONE file, not editing a manifest # The file inherits the share's ACL, which grants file-level reads only to the # SFLD user. Treat it as a secret: scope the token to collector.ingest ONLY, # so a leak cannot read or mutate anything else. # # IDEMPOTENT: compares current registry values against desired and writes only # on a difference, so DetectionMethod=Always costs one registry read per cycle # after the first run. Rotating the key on the share re-converges the fleet on # the next cycle with no other action. # # Always exits 0 so the GE-Enforce "last run result" stays clean. param( # Override for a one-off run; normally read from the sibling config file. [string]$CollectorKey, [string]$BaseUrl ) $ErrorActionPreference = 'Continue' $SHOPDBREG = 'HKLM:\SOFTWARE\GE\ShopDB' $KEYCONFIG = Join-Path $PSScriptRoot '..\configs\shopdb-collector.txt' $LOGDIR = 'C:\Logs\Shopfloor' $LOGFILE = Join-Path $LOGDIR 'shopdb-collector-key.log' if (-not (Test-Path $LOGDIR)) { New-Item -ItemType Directory -Path $LOGDIR -Force -EA SilentlyContinue | Out-Null } function Log { param([string]$Message) $line = '[{0}] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message Add-Content -Path $LOGFILE -Value $line -EA SilentlyContinue Write-Host $line } function Read-KeyConfig { <# LABELLED lines only: baseurl=https://host/shopdb collector= A bare line is ignored rather than guessed at - a fetch token landing in the collector slot would leave reporting broken while looking configured. #> $result = @{ BaseUrl = ''; CollectorKey = '' } if (-not (Test-Path $KEYCONFIG)) { return $result } foreach ($line in (Get-Content $KEYCONFIG -EA SilentlyContinue)) { $t = $line.Trim() if (-not $t -or $t.StartsWith('#')) { continue } if ($t -match '^(?i)collector\s*=\s*(.+)$') { $result.CollectorKey = $Matches[1].Trim() } elseif ($t -match '^(?i)baseurl\s*=\s*(.+)$') { $result.BaseUrl = $Matches[1].Trim().TrimEnd('/') } else { Log " ignoring unlabelled line in $(Split-Path $KEYCONFIG -Leaf)" } } return $result } Log '=== Set-ShopdbCollectorKey start ===' $cfg = Read-KeyConfig if (-not $CollectorKey) { $CollectorKey = $cfg.CollectorKey } if (-not $BaseUrl) { $BaseUrl = $cfg.BaseUrl } if (-not $CollectorKey) { # Not an error: an unconfigured share is the normal state before a site # issues its token. Say exactly what to do rather than failing silently. Log "No collector key configured. Put a 'collector=' line in:" Log " $KEYCONFIG" Log "Nothing written." exit 0 } # --- compare before writing ------------------------------------------------ $current = $null try { $current = Get-ItemProperty -Path $SHOPDBREG -EA Stop } catch { } $needKey = (-not $current) -or ($current.CollectorKey -ne $CollectorKey) $needUrl = $BaseUrl -and ((-not $current) -or ($current.BaseUrl -ne $BaseUrl)) if (-not $needKey -and -not $needUrl) { Log 'Registry already matches - nothing to do.' Log '=== Set-ShopdbCollectorKey end ===' exit 0 } try { if (-not (Test-Path $SHOPDBREG)) { New-Item -Path $SHOPDBREG -Force | Out-Null } if ($needUrl) { New-ItemProperty -Path $SHOPDBREG -Name BaseUrl -Value $BaseUrl ` -PropertyType String -Force -EA Stop | Out-Null Log "Set BaseUrl = $BaseUrl" } if ($needKey) { New-ItemProperty -Path $SHOPDBREG -Name CollectorKey -Value $CollectorKey ` -PropertyType String -Force -EA Stop | Out-Null Log "Set CollectorKey (length $($CollectorKey.Length)) - value not logged" } # Lock the key down to SYSTEM + Administrators, matching what # Install-ShopdbKiosk does on displays. Without this the value is readable # by any interactive user, and a shopfloor PC is a shared login. try { $acl = Get-Acl $SHOPDBREG $acl.SetAccessRuleProtection($true, $false) foreach ($who in 'SYSTEM', 'Administrators') { $acl.AddAccessRule((New-Object Security.AccessControl.RegistryAccessRule( $who, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))) } Set-Acl -Path $SHOPDBREG -AclObject $acl -EA Stop Log 'ACL set: SYSTEM + Administrators only.' } catch { Log "WARNING - could not tighten the ACL: $_" } } catch { Log "FAILED to write ${SHOPDBREG}: $_" } Log '=== Set-ShopdbCollectorKey end ===' exit 0