Skip to content

Instantly share code, notes, and snippets.

@fidelix
Created August 7, 2026 21:00
Show Gist options
  • Select an option

  • Save fidelix/417deb417a3a0645816711f9a6fad37b to your computer and use it in GitHub Desktop.

Select an option

Save fidelix/417deb417a3a0645816711f9a6fad37b to your computer and use it in GitHub Desktop.
Enable-AnsibleWinRM.ps1 - Solidseg atendente bootstrap
# Enables the minimum remote-management baseline needed for Ansible to manage
# a standalone Windows IoT Enterprise LTSC workstation on a trusted LAN.
#
# Authentication: local Windows administrator + NTLM.
# Transport: WinRM HTTP (TCP 5985), with WinRM message encryption enabled by
# Ansible. Firewall allows TCP 5985 from any remote address on every profile.
#
# Ethernet must be Private: stock WinRM and RDP firewall rules exclude Public.
# With Public, RDP looks like "wrong password" / refused even with good creds.
# This script pins Unidentified Networks policy to Private, rewrites
# NetworkList Category for wired/Public profiles, Set-NetConnectionProfile
# when NLA published a profile, and enables RDP (TCP/UDP 3389) for supervisors.
#
# Do not expose TCP 5985 to the Internet or add a router port-forwarding rule.
#
# Fresh IoT images often start WinRM with NO HTTP listener (nothing on :5985).
# Enable-PSRemoting/Set-WSManQuickConfig then fail with 0x80338012 (chicken/egg).
# This script configures the listener offline via registry + URL ACL while the
# service is stopped, then starts/restarts WinRM and verifies the bind.
$ErrorActionPreference = 'Continue'
$ProgressPreference = 'SilentlyContinue'
$script:failed = $false
foreach ($name in @('HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'ALL_PROXY', 'all_proxy')) {
Remove-Item -LiteralPath "Env:\$name" -ErrorAction SilentlyContinue
}
$env:NO_PROXY = '*'
$env:no_proxy = '*'
$script:WinrmUrl = 'http://+:5985/wsman/'
$script:WinrmUrlUser = 'NT SERVICE\WinRM'
$script:WinrmUrlSddl = 'D:(A;;GX;;;S-1-5-80-569256582-2953403351-2909559716-1301513147-412116970)'
function Write-Step {
param([string]$Message)
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Write-Output "[$ts] $Message"
}
function Invoke-Step {
param(
[Parameter(Mandatory = $true)][string]$Name,
[Parameter(Mandatory = $true)][scriptblock]$Action,
[switch]$Required
)
Write-Step $Name
try {
$ErrorActionPreference = 'Stop'
& $Action
Write-Step "OK: $Name"
}
catch {
Write-Step ("FAIL: {0} :: {1}" -f $Name, $_.Exception.Message)
if ($Required) {
$script:failed = $true
}
}
finally {
$ErrorActionPreference = 'Continue'
}
}
function Test-LocalWinrmReady {
try {
$ErrorActionPreference = 'Stop'
Test-WSMan -ComputerName 'localhost' | Out-Null
return $true
}
catch {
return $false
}
finally {
$ErrorActionPreference = 'Continue'
}
}
function Test-Port5985Listening {
$lines = @(& netstat.exe -an | Select-String -Pattern '(:5985)\s+.*LISTENING')
return ($lines.Count -gt 0)
}
function Wait-WinrmListening {
param([int]$Seconds = 60)
$deadline = (Get-Date).AddSeconds($Seconds)
do {
if (Test-Port5985Listening) {
return $true
}
Start-Sleep -Seconds 2
} while ((Get-Date) -lt $deadline)
return $false
}
function Write-WinrmDiagnostics {
Write-Step '--- diagnostics ---'
Write-Step ("WinRM status: {0} start={1}" -f (Get-Service WinRM).Status, (Get-Service WinRM).StartType)
Write-Step 'netstat 5985:'
& netstat.exe -an | Select-String -Pattern '5985' | ForEach-Object { Write-Step $_.Line.Trim() }
Write-Step 'URL ACL:'
& netsh.exe http show urlacl url=$script:WinrmUrl 2>&1 | ForEach-Object { Write-Step (" {0}" -f $_) }
$listener = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Listener\*+HTTP'
if (Test-Path -LiteralPath $listener) {
Write-Step 'Listener registry:'
Get-ItemProperty -LiteralPath $listener | Format-List * | Out-String | ForEach-Object { Write-Step $_.TrimEnd() }
}
else {
Write-Step 'Listener registry: MISSING'
}
$svc = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Service'
if (Test-Path -LiteralPath $svc) {
Write-Step 'Service registry:'
Get-ItemProperty -LiteralPath $svc | Format-List * | Out-String | ForEach-Object { Write-Step $_.TrimEnd() }
}
Write-Step '--- end diagnostics ---'
}
Write-Step 'Ansible WinRM bootstrap starting.'
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
$elevated = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
Write-Step ("User={0} Elevated={1}" -f $identity.Name, $elevated)
if (-not $elevated) {
Write-Step 'ERROR: must run elevated (LOCAL SYSTEM / Administrator).'
exit 1
}
Invoke-Step -Name 'Ensure dedicated WinRM firewall allow rule (Profile=Any)' -Required -Action {
$ruleName = 'Ansible-WinRM-HTTP'
$existing = Get-NetFirewallRule -Name $ruleName -ErrorAction SilentlyContinue
if ($existing) {
Set-NetFirewallRule -Name $ruleName -Enabled True -Profile Any -Direction Inbound -Action Allow | Out-Null
$addr = Get-NetFirewallAddressFilter -AssociatedNetFirewallRule (Get-NetFirewallRule -Name $ruleName)
Set-NetFirewallAddressFilter -InputObject $addr -RemoteAddress Any | Out-Null
}
else {
New-NetFirewallRule `
-Name $ruleName `
-DisplayName 'WinRM - Ansible (Any profile)' `
-Direction Inbound `
-Action Allow `
-Protocol TCP `
-LocalPort 5985 `
-RemoteAddress Any `
-Profile Any | Out-Null
}
}
# Ethernet on a fresh IoT image often lands as Public; stock WinRM rules then
# block remote management. Force Private by policy (new/unidentified nets) and
# by rewriting current NetworkList profiles + live connection profiles.
Invoke-Step -Name 'Default Unidentified Networks policy to Private' -Required -Action {
# Network List Manager Policies → Unidentified Networks → Private
$unidentified = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\NetworkList\Signatures\010103000F0000F0010000000F0000F0C967A3643C3AD267615674178C508D39BD1E91E3B513C00DA47D02513D9459F31'
if (-not (Test-Path -LiteralPath $unidentified)) {
New-Item -Path $unidentified -Force | Out-Null
}
New-ItemProperty -LiteralPath $unidentified -Name 'Category' -PropertyType DWord -Value 1 -Force | Out-Null
New-ItemProperty -LiteralPath $unidentified -Name 'CategoryReadOnly' -PropertyType DWord -Value 0 -Force | Out-Null
$newNetworkWindow = 'HKLM:\SYSTEM\CurrentControlSet\Control\Network\NewNetworkWindowOff'
if (-not (Test-Path -LiteralPath $newNetworkWindow)) {
New-Item -Path $newNetworkWindow -Force | Out-Null
}
}
Invoke-Step -Name 'Force Ethernet / wired NetworkList profiles to Private' -Required -Action {
$profilesRoot = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkList\Profiles'
if (Test-Path -LiteralPath $profilesRoot) {
Get-ChildItem -LiteralPath $profilesRoot | ForEach-Object {
$path = $_.PSPath
$props = Get-ItemProperty -LiteralPath $path
# NameType 6 = Ethernet; also rewrite anything still Public (0).
$nameType = [int]($props.NameType)
$category = [int]($props.Category)
if ($nameType -eq 6 -or $category -eq 0) {
New-ItemProperty -LiteralPath $path -Name 'Category' -PropertyType DWord -Value 1 -Force | Out-Null
Write-Step ("NetworkList {0} (NameType={1}) -> Private" -f $props.ProfileName, $nameType)
}
}
}
}
Invoke-Step -Name 'Set connected Ethernet networks to Private (WinRM-friendly)' -Required -Action {
$deadline = (Get-Date).AddSeconds(60)
$lastError = $null
do {
$profiles = @(Get-NetConnectionProfile -ErrorAction SilentlyContinue)
$targets = @($profiles | Where-Object {
$_.NetworkCategory -ne 'Private' -and $_.NetworkCategory -ne 'DomainAuthenticated'
})
if ($targets.Count -eq 0 -and $profiles.Count -gt 0) {
Write-Step ("All {0} connection profile(s) already Private/Domain" -f $profiles.Count)
return
}
foreach ($profile in $targets) {
try {
Set-NetConnectionProfile -InterfaceIndex $profile.InterfaceIndex -NetworkCategory Private -ErrorAction Stop
Write-Step ("Profile {0} -> Private" -f $profile.InterfaceAlias)
}
catch {
$lastError = $_
Write-Step ("WARN: Set-NetConnectionProfile {0}: {1}" -f $profile.InterfaceAlias, $_.Exception.Message)
}
}
# Prefer Ethernet adapters even if NLA has not published a profile yet.
$ethernet = @(Get-NetAdapter -Physical -ErrorAction SilentlyContinue | Where-Object {
$_.Status -eq 'Up' -and (
$_.MediaType -match '802\.3' -or
$_.InterfaceDescription -match 'Ethernet|Realtek|Intel\(R\).*Ethernet|I2[0-9]{2}' -or
$_.Name -match '^Ethernet'
)
})
foreach ($adapter in $ethernet) {
$live = Get-NetConnectionProfile -InterfaceIndex $adapter.ifIndex -ErrorAction SilentlyContinue
if ($null -eq $live) {
Write-Step ("Ethernet {0} up but no NLA profile yet; policy will classify as Private" -f $adapter.Name)
continue
}
if ($live.NetworkCategory -ne 'Private' -and $live.NetworkCategory -ne 'DomainAuthenticated') {
Set-NetConnectionProfile -InterfaceIndex $adapter.ifIndex -NetworkCategory Private
Write-Step ("Ethernet {0} -> Private" -f $adapter.Name)
}
}
$stillPublic = @(Get-NetConnectionProfile -ErrorAction SilentlyContinue | Where-Object {
$_.NetworkCategory -eq 'Public'
})
if ($stillPublic.Count -eq 0) {
return
}
Start-Sleep -Seconds 2
} while ((Get-Date) -lt $deadline)
# Live NLA can lag behind registry Category=1 + Unidentified=Private policy.
# Fail only when a Public profile still has Category=0 in NetworkList.
$publicNow = @(Get-NetConnectionProfile -ErrorAction SilentlyContinue | Where-Object {
$_.NetworkCategory -eq 'Public'
})
foreach ($profile in $publicNow) {
$regProfiles = @(Get-ChildItem -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkList\Profiles' -ErrorAction SilentlyContinue)
$matched = $false
foreach ($reg in $regProfiles) {
$props = Get-ItemProperty -LiteralPath $reg.PSPath
if ($props.ProfileName -eq $profile.Name -and [int]$props.Category -eq 1) {
$matched = $true
break
}
}
if (-not $matched) {
$detail = if ($lastError) { " Last error: $($lastError.Exception.Message)" } else { '' }
throw ("Ethernet/network still Public and registry Category not Private: {0}.{1}" -f $profile.InterfaceAlias, $detail)
}
Write-Step ("WARN: Get-NetConnectionProfile still Public for {0}; registry Category=Private applied" -f $profile.InterfaceAlias)
}
}
Invoke-Step -Name 'Stop WinRM before writing listener config' -Required -Action {
Stop-Service -Name WinRM -Force -ErrorAction SilentlyContinue
$deadline = (Get-Date).AddSeconds(30)
do {
if ((Get-Service -Name WinRM).Status -eq 'Stopped') { break }
Start-Sleep -Seconds 1
} while ((Get-Date) -lt $deadline)
if ((Get-Service -Name WinRM).Status -ne 'Stopped') {
# Still try to continue; Start later may recover.
Write-Step ("WARN: WinRM status is {0} after stop attempt" -f (Get-Service WinRM).Status)
}
}
Invoke-Step -Name 'Ensure HTTP.sys URL ACL for WinRM' -Required -Action {
$show = & netsh.exe http show urlacl url=$script:WinrmUrl 2>&1 | Out-String
if ($show -match 'Reserved URL') {
Write-Step 'URL ACL already present.'
return
}
$add = & netsh.exe http add urlacl url=$script:WinrmUrl user=$script:WinrmUrlUser 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) {
Write-Step ("user ACL failed, trying SDDL: {0}" -f $add.Trim())
$add = & netsh.exe http add urlacl url=$script:WinrmUrl sddl=$script:WinrmUrlSddl 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) {
throw "netsh http add urlacl failed: $($add.Trim())"
}
}
Write-Step ($add.Trim())
}
Invoke-Step -Name 'Ensure registry HTTP listener + allow_remote_requests' -Required -Action {
$listenerRoot = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Listener'
if (-not (Test-Path -LiteralPath $listenerRoot)) {
New-Item -Path $listenerRoot -Force | Out-Null
}
$listener = Join-Path $listenerRoot '*+HTTP'
# LiteralPath required because of '*' in the key name.
if (-not (Test-Path -LiteralPath $listener)) {
New-Item -Path $listener -Force | Out-Null
}
New-ItemProperty -LiteralPath $listener -Name 'Port' -PropertyType String -Value '5985' -Force | Out-Null
New-ItemProperty -LiteralPath $listener -Name 'uriprefix' -PropertyType String -Value 'wsman' -Force | Out-Null
New-ItemProperty -LiteralPath $listener -Name 'Enabled' -PropertyType DWord -Value 1 -Force | Out-Null
$serviceKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Service'
if (-not (Test-Path -LiteralPath $serviceKey)) {
New-Item -Path $serviceKey -Force | Out-Null
}
New-ItemProperty -LiteralPath $serviceKey -Name 'allow_remote_requests' -PropertyType DWord -Value 1 -Force | Out-Null
# Unblock autoconfig filters if a policy left them empty.
$policy = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service'
if (-not (Test-Path -LiteralPath $policy)) {
New-Item -Path $policy -Force | Out-Null
}
New-ItemProperty -LiteralPath $policy -Name 'AllowAutoConfig' -PropertyType DWord -Value 1 -Force | Out-Null
New-ItemProperty -LiteralPath $policy -Name 'IPv4Filter' -PropertyType String -Value '*' -Force | Out-Null
New-ItemProperty -LiteralPath $policy -Name 'IPv6Filter' -PropertyType String -Value '*' -Force | Out-Null
}
Invoke-Step -Name 'LocalAccountTokenFilterPolicy=1 (remote local admin)' -Required -Action {
New-ItemProperty `
-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' `
-Name 'LocalAccountTokenFilterPolicy' `
-PropertyType DWord `
-Value 1 `
-Force | Out-Null
}
# Stock RemoteDesktop-UserMode-In-* rules are Domain+Private only. Public
# Ethernet drops RDP before auth completes — same symptom as bad password.
Invoke-Step -Name 'Enable Remote Desktop (RDP) for supervision' -Required -Action {
New-ItemProperty `
-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' `
-Name 'fDenyTSConnections' `
-PropertyType DWord `
-Value 0 `
-Force | Out-Null
$tsPolicy = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services'
if (-not (Test-Path -LiteralPath $tsPolicy)) {
New-Item -Path $tsPolicy -Force | Out-Null
}
# Shadow=2: full control without consent (supervisor shadow).
New-ItemProperty -LiteralPath $tsPolicy -Name 'Shadow' -PropertyType DWord -Value 2 -Force | Out-Null
Set-Service -Name TermService -StartupType Automatic -ErrorAction SilentlyContinue
Start-Service -Name TermService -ErrorAction SilentlyContinue
}
Invoke-Step -Name 'Ensure dedicated RDP firewall allow rules (Profile=Any)' -Required -Action {
foreach ($spec in @(
@{ Name = 'Solidseg-RDP-TCP'; Display = 'RDP - Solidseg (TCP Any profile)'; Protocol = 'TCP'; Port = 3389 },
@{ Name = 'Solidseg-RDP-UDP'; Display = 'RDP - Solidseg (UDP Any profile)'; Protocol = 'UDP'; Port = 3389 }
)) {
$existing = Get-NetFirewallRule -Name $spec.Name -ErrorAction SilentlyContinue
if ($existing) {
Set-NetFirewallRule -Name $spec.Name -Enabled True -Profile Any -Direction Inbound -Action Allow | Out-Null
$addr = Get-NetFirewallAddressFilter -AssociatedNetFirewallRule (Get-NetFirewallRule -Name $spec.Name)
Set-NetFirewallAddressFilter -InputObject $addr -RemoteAddress Any | Out-Null
}
else {
New-NetFirewallRule `
-Name $spec.Name `
-DisplayName $spec.Display `
-Direction Inbound `
-Action Allow `
-Protocol $spec.Protocol `
-LocalPort $spec.Port `
-RemoteAddress Any `
-Profile Any | Out-Null
}
}
$stock = @(Get-NetFirewallRule -Name 'RemoteDesktop-UserMode-In-*' -ErrorAction SilentlyContinue)
foreach ($rule in $stock) {
Set-NetFirewallRule -InputObject $rule -Enabled True -Profile Any | Out-Null
$addressFilter = Get-NetFirewallAddressFilter -AssociatedNetFirewallRule $rule
Set-NetFirewallAddressFilter -InputObject $addressFilter -RemoteAddress Any | Out-Null
}
}
Invoke-Step -Name 'Enable stock WINRM-HTTP-In-TCP* rules (RemoteAddress=Any)' -Action {
$rules = @(Get-NetFirewallRule -Name 'WINRM-HTTP-In-TCP*' -ErrorAction SilentlyContinue)
foreach ($rule in $rules) {
Set-NetFirewallRule -InputObject $rule -Enabled True | Out-Null
$addressFilter = Get-NetFirewallAddressFilter -AssociatedNetFirewallRule $rule
Set-NetFirewallAddressFilter -InputObject $addressFilter -RemoteAddress Any | Out-Null
}
}
Invoke-Step -Name 'Start WinRM and bind HTTP listener' -Required -Action {
Set-Service -Name WinRM -StartupType Automatic
Start-Service -Name WinRM
Start-Sleep -Seconds 2
Restart-Service -Name WinRM -Force
if (-not (Wait-WinrmListening -Seconds 45)) {
Write-Step 'Port 5985 still closed after registry listener; trying Enable-PSRemoting once.'
try {
Enable-PSRemoting -SkipNetworkProfileCheck -Force -ErrorAction Stop | Out-Null
}
catch {
Write-Step ("Enable-PSRemoting failed (continuing): {0}" -f $_.Exception.Message)
}
Restart-Service -Name WinRM -Force -ErrorAction SilentlyContinue
if (-not (Wait-WinrmListening -Seconds 45)) {
Write-WinrmDiagnostics
throw 'Nothing listening on TCP 5985 after listener config + restart'
}
}
Write-Step 'TCP 5985 is LISTENING'
}
Invoke-Step -Name 'Re-assert dedicated WinRM firewall allow rule' -Required -Action {
Set-NetFirewallRule -Name 'Ansible-WinRM-HTTP' -Enabled True -Profile Any | Out-Null
$filter = Get-NetFirewallAddressFilter -AssociatedNetFirewallRule (Get-NetFirewallRule -Name 'Ansible-WinRM-HTTP')
Set-NetFirewallAddressFilter -InputObject $filter -RemoteAddress Any | Out-Null
}
Invoke-Step -Name 'Verify WinRM listening + Test-WSMan' -Required -Action {
if (-not (Test-Port5985Listening)) {
Write-WinrmDiagnostics
throw 'Nothing listening on TCP 5985'
}
Write-Step 'netstat: :5985 LISTENING'
$okWsman = $false
for ($i = 1; $i -le 15; $i++) {
if (Test-LocalWinrmReady) {
$okWsman = $true
break
}
Start-Sleep -Seconds 2
}
if (-not $okWsman) {
Write-WinrmDiagnostics
throw 'Test-WSMan localhost failed'
}
Write-Step 'Test-WSMan localhost OK'
$tnc = Test-NetConnection -ComputerName 127.0.0.1 -Port 5985 -WarningAction SilentlyContinue
if (-not $tnc.TcpTestSucceeded) {
Write-WinrmDiagnostics
throw 'Test-NetConnection 127.0.0.1:5985 failed'
}
}
if ($script:failed) {
Write-Step 'Ansible WinRM bootstrap FAILED.'
exit 1
}
Write-Step 'Ansible WinRM bootstrap complete.'
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment