Skip to content
Logo
Table of contents
VeryCloud
DocumentationVPS Windows

PowerShell Remoting (WinRM): managing multiple Windows Servers

PowerShell Remoting (WinRM): managing multiple Windows Servers

Control multiple Windows servers remotely with PowerShell Remoting. Run scripts, install packages, retrieve information from an entire fleet from a single workstation, without RDP.

Category
VPS Windows
Level
Intermediate
Reading time
6 min
Published
May 17, 2026

Introduction

Managing 5 Windows servers by RDP-ing into each is inefficient. PowerShell Remoting (based on WinRM, Windows Remote Management) lets you:

  • Run a command on 10 servers in parallel
  • Retrieve info from a centralized script
  • Install updates across a whole fleet
  • Audit configurations

It's the SSH equivalent of the Windows world: the foundation of modern Windows sysadmin.

Prerequisites

  • 2+ Windows Server 2019/2022/2025 (or Windows 10/11 Pro)
  • A user with admin privileges on all servers
  • Network connectivity between machines (TCP 5985 HTTP or 5986 HTTPS)

Step 1: Enable WinRM on target servers

On each target server (PowerShell as admin):

Enable-PSRemoting -Force

This command:

  • Starts the WinRM service
  • Configures firewall for TCP 5985 (HTTP)
  • Creates a WinRM listener
  • Enables delegation for admins

Verify:

Get-Service WinRM
Get-PSSessionConfiguration
winrm enumerate winrm/config/listener

Step 2: Test connection from the management server

From your admin workstation (PowerShell):

Test-WSMan -ComputerName srv-app-01

Expected output:

wsmid           : http://schemas.dmtf.org/wbem/wsman/identity/1/...
ProductVendor   : Microsoft Corporation
ProductVersion  : OS: 0.0.0 SP: 0.0 Stack: 3.0

If error, see troubleshooting section.

Step 3: Configure TrustedHosts (for non-domain)

If your servers are not in an AD domain, you must add targets to client TrustedHosts:

# Add a specific server
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "srv-app-01" -Force

# Add multiple
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "srv-app-01,srv-db-01,srv-web-01" -Force

# Allow all (DEV only)
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*" -Force

# Check
Get-Item WSMan:\localhost\Client\TrustedHosts

Step 4: Run a remote command

One command, one server

Invoke-Command -ComputerName srv-app-01 -ScriptBlock { 
    Get-ComputerInfo | Select-Object CsName, OsName, OsVersion 
} -Credential (Get-Credential)

A window asks for credentials. To automate:

$cred = Get-Credential
Invoke-Command -ComputerName srv-app-01 -ScriptBlock { hostname } -Credential $cred

One command across multiple servers in parallel

$servers = @("srv-app-01", "srv-app-02", "srv-db-01")

Invoke-Command -ComputerName $servers -ScriptBlock {
    Get-Service | Where-Object Status -eq 'Running' | Measure-Object | Select-Object Count
} -Credential $cred

Output: Get-Service runs in parallel on all 3 servers.

Step 5: Persistent sessions (PSSession)

To run multiple commands without re-authenticating each time:

$session = New-PSSession -ComputerName srv-app-01 -Credential $cred

Invoke-Command -Session $session -ScriptBlock { Get-Service WinRM }
Invoke-Command -Session $session -ScriptBlock { Get-Process | Select -First 5 }

# When done
Remove-PSSession $session

Interactive session (SSH-like)

Enter-PSSession -ComputerName srv-app-01 -Credential $cred

Prompt changes: [srv-app-01]: PS C:\Users\admin\>. Type Windows commands as if local. To exit:

Exit-PSSession

Step 6: Copy files via WinRM

$session = New-PSSession -ComputerName srv-app-01 -Credential $cred

# Push (local → remote)
Copy-Item -Path "C:\scripts\install.ps1" -Destination "C:\temp\install.ps1" -ToSession $session

# Pull (remote → local)
Copy-Item -Path "C:\logs\app.log" -Destination "C:\local-logs\srv-app-01_app.log" -FromSession $session

Remove-PSSession $session

Step 7: Common admin scripts

Complete fleet inventory

$servers = @("srv-app-01", "srv-app-02", "srv-db-01")
$cred = Get-Credential

$inventory = Invoke-Command -ComputerName $servers -Credential $cred -ScriptBlock {
    [PSCustomObject]@{
        Hostname    = $env:COMPUTERNAME
        OS          = (Get-CimInstance Win32_OperatingSystem).Caption
        Version     = (Get-CimInstance Win32_OperatingSystem).Version
        CPU         = (Get-CimInstance Win32_Processor).Name
        Cores       = (Get-CimInstance Win32_Processor).NumberOfCores
        RAM_GB      = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)
        Disk_C_Free = [math]::Round((Get-PSDrive C).Free / 1GB, 2)
        Uptime      = (Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
        LastUpdate  = (Get-HotFix | Sort-Object InstalledOn -Descending | Select -First 1).InstalledOn
    }
}

$inventory | Format-Table -AutoSize
$inventory | Export-Csv "C:\fleet-inventory.csv" -NoTypeInformation

Install Windows updates everywhere

