# ============================================================ # enable-winrm.ps1 # Готовит Windows-хост для управления через Ansible/WinRM. # Поддержка: Windows Server 2012 R2 / 2019 / Windows 10. # # Рантайм-вывод на английском, чтобы PS 4 + CP866 на 2012 R2 # не выводили кириллицу как крякозябры. # # Одна строка для запуска: # [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; ` # iwr -UseBasicParsing https://awm.aplo.pw/bootstrap/enable-winrm.ps1 ` # -OutFile $env:TEMP\enable-winrm.ps1; ` # powershell -ExecutionPolicy Bypass -File $env:TEMP\enable-winrm.ps1 # ============================================================ [CmdletBinding()] param( [switch]$EnableBasicAuth = $true, [int]$ListenerPort = 5986, [switch]$SkipWmfInstall ) $ErrorActionPreference = 'Stop' function Write-Step($m) { Write-Host "==> $m" -ForegroundColor Cyan } function Write-Ok($m) { Write-Host "OK $m" -ForegroundColor Green } function Write-Warn2($m){ Write-Host "!! $m" -ForegroundColor Yellow } function Write-Err($m) { Write-Host "XX $m" -ForegroundColor Red } # --- Права Administrator --- $me = [Security.Principal.WindowsIdentity]::GetCurrent() if (-not (New-Object Security.Principal.WindowsPrincipal($me)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw "Must run as Administrator." } # --- TLS 1.2 для скачиваний --- try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} # --- Версия PS / ОС --- $psMajor = $PSVersionTable.PSVersion.Major $psMinor = $PSVersionTable.PSVersion.Minor $osCaption = (Get-WmiObject Win32_OperatingSystem).Caption $osVersion = (Get-WmiObject Win32_OperatingSystem).Version Write-Step "OS: $osCaption ($osVersion) PowerShell: $psMajor.$psMinor" # ============================================================ # STAGE 1: WMF 5.1 (только если PS < 5.1) # ============================================================ if (($psMajor -lt 5) -or ($psMajor -eq 5 -and $psMinor -lt 1)) { if ($SkipWmfInstall) { Write-Warn2 "PowerShell $psMajor.$psMinor and -SkipWmfInstall given. Skipping WMF install." } else { Write-Step "PowerShell $psMajor.$psMinor - installing Windows Management Framework 5.1" $msuUrl = $null; $msuName = $null switch -Regex ($osVersion) { '^6\.3' { # Windows 8.1 / Server 2012 R2 $msuUrl = 'https://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win8.1AndW2K12R2-KB3191564-x64.msu' $msuName = 'Win8.1AndW2K12R2-KB3191564-x64.msu' } '^6\.2' { Write-Err "Windows 2012 (non-R2) - WMF 5.1 not officially supported. Upgrade the OS." exit 1 } default { Write-Err "Unsupported OS for WMF 5.1 auto-install (version $osVersion). Install PS 5.1 manually." exit 1 } } $tmpMsu = Join-Path $env:TEMP $msuName if (-not (Test-Path $tmpMsu)) { Write-Step "Downloading $msuName (~25 MB)..." Invoke-WebRequest -UseBasicParsing -Uri $msuUrl -OutFile $tmpMsu } else { Write-Ok "MSU already downloaded: $tmpMsu" } Write-Step ".NET Framework 4.5.2+ check (required for WMF 5.1)" $netKey = 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' $netRel = (Get-ItemProperty -Path $netKey -ErrorAction SilentlyContinue).Release if (-not $netRel -or $netRel -lt 379893) { Write-Err ".NET 4.5.2+ not installed (Release=$netRel). Install KB2901907 / .NET 4.8 first." exit 1 } Write-Ok ".NET Release=$netRel - OK" Write-Step "Installing WMF 5.1 (wusa.exe /quiet /norestart) - takes ~5 min..." $proc = Start-Process -FilePath 'wusa.exe' -ArgumentList "`"$tmpMsu`"", '/quiet', '/norestart' -Wait -PassThru switch ($proc.ExitCode) { 0 { Write-Ok "WMF 5.1 installed." } 3010 { Write-Ok "WMF 5.1 installed, reboot required." } 2359302 { Write-Ok "WMF 5.1 was already installed." } default { Write-Err "wusa.exe exit code $($proc.ExitCode). Check %WINDIR%\Logs\CBS\CBS.log"; exit 1 } } Write-Host "" Write-Host "======================================================" -ForegroundColor Yellow Write-Host " REBOOT the host NOW, then RUN THIS SCRIPT AGAIN." -ForegroundColor Yellow Write-Host " After reboot PowerShell becomes 5.1 and WinRM is configured." -ForegroundColor Yellow Write-Host "======================================================" -ForegroundColor Yellow exit 0 } } # ============================================================ # STAGE 2: WinRM config (PS >= 5.1 или -SkipWmfInstall) # ============================================================ # --- Network profile: WinRM refuses Public networks --- Write-Step "Network profile check" if (Get-Command Get-NetConnectionProfile -ErrorAction SilentlyContinue) { $publics = Get-NetConnectionProfile | Where-Object { $_.NetworkCategory -eq 'Public' } if ($publics) { foreach ($p in $publics) { Write-Warn2 "Network '$($p.Name)' is Public - switching to Private for WinRM" try { Set-NetConnectionProfile -InterfaceIndex $p.InterfaceIndex -NetworkCategory Private Write-Ok "Switched to Private" } catch { Write-Err "Failed to switch '$($p.Name)' to Private: $($_.Exception.Message)" Write-Err "Do it manually: Settings -> Network -> Properties -> Private" exit 1 } } } else { Write-Ok "All interfaces are Private/Domain" } } Write-Step "winrm quickconfig" & winrm quickconfig -force | Out-Null Write-Ok "WinRM service configured" # --- Self-signed cert --- Write-Step "Self-signed certificate for HTTPS listener" $dns = $env:COMPUTERNAME $existing = Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Subject -eq "CN=$dns" -and $_.NotAfter -gt (Get-Date) } | Select-Object -First 1 if ($existing) { $cert = $existing Write-Ok "Reusing existing cert $($cert.Thumbprint)" } else { if (Get-Command New-SelfSignedCertificate -ErrorAction SilentlyContinue) { $sscArgs = @{ DnsName = $dns; CertStoreLocation = 'Cert:\LocalMachine\My' } if ((Get-Command New-SelfSignedCertificate).Parameters.ContainsKey('NotAfter')) { $sscArgs.NotAfter = (Get-Date).AddYears(5) } $cert = New-SelfSignedCertificate @sscArgs Write-Ok "Created cert $($cert.Thumbprint)" } else { throw "New-SelfSignedCertificate not available. Install WMF 5.1 or create cert manually (makecert.exe / ADCS)." } } # --- HTTPS listener --- Write-Step "HTTPS listener on port $ListenerPort" $listenerPath = 'WSMan:\localhost\Listener' Get-ChildItem $listenerPath | Where-Object { $_.Keys -contains 'Transport=HTTPS' } | ForEach-Object { Remove-Item -Path $_.PSPath -Recurse -Force Write-Ok "Old HTTPS listener removed" } & winrm create "winrm/config/Listener?Address=*+Transport=HTTPS" "@{Hostname=`"$dns`"; CertificateThumbprint=`"$($cert.Thumbprint)`"; Port=`"$ListenerPort`"}" | Out-Null Write-Ok "HTTPS listener created" # --- Auth --- Write-Step "Auth methods" Set-Item WSMan:\localhost\Service\Auth\Basic $EnableBasicAuth Set-Item WSMan:\localhost\Service\Auth\Negotiate $true Set-Item WSMan:\localhost\Service\AllowUnencrypted $false Write-Ok "Basic=$EnableBasicAuth, Negotiate=true, AllowUnencrypted=false" # --- Firewall --- Write-Step "Firewall: allow $ListenerPort/tcp" $fwName = "WinRM-HTTPS-$ListenerPort" if (Get-Command New-NetFirewallRule -ErrorAction SilentlyContinue) { Get-NetFirewallRule -DisplayName $fwName -ErrorAction SilentlyContinue | Remove-NetFirewallRule New-NetFirewallRule -DisplayName $fwName -Direction Inbound -LocalPort $ListenerPort -Protocol TCP -Action Allow | Out-Null } else { & netsh advfirewall firewall delete rule name="$fwName" 2>$null | Out-Null & netsh advfirewall firewall add rule name="$fwName" dir=in action=allow protocol=TCP localport=$ListenerPort | Out-Null } Write-Ok "Firewall rule $fwName added" # --- Service --- Write-Step "Start / set WinRM auto-start" Set-Service -Name WinRM -StartupType Automatic if ((Get-Service WinRM).Status -ne 'Running') { Start-Service WinRM } Write-Ok "WinRM status: $((Get-Service WinRM).Status)" # --- Create/refresh local "awm" user for Ansible --- Write-Step "Local user 'awm' for Ansible (Administrators group)" # Generate a strong random password (alphanumeric + safe symbols, no ambiguous chars) $pwChars = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%*-_='.ToCharArray() $awmPassPlain = -join (1..24 | ForEach-Object { $pwChars | Get-Random }) $awmPassSec = ConvertTo-SecureString -String $awmPassPlain -AsPlainText -Force $existingUser = Get-LocalUser -Name 'awm' -ErrorAction SilentlyContinue if ($existingUser) { Set-LocalUser -Name 'awm' -Password $awmPassSec Write-Ok "Password reset for existing user 'awm'" } else { New-LocalUser -Name 'awm' -Password $awmPassSec -FullName 'AWM Ansible' ` -Description 'Ansible management account (AWM panel)' ` -PasswordNeverExpires -AccountNeverExpires | Out-Null Write-Ok "User 'awm' created" } # Resolve built-in Administrators group by SID (locale-independent, S-1-5-32-544) $adminsGroup = (Get-LocalGroup -SID 'S-1-5-32-544').Name Write-Ok "Administrators group: $adminsGroup" # Ensure in group (idempotent) $inAdmins = Get-LocalGroupMember -Group $adminsGroup -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "*\awm" -or $_.Name -eq 'awm' } if (-not $inAdmins) { Add-LocalGroupMember -Group $adminsGroup -Member 'awm' Write-Ok "Added 'awm' to $adminsGroup" } else { Write-Ok "'awm' already in $adminsGroup" } # --- Summary --- Write-Host "" Write-Host "=== WinRM ready ===" -ForegroundColor Green Write-Host "Host: $dns" Write-Host "Port: $ListenerPort (HTTPS)" Write-Host "Thumb: $($cert.Thumbprint)" Write-Host "Basic: $EnableBasicAuth" Write-Host "" Write-Host "Check from Linux control-node:" Write-Host " ansible-playbook playbooks/ping.yml -l $dns" # ============================================================ # STAGE 3: self-register in AWM panel (optional, best-effort) # ============================================================ Write-Step "Self-registering in AWM panel" try { # Force TLS 1.2 for this scope (child process may lose parent's setting) [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Collect all local IPv4 addresses (no loopback, no link-local, no APIPA) $ips = @() try { $ips = @(Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.IPAddress -notlike '127.*' -and $_.IPAddress -notlike '169.254.*' -and $_.AddressState -eq 'Preferred' } | Select-Object -ExpandProperty IPAddress) } catch {} if (-not $ips -or $ips.Count -eq 0) { # Fallback for old PowerShell versions $ips = @((Get-WmiObject Win32_NetworkAdapterConfiguration -Filter "IPEnabled=true" | ForEach-Object { $_.IPAddress } | Where-Object { $_ -match '^\d+\.' -and $_ -notlike '127.*' -and $_ -notlike '169.254.*' })) } $payload = @{ hostname = $env:COMPUTERNAME os_version = $osCaption ps_version = $PSVersionTable.PSVersion.ToString() winrm_port = $ListenerPort cert_thumbprint = $cert.Thumbprint local_ips = $ips awm_user = 'awm' awm_password = $awmPassPlain } | ConvertTo-Json -Compress $resp = Invoke-RestMethod -UseBasicParsing ` -Uri 'https://awm.aplo.pw/api/register-host' ` -Method POST -Body $payload -ContentType 'application/json' ` -TimeoutSec 15 Write-Ok "AWM: $($resp.status) as '$($resp.name)' (enabled=$($resp.enabled))" Write-Host "" Write-Host "Open AWM panel and approve this host:" -ForegroundColor Cyan Write-Host " https://awm.beta.aplosoft.ru/hosts" -ForegroundColor Cyan } catch { Write-Warn2 "Self-register failed: $($_.Exception.Message)" # Diagnostics — print DNS / TCP probe so the user knows why try { $tnc = Test-NetConnection -ComputerName awm.aplo.pw -Port 443 -WarningAction SilentlyContinue Write-Warn2 "Diag: DNS=$($tnc.NameResolutionSucceeded) TCP443=$($tnc.TcpTestSucceeded) ResolvedIP=$($tnc.RemoteAddress)" } catch { Write-Warn2 "Diag failed: $($_.Exception.Message)" } Write-Warn2 "Add the host manually via https://awm.beta.aplosoft.ru/hosts" Write-Host "" Write-Host "MANUAL CREDENTIAL (copy this to AWM panel -> Credentials -> local):" -ForegroundColor Yellow Write-Host " username: awm" -ForegroundColor Yellow Write-Host " password: $awmPassPlain" -ForegroundColor Yellow Write-Host "" }