Created
August 7, 2026 21:25
-
-
Save fidelix/5ba2a5c6d4d02b256107aeccfd14b0e6 to your computer and use it in GitHub Desktop.
Enable-AnsibleWinRM.ps1 cert auth
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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