Skip to content

Instantly share code, notes, and snippets.

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

  • Save fidelix/5ba2a5c6d4d02b256107aeccfd14b0e6 to your computer and use it in GitHub Desktop.

Select an option

Save fidelix/5ba2a5c6d4d02b256107aeccfd14b0e6 to your computer and use it in GitHub Desktop.
Enable-AnsibleWinRM.ps1 cert auth
# Enables the remote-management baseline for Ansible on Windows IoT Enterprise LTSC.
#
# Auth (WinRM certificate — not SSH keys):
# - HTTPS :5986 + client cert mapped to local user "solidseg"
# - Ansible uses keys/winrm/solidseg.{pem,key}; no password per run
# - USB ships solidseg.pem (public) + solidseg.pass (local password only for
# CertMapping; password change requires re-map)
# - HTTP :5985 NTLM kept as break-glass (network must be Private)
#
# Also forces Ethernet Private and enables RDP :3389.
# Do not expose 5985/5986/3389 to the Internet.
#
# Fresh IoT images often start WinRM with NO HTTP listener. This script writes
# the listener via registry + URL ACL while WinRM is stopped, then starts it.
$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'
}
}
# --- Certificate auth (Ansible primary) ------------------------------------
# WinRM does not use SSH keys. Client X.509 cert + CertMapping to solidseg.
$script:ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$script:ClientCertPem = Join-Path $script:ScriptDir 'solidseg.pem'
$script:SolidsegPassFile = Join-Path $script:ScriptDir 'solidseg.pass'
$script:SolidsegUser = 'solidseg'
Invoke-Step -Name 'Ensure solidseg local admin + password from solidseg.pass' -Required -Action {
if (-not (Test-Path -LiteralPath $script:SolidsegPassFile)) {
throw "Missing $script:SolidsegPassFile (USB must include the local password used for CertMapping)"
}
$passPlain = (Get-Content -LiteralPath $script:SolidsegPassFile -Raw).Trim()
if ([string]::IsNullOrWhiteSpace($passPlain)) {
throw 'solidseg.pass is empty'
}
$secure = ConvertTo-SecureString $passPlain -AsPlainText -Force
$user = Get-LocalUser -Name $script:SolidsegUser -ErrorAction SilentlyContinue
if (-not $user) {
New-LocalUser -Name $script:SolidsegUser -Password $secure -PasswordNeverExpires -UserMayNotChangePassword | Out-Null
Write-Step 'Created local user solidseg'
}
else {
$user | Set-LocalUser -Password $secure -PasswordNeverExpires $true
Write-Step 'Reset solidseg password from solidseg.pass'
}
$admins = Get-LocalGroupMember -Group 'Administrators' -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty Name
$isAdmin = $admins | Where-Object { $_ -like "*\$($script:SolidsegUser)" -or $_ -eq $script:SolidsegUser }
if (-not $isAdmin) {
Add-LocalGroupMember -Group 'Administrators' -Member $script:SolidsegUser
Write-Step 'Added solidseg to Administrators'
}
}
Invoke-Step -Name 'Import solidseg.pem into TrustedPeople + Root' -Required -Action {
if (-not (Test-Path -LiteralPath $script:ClientCertPem)) {
throw "Missing $script:ClientCertPem"
}
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($script:ClientCertPem)
foreach ($storeName in @('TrustedPeople', 'Root')) {
$store = Get-Item -LiteralPath "Cert:\LocalMachine\$storeName"
$store.Open('ReadWrite')
$exists = $store.Certificates | Where-Object { $_.Thumbprint -eq $cert.Thumbprint }
if (-not $exists) {
$store.Add($cert)
Write-Step ("Imported client cert into LocalMachine\{0}" -f $storeName)
}
else {
Write-Step ("Client cert already in LocalMachine\{0}" -f $storeName)
}
$store.Dispose()
}
}
Invoke-Step -Name 'Enable WinRM Certificate authentication' -Required -Action {
Set-Item -LiteralPath WSMan:\localhost\Service\Auth\Certificate -Value $true
}
Invoke-Step -Name 'Ensure HTTPS WinRM listener :5986' -Required -Action {
$https = @(Get-ChildItem -LiteralPath WSMan:\localhost\Listener -ErrorAction SilentlyContinue |
Where-Object { (Get-Item "WSMan:\localhost\Listener\$_\Transport").Value -eq 'HTTPS' })
if ($https.Count -eq 0) {
$serverCert = Get-ChildItem Cert:\LocalMachine\My |
Where-Object {
$_.HasPrivateKey -and
$_.NotAfter -gt (Get-Date) -and
($_.EnhancedKeyUsageList.ObjectId -contains '1.3.6.1.5.5.7.3.1' -or -not $_.EnhancedKeyUsageList)
} |
Sort-Object NotAfter -Descending |
Select-Object -First 1
if (-not $serverCert) {
$serverCert = New-SelfSignedCertificate `
-DnsName $env:COMPUTERNAME, 'localhost' `
-CertStoreLocation 'Cert:\LocalMachine\My' `
-KeyExportPolicy Exportable `
-KeySpec KeyExchange `
-KeyLength 2048 `
-HashAlgorithm SHA256 `
-NotAfter (Get-Date).AddYears(10) `
-TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.1')
Write-Step ("Created HTTPS server cert thumbprint={0}" -f $serverCert.Thumbprint)
}
New-Item -Path WSMan:\localhost\Listener -Transport HTTPS -Address * `
-CertificateThumbprint $serverCert.Thumbprint -Force | Out-Null
Write-Step 'Created WinRM HTTPS listener'
}
else {
Write-Step 'WinRM HTTPS listener already present'
}
$ruleName = 'Ansible-WinRM-HTTPS'
$existing = Get-NetFirewallRule -Name $ruleName -ErrorAction SilentlyContinue
if ($existing) {
Set-NetFirewallRule -Name $ruleName -Enabled True -Profile Any -Direction Inbound -Action Allow | Out-Null
}
else {
New-NetFirewallRule `
-Name $ruleName `
-DisplayName 'WinRM - Ansible HTTPS (Any profile)' `
-Direction Inbound `
-Action Allow `
-Protocol TCP `
-LocalPort 5986 `
-RemoteAddress Any `
-Profile Any | Out-Null
}
}
Invoke-Step -Name 'Map client certificate to solidseg (CertMapping)' -Required -Action {
$passPlain = (Get-Content -LiteralPath $script:SolidsegPassFile -Raw).Trim()
$secure = ConvertTo-SecureString $passPlain -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential ($script:SolidsegUser, $secure)
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($script:ClientCertPem)
$subject = $cert.GetNameInfo('UpnName', $false)
if ([string]::IsNullOrWhiteSpace($subject)) {
throw 'Client cert is missing UPN SAN (expected solidseg@localhost)'
}
$certChain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
[void]$certChain.Build($cert)
$caThumbprint = $certChain.ChainElements.Certificate[-1].Thumbprint
$existing = @(Get-ChildItem -LiteralPath WSMan:\localhost\ClientCertificate -ErrorAction SilentlyContinue |
Where-Object {
$keys = ($_ | Get-Item).Keys
("Subject=$subject" -in $keys)
})
foreach ($m in $existing) {
Remove-Item -LiteralPath $m.PSPath -Force -Recurse -ErrorAction SilentlyContinue
}
New-Item -Path WSMan:\localhost\ClientCertificate `
-Subject $subject `
-Issuer $caThumbprint `
-Credential $credential `
-Force | Out-Null
Write-Step ("CertMapping Subject={0} Issuer={1} -> solidseg" -f $subject, $caThumbprint)
}
Invoke-Step -Name 'Verify WinRM HTTPS :5986 listening' -Required -Action {
$lines = @(& netstat.exe -an | Select-String -Pattern '(:5986)\s+.*LISTENING')
if ($lines.Count -eq 0) {
Restart-Service -Name WinRM -Force
Start-Sleep -Seconds 3
$lines = @(& netstat.exe -an | Select-String -Pattern '(:5986)\s+.*LISTENING')
}
if ($lines.Count -eq 0) {
throw 'Nothing listening on TCP 5986 (HTTPS WinRM)'
}
Write-Step 'TCP 5986 is LISTENING'
}
if ($script:failed) {
Write-Step 'Ansible WinRM bootstrap FAILED.'
exit 1
}
Write-Step 'Ansible WinRM bootstrap complete (HTTP NTLM + HTTPS certificate).'
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment