From 27045d5e4aa18c35ab3ab77f09515aa45ce33630 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Thu, 21 May 2026 19:38:59 -0400 Subject: [PATCH] gea-shopfloor-collections: controller NIC auto-IP + credential break-glass 04-SetControllerNicIP.ps1 (imaging-time, runs once via Run-ShopfloorSetup): - Finds the Realtek physical Ethernet adapter (controller NIC on every collections bay; corp LAN is Intel) - Skips any candidate with a DHCP default gateway (that one is the corp LAN, not the controller) - Skips any candidate already on 192.168.1.2 - Sets static 192.168.1.2/24, no gateway, clears DNS - matches the manual procedure documented in post-deploy-debug-flowchart.md section 2B - Refuses to guess when multiple Realtek NICs remain ambiguous - Imaging-time only, not enforced via GE-Enforce so the tech can override on a specific bay if needed without the drift-catcher reverting Set-ControllerCredential.ps1 + manifest-entry-controller-credential.json: - Break-glass cmdkey /add for the controller SMB share (\\192.168.1.1\md1 used by DNC). Scoped to the 12 Okuma LOC650 machine numbers (3201-3212). - Manifest entry is detection-less so it runs every enforce cycle if the script is armed (.ps1 extension); disarmed by default (.ps1.bak on the share) so a coach can rename when a bay loses its credential without the enforcer overwriting per-bay deviations between events. - Smoke-tested end-to-end on win11 VM via QGA: SYSTEM context cmdkey /add succeeds, cmdkey /list shows the entry. DNC service runs as LocalSystem so SYSTEM vault is the right target. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../04-SetControllerNicIP.ps1 | 142 ++++++++++++++++++ .../Set-ControllerCredential.ps1 | 82 ++++++++++ .../manifest-entry-controller-credential.json | 8 + 3 files changed, 232 insertions(+) create mode 100644 playbook/shopfloor-setup/gea-shopfloor-collections/04-SetControllerNicIP.ps1 create mode 100644 playbook/shopfloor-setup/gea-shopfloor-collections/Set-ControllerCredential.ps1 create mode 100644 playbook/shopfloor-setup/gea-shopfloor-collections/manifest-entry-controller-credential.json diff --git a/playbook/shopfloor-setup/gea-shopfloor-collections/04-SetControllerNicIP.ps1 b/playbook/shopfloor-setup/gea-shopfloor-collections/04-SetControllerNicIP.ps1 new file mode 100644 index 0000000..a807465 --- /dev/null +++ b/playbook/shopfloor-setup/gea-shopfloor-collections/04-SetControllerNicIP.ps1 @@ -0,0 +1,142 @@ +# 04-SetControllerNicIP.ps1 - Auto-configure the controller-facing Realtek NIC +# at imaging time on gea-shopfloor-collections bays. +# +# Standard config (per post-deploy-debug-flowchart.md, section 2B): +# IP: 192.168.1.2 +# Mask: 255.255.255.0 (/24) +# Gateway: (blank) +# DNS: (none - cleared) +# +# Logic: +# 1. Find physical Ethernet adapters whose vendor description contains +# 'Realtek'. (Shopfloor PCs ship with a Realtek PCIe GbE add-in card +# for the controller; the corp LAN NIC is typically Intel.) +# 2. Skip any candidate that already has a DHCP-assigned default gateway +# (that's the corp LAN NIC, not the controller). +# 3. Skip any candidate that already has 192.168.1.2 - already configured. +# 4. For the single remaining candidate, set manual IP / mask, clear DNS. +# 5. If multiple Realtek NICs remain after filtering, log + bail (manual +# investigation needed; the script will NOT guess which is controller). +# +# Imaging-time only - run once during Run-ShopfloorSetup for collections +# bays. Not registered as a GE-Enforce drift-catcher because once set, +# manual changes by the tech are intentional and should not be reverted. +# +# Log: C:\Logs\Shopfloor\controller-nic.log + +$ErrorActionPreference = 'Continue' + +$logDir = 'C:\Logs\Shopfloor' +if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } +$logFile = Join-Path $logDir 'controller-nic.log' + +function Write-NicLog { + param([string]$Message, [string]$Level = 'INFO') + $line = '[{0}] [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message + Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue + Write-Host $line +} + +$targetIP = '192.168.1.2' +$prefixLen = 24 + +Write-NicLog "=== 04-SetControllerNicIP start ===" + +# Enumerate physical Ethernet adapters with Realtek in the vendor description. +$candidates = Get-NetAdapter -Physical -ErrorAction SilentlyContinue | + Where-Object { $_.InterfaceDescription -match 'Realtek' -and $_.MediaType -eq '802.3' } + +if (-not $candidates) { + Write-NicLog "No Realtek physical Ethernet adapters found - nothing to configure (PC may have no controller NIC)." + exit 0 +} + +Write-NicLog "Found $($candidates.Count) Realtek adapter(s):" +foreach ($c in $candidates) { + Write-NicLog " $($c.Name) ($($c.InterfaceDescription)) [status=$($c.Status)]" +} + +# Filter out anything that already looks like the corp LAN NIC (has a +# DHCP-assigned default gateway -- corp DHCP servers always hand one out). +$filtered = @() +foreach ($c in $candidates) { + $ipCfg = Get-NetIPConfiguration -InterfaceIndex $c.ifIndex -ErrorAction SilentlyContinue + $gw = $null + if ($ipCfg -and $ipCfg.IPv4DefaultGateway) { + $gw = ($ipCfg.IPv4DefaultGateway | Select-Object -First 1).NextHop + } + if ($gw) { + Write-NicLog " Skipping $($c.Name) - has IPv4 default gateway $gw (likely corp LAN, not controller)" + continue + } + + # Skip if it already has 192.168.1.2 - already configured. + $existing = Get-NetIPAddress -InterfaceIndex $c.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | + Where-Object { $_.IPAddress -eq $targetIP } + if ($existing) { + Write-NicLog " Skipping $($c.Name) - already has $targetIP/$prefixLen (configured previously)" + continue + } + + $filtered += $c +} + +if ($filtered.Count -eq 0) { + Write-NicLog "After filtering, no candidate remains - either already configured or all Realtek adapters look like corp LAN. Nothing to do." + exit 0 +} +if ($filtered.Count -gt 1) { + Write-NicLog "Multiple unconfigured Realtek adapters found ($($filtered.Count)). Refusing to guess which is the controller NIC. Manual configuration required." 'WARN' + foreach ($f in $filtered) { Write-NicLog " Ambiguous: $($f.Name) ($($f.InterfaceDescription))" 'WARN' } + exit 0 +} + +$target = $filtered[0] +Write-NicLog "Selected controller NIC: $($target.Name) ($($target.InterfaceDescription)) [ifIndex=$($target.ifIndex)]" + +# Wipe any existing IPv4 addresses + gateways on this interface so the new +# static doesn't conflict with stale DHCP leases or APIPA addresses. +try { + Get-NetIPAddress -InterfaceIndex $target.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | + Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue + Get-NetRoute -InterfaceIndex $target.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | + Where-Object { $_.DestinationPrefix -eq '0.0.0.0/0' } | + Remove-NetRoute -Confirm:$false -ErrorAction SilentlyContinue + Write-NicLog "Cleared existing IPv4 addresses + default route on $($target.Name)" +} catch { + Write-NicLog "Failed to clear existing IPv4 state on $($target.Name): $_" 'WARN' +} + +# Switch the interface to manual IP assignment + set the static. +try { + Set-NetIPInterface -InterfaceIndex $target.ifIndex -Dhcp Disabled -ErrorAction Stop + New-NetIPAddress -InterfaceIndex $target.ifIndex -IPAddress $targetIP ` + -PrefixLength $prefixLen -ErrorAction Stop | Out-Null + Write-NicLog "Set $targetIP/$prefixLen on $($target.Name) (no gateway)" +} catch { + Write-NicLog "Failed to set static IP on $($target.Name): $_" 'ERROR' + exit 1 +} + +# Clear DNS so the corp resolver doesn't get queried for controller-side hostnames. +try { + Set-DnsClientServerAddress -InterfaceIndex $target.ifIndex -ResetServerAddresses -ErrorAction Stop + Write-NicLog "Cleared DNS servers on $($target.Name)" +} catch { + Write-NicLog "Failed to clear DNS on $($target.Name): $_" 'WARN' +} + +# Verify what landed. +try { + $finalCfg = Get-NetIPConfiguration -InterfaceIndex $target.ifIndex -ErrorAction SilentlyContinue + $finalIPs = ($finalCfg.IPv4Address | ForEach-Object { "$($_.IPAddress)/$($_.PrefixLength)" }) -join ', ' + $finalGw = if ($finalCfg.IPv4DefaultGateway) { ($finalCfg.IPv4DefaultGateway | Select-Object -First 1).NextHop } else { '(none)' } + $finalDns = ($finalCfg.DNSServer | Where-Object AddressFamily -eq 2 | Select-Object -ExpandProperty ServerAddresses) -join ', ' + if (-not $finalDns) { $finalDns = '(none)' } + Write-NicLog "Verify: $($target.Name) IPs=$finalIPs Gateway=$finalGw DNS=$finalDns" +} catch { + Write-NicLog "Verification readback failed: $_" 'WARN' +} + +Write-NicLog "=== 04-SetControllerNicIP end ===" +exit 0 diff --git a/playbook/shopfloor-setup/gea-shopfloor-collections/Set-ControllerCredential.ps1 b/playbook/shopfloor-setup/gea-shopfloor-collections/Set-ControllerCredential.ps1 new file mode 100644 index 0000000..20ec66d --- /dev/null +++ b/playbook/shopfloor-setup/gea-shopfloor-collections/Set-ControllerCredential.ps1 @@ -0,0 +1,82 @@ +# Set-ControllerCredential.ps1 - Self-heal a Windows Credential Manager +# entry for the controller's NIC subnet (default 192.168.1.1). Re-applied +# every GE-Enforce cycle so the entry survives any Defender / policy +# blowaway between cycles. +# +# Why: collections bays periodically lose the controller credential from +# Credential Manager (cause TBD - suspect Defender / Intune scrub). Without +# the cred, controller-side connections need interactive prompts. Restoring +# it on each enforce cycle keeps the bay working unattended. +# +# Wiring: pointed to by gea-shopfloor-collections\manifest.json on the SFLD +# share. GE-Enforce calls this directly via the Install-FromManifest PS1 +# action type. Idempotent - cmdkey /add overwrites silently if the entry +# already exists. +# +# Edit the three values below to set the actual credential, then push the +# manifest entry to the SFLD share (see README in same dir). +# +# Log: C:\Logs\Shopfloor\controller-credential.log + +$ErrorActionPreference = 'Continue' + +# --- EDIT THESE THREE VALUES --- +$Target = '192.168.1.1' # Controller IP or hostname +$Username = 'CHANGEME' # Controller-side username +$Password = 'CHANGEME' # Controller-side password +# --- END EDIT --- + +$logDir = 'C:\Logs\Shopfloor' +if (-not (Test-Path $logDir)) { + try { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } catch { $logDir = $env:TEMP } +} +$logFile = Join-Path $logDir 'controller-credential.log' + +function Write-CredLog { + param([string]$Message, [string]$Level = 'INFO') + $line = '[{0}] [{1}] [{2}] {3}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $env:USERNAME, $Message + Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue + Write-Host $line +} + +Write-CredLog "=== Set-ControllerCredential start (target=$Target) ===" + +if ($Username -eq 'CHANGEME' -or $Password -eq 'CHANGEME') { + Write-CredLog 'Credential placeholders not filled in - refusing to write CHANGEME values' 'ERROR' + exit 1 +} + +# Check current state. cmdkey /list output for a missing target returns +# 'Currently stored credentials: ... NONE' or omits the target. +$listOut = & cmdkey /list:$Target 2>&1 | Out-String +if ($listOut -match [regex]::Escape("Target: $Target")) { + Write-CredLog "Entry for $Target already present in Credential Manager - re-applying anyway (cmdkey /add updates idempotently)" +} else { + Write-CredLog "No entry for $Target found in Credential Manager - adding" +} + +# cmdkey /add expects /generic for non-domain creds (SMB / RDP / SQL). +# Use /add: for plain network credential (matches the older 'net use' +# style cred used by NET shares + most non-AD apps). Switch to /generic: +# if the target is HTTP / RDP TERMSRV-specific. +& cmdkey /add:$Target /user:$Username /pass:$Password 2>&1 | ForEach-Object { + Write-CredLog " cmdkey: $_" +} +$rc = $LASTEXITCODE +if ($rc -eq 0) { + Write-CredLog "cmdkey /add succeeded (target=$Target user=$Username)" +} else { + Write-CredLog "cmdkey /add failed (exit $rc)" 'ERROR' + exit $rc +} + +# Quick verify +$verify = & cmdkey /list:$Target 2>&1 | Out-String +if ($verify -match [regex]::Escape("Target: $Target")) { + Write-CredLog 'Verification passed - entry present after add' +} else { + Write-CredLog 'Verification FAILED - entry not visible after add' 'WARN' +} + +Write-CredLog '=== Set-ControllerCredential end ===' +exit 0 diff --git a/playbook/shopfloor-setup/gea-shopfloor-collections/manifest-entry-controller-credential.json b/playbook/shopfloor-setup/gea-shopfloor-collections/manifest-entry-controller-credential.json new file mode 100644 index 0000000..d594806 --- /dev/null +++ b/playbook/shopfloor-setup/gea-shopfloor-collections/manifest-entry-controller-credential.json @@ -0,0 +1,8 @@ +{ + "_comment": "Drop this entry into the SFLD share at \\tsgwp00525\\sfld$\\v2\\shared\\dt\\shopfloor\\gea-shopfloor-collections\\manifest.json (Applications array). Edit Set-ControllerCredential.ps1 first to fill in the real $Username + $Password before pushing the script + manifest. Detection is omitted on purpose: Install-FromManifest treats missing DetectionMethod as 'not installed' and runs the script every enforce cycle, which is exactly what we want (Defender / Intune occasionally scrubs the entry; we re-apply on each cycle). cmdkey /add is idempotent so re-running is harmless. TargetMachineNumbers is optional; remove it to apply to every collections bay, or list specific machine numbers to scope down.", + "Name": "Controller credential for Okuma LOC650 bays (192.168.1.1)", + "PCTypes": ["gea-shopfloor-collections"], + "TargetMachineNumbers": ["3201", "3202", "3203", "3204", "3205", "3206", "3207", "3208", "3209", "3210", "3211", "3212"], + "Script": "apps/Set-ControllerCredential.ps1", + "Type": "PS1" +}