Invoke-Command -ComputerName $servers -Credential $cred -ScriptBlock {
    Install-PackageProvider -Name NuGet -Force | Out-Null
    Install-Module PSWindowsUpdate -Force | Out-Null
    Import-Module PSWindowsUpdate
    Get-WindowsUpdate -AcceptAll -Install -AutoReboot
}

Service status across the fleet

Invoke-Command -ComputerName $servers -Credential $cred -ScriptBlock {
    Get-Service -Name "Spooler","WinRM","DNS" | 
        Select-Object @{N='Server';E={$env:COMPUTERNAME}}, Name, Status
} | Format-Table -AutoSize

List active users

Invoke-Command -ComputerName $servers -Credential $cred -ScriptBlock {
    query user
}

Restart multiple servers

Restart-Computer -ComputerName $servers -Credential $cred -Force -Wait -For PowerShell -Timeout 600

Step 8: Secure with HTTPS (WinRM over SSL)

By default WinRM uses unencrypted HTTP (port 5985). In production, encrypt with HTTPS (port 5986).

Generate a self-signed cert (test) or use a CA

$cert = New-SelfSignedCertificate -DnsName "srv-app-01" -CertStoreLocation "Cert:\LocalMachine\My"

Create the HTTPS listener

$thumbprint = $cert.Thumbprint
New-Item -Path WSMan:\localhost\Listener -Transport HTTPS -Address * -CertificateThumbprint $thumbprint -Force

Open firewall

New-NetFirewallRule -DisplayName "WinRM HTTPS" -Protocol TCP -LocalPort 5986 -Action Allow

Disable HTTP

Get-ChildItem WSMan:\localhost\Listener | Where-Object { $_.Keys -match "HTTP$" } | Remove-Item -Recurse
Disable-NetFirewallRule -DisplayName "Windows Remote Management (HTTP-In)"

Connect over HTTPS

Enter-PSSession -ComputerName srv-app-01 -UseSSL -Credential $cred

For a self-signed cert, add -SessionOption (New-PSSessionOption -SkipCACheck -SkipCNCheck).

Step 9: JEA (Just Enough Administration)

To give a user the right to run certain commands without making them admin:

# Create a session config
New-PSSessionConfigurationFile -Path C:\Config\WebAdmin.pssc -SessionType RestrictedRemoteServer -RoleDefinitions @{'DOMAIN\WebOps' = @{ RoleCapabilities = 'WebOpsRole' }}

# Create role capability
New-PSRoleCapabilityFile -Path C:\Config\WebOpsRole.psrc -VisibleCmdlets @('Restart-Service', 'Get-Service', 'Get-IISSite')

# Register
Register-PSSessionConfiguration -Path C:\Config\WebAdmin.pssc -Name WebAdmin

User DOMAIN\WebOps can now connect to this restricted session and only use allowed cmdlets.

Step 10: Group Policy to enable WinRM at scale

To enable WinRM on all servers in an AD domain:

  1. GPMC.msc → create/edit a GPO
  2. Computer Configuration → Policies → Administrative Templates → Windows Components → Windows Remote Management (WinRM)
  3. Enable:
    • WinRM Service: Allow remote server management through WinRM = Enabled
    • Windows Firewall: open ports 5985/5986
  4. Link GPO to servers OU

gpupdate /force on servers to apply immediately.

Troubleshooting

"WinRM cannot complete the operation"

WinRM service isn't started on the target:

Get-Service WinRM
Start-Service WinRM
Set-Service WinRM -StartupType Automatic

"Access is denied"

User not in Remote Management Users group or not admin. Add:

Add-LocalGroupMember -Group "Remote Management Users" -Member "DOMAIN\username"

"The WinRM client cannot process the request"

Add target to TrustedHosts (step 3) or use HTTPS.

Firewall blocks

Get-NetFirewallRule | Where-Object DisplayName -like "*Windows Remote Management*" | Enable-NetFirewallRule

Timeout on long commands

$opt = New-PSSessionOption -OperationTimeout 600000  # 10 min
Invoke-Command -ComputerName srv -ScriptBlock {...} -SessionOption $opt

Useful commands

# List active sessions
Get-PSSession

# Kill all sessions
Get-PSSession | Remove-PSSession

# View WinRM config
winrm get winrm/config
winrm get winrm/config/listener

# Test from CLI
winrm identify -r:http://srv-app-01:5985

# Full WinRM reset
winrm quickconfig -force
winrm invoke restore winrm/config

# List listeners
winrm enumerate winrm/config/listener

# Get WinRM log
Get-WinEvent -LogName "Microsoft-Windows-WinRM/Operational" -MaxEvents 50

Conclusion

With PowerShell Remoting mastered:

  • You manage 1 or 100 servers with the same effort
  • Scripts run in parallel automatically
  • You avoid RDP sessions (faster, audit-friendly)

Going further:

  • Combine with DSC (Desired State Configuration) for infra-as-code
  • Use Azure Arc to manage on-prem Windows servers from the cloud
  • Migrate to Ansible or Puppet for multi-OS orchestration

Resources

Join our Discord community server

For any questions, suggestions, or just to chat with the community, join us on Discord!

900+Members