Skip to content
ADMIN
FUTURE
Главная страница » PowerShell Remoting: Managing Servers via WinRM Without RDP
FIELD NOTES / 28.09.2026

PowerShell Remoting: Managing Servers via WinRM Without RDP

Maksim Shapovalov12 min read
Схема PowerShell Remoting: консоль администратора рассылает команды через WinRM на несколько серверов

From an RDP marathon to a single command

A familiar scene: you need to check free disk space on forty servers. The “firefighter admin” opens an RDP client, connects to the first server, opens “My Computer,” looks, closes it, connects to the second one… An hour and a half later, they know the state of the disks. A day after that, it turns out five servers filled up their disks in the meantime.

The “architect of the future” opens one PowerShell terminal and gets the same report for all forty servers in a few seconds — not because they type faster, but because they never log into the servers by hand at all. They use PowerShell Remoting — Windows’s built-in mechanism for remote command execution over the WinRM (Windows Remote Management) protocol.

This isn’t “yet another way to connect to a server.” It’s a fundamentally different model: instead of getting behind the wheel of each machine one at a time, you send the same route sheet to the entire fleet at once. This article covers how it works under the hood, how not to shoot yourself in the foot with security, and the traps (hello, double hop) almost everyone hits the first time they roll out remoting.


What WinRM is and how it works

WinRM is Microsoft’s implementation of the open WS-Management protocol: SOAP messages over HTTP. On every server with remoting enabled, the WinRM service runs, listening on port 5985 (HTTP) or 5986 (HTTPS). PowerShell over this transport spins up a full runspace — meaning a real wsmprovhost.exe process starts on the remote machine, your code actually runs inside it, and the result gets serialized and sent back.

An important side effect of this architecture: the objects a remote command returns aren’t “live” .NET objects — they’re deserialized snapshots of them (methods are lost, only properties remain). And PowerShell automatically stamps a PSComputerName property onto every such object — so you always know exactly which server a result line came from, without even asking explicitly.

Quick start

PowerShell

# On the server: enable WinRM, stand up an HTTP listener,
# create a firewall rule, and register the session configuration
Enable-PSRemoting -Force

# Check that the server actually responds over WinRM
Test-WSMan -ComputerName SRV-WEB01

# Which "entry points" (session configurations) are registered
Get-PSSessionConfiguration | Select-Object Name, Permission

On domain-joined servers, starting with recent Windows Server versions, WinRM is often already enabled by default — but it’s always worth running Test-WSMan before the first script run against a new server: it costs ten seconds, cheaper than later wondering why one server out of forty fell out of the report.


Invoke-Command and persistent sessions are different tools

The simplest call is one-shot, “run and forget”:

PowerShell

$cred = Get-Credential -Message 'Account with rights on the servers'

Invoke-Command -ComputerName SRV-WEB01, SRV-SQL01 -Credential $cred -ScriptBlock {
    Get-Service -Name W3SVC, MSSQLSERVER -ErrorAction SilentlyContinue
}

Here, every Invoke-Command -ComputerName call opens a temporary connection under the hood, runs the code, and closes it right away. That’s convenient for one-off checks, but inefficient if you need to run ten commands in a row against the same servers — each one tears down and re-establishes the WinRM session.

For that, there’s a second mode: persistent sessions:

PowerShell

$sessions = New-PSSession -ComputerName SRV-WEB01, SRV-SQL01 -Credential $cred

# First call — import the module into each session
Invoke-Command -Session $sessions -ScriptBlock { Import-Module NetAdapter }

# The second call against the same sessions already sees the imported module —
# state persists between calls, because it's the same runspace
Invoke-Command -Session $sessions -ScriptBlock { Get-NetAdapter | Where-Object Status -eq 'Up' }

# Don't forget to close them — otherwise wsmprovhost.exe sits on the server
# until the idle timeout expires
$sessions | Remove-PSSession

💡 Simple rule: one one-off request to the fleet — use -ComputerName. A series of related commands against the same servers (with shared variables, imported modules, in-between state) — use New-PSSession + -Session. Mix them up, and you’ll either needlessly recreate sessions for every little thing, or forget to close a hundred hanging runspaces on your servers.


Polling the fleet in parallel, and error handling

Invoke-Command with multiple server names already runs in parallel, not sequentially — by default capped at -ThrottleLimit 32 simultaneous connections. For hundreds of servers, this limit is worth explicitly tuning up or down depending on the load on your management machine and network, rather than leaving it “as is.”

A real report script for uptime and free disk space across the whole fleet — one that handles unreachable servers gracefully instead of the whole script dying because of one powered-off host:

PowerShell

$servers = 'SRV-WEB01', 'SRV-WEB02', 'SRV-SQL01', 'SRV-DC01'
$cred    = Get-Credential -Message 'Account with rights on the servers'

$results = Invoke-Command -ComputerName $servers -Credential $cred `
    -ThrottleLimit 10 -ErrorAction SilentlyContinue -ErrorVariable connErrors `
    -ScriptBlock {
        $os    = Get-CimInstance Win32_OperatingSystem
        $disks = Get-Volume | Where-Object { $_.DriveLetter -and $_.DriveType -eq 'Fixed' }

        [PSCustomObject]@{
            UptimeDays  = [math]::Round(((Get-Date) - $os.LastBootUpTime).TotalDays, 1)
            FreeDiskGB  = [math]::Round((($disks | Measure-Object SizeRemaining -Sum).Sum / 1GB), 1)
            TotalDiskGB = [math]::Round((($disks | Measure-Object Size -Sum).Sum / 1GB), 1)
        }
    }

# Servers that didn't respond don't crash the script — they're listed separately
foreach ($e in $connErrors) {
    Write-Warning "Unreachable: $($e.TargetObject) — $($e.Exception.Message)"
}

$results |
    Select-Object PSComputerName, UptimeDays, FreeDiskGB, TotalDiskGB |
    Sort-Object UptimeDays -Descending |
    Format-Table -AutoSize

$results | Export-Csv "C:Reportsfleet-health-$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation -Encoding UTF8

Notice Get-CimInstance instead of the old Get-WmiObject: CIM cmdlets, when talking to a remote machine, also go over WinRM instead of DCOM/RPC like classic WMI did. That means you don’t need to separately open the DCOM port range on the firewall (135 + a dynamic range) — all the traffic, both remoting itself and the CIM queries inside it, goes through the same 5985/5986 port.

The same principle — “one command instead of walking the servers” — is also at the heart of the Active Directory security audit — except there, instead of disks and uptime, Get-ADUser and Search-ADAccount hunt for inactive accounts and forever passwords.


Authentication: a domain means Kerberos, a workgroup doesn’t

Inside a domain, everything “just works”: the client and server authenticate each other via Kerberos — this is mutual authentication, the server also proves to the client that it’s who it claims to be.

The trouble starts outside the domain — a workgroup, a DMZ, a server in a different, untrusted domain. Kerberos isn’t available there, WinRM falls back to NTLM, and NTLM doesn’t verify the server’s identity. By default, the WinRM client will simply refuse to connect to such a host — and that’s not a bug, it’s the only protection against negotiating with a “server” that’s actually an impostor. You can force past it like this:

PowerShell

Set-Item WSMan:localhostClientTrustedHosts -Value 'SRV-WEB01,SRV-WEB02' -Force

Don’t put * in there. This isn’t a checkbox formality — it literally means “trust any server with any name that responds at this address,” i.e. disabling NTLM’s only defense against a MITM attack. List specific names or a specific network segment’s mask, and for regular remoting outside the domain, switch to the HTTPS listener below — that actually closes the problem, instead of relying on a list of trusted names.


Securing WinRM: HTTPS, Basic auth, and JEA

An HTTPS listener instead of TrustedHosts

The right way to work with non-domain or cross-forest servers isn’t to widen the exceptions list — it’s to stand up an encrypted transport with a real certificate (not a throwaway self-signed one — a self-signed cert protects against traffic sniffing, but not against server impersonation, unless you’re verifying its thumbprint):

PowerShell

$thumb = (Get-ChildItem Cert:LocalMachineMy |
    Where-Object Subject -eq 'CN=srv-web01.contoso.local').Thumbprint

New-Item -Path WSMan:localhostListener -Transport HTTPS `
    -Address * -CertificateThumbPrint $thumb -Force

Enable-NetFirewallRule -DisplayName 'Windows Remote Management (HTTPS-In)'

# Basic authentication sends the login/password as Base64 — that's NOT encryption,
# it's just encoding. Without HTTPS, this is equivalent to a plaintext password
Set-Item WSMan:localhostServiceAuthBasic -Value $false

JEA: don’t hand out admin access where one command would do

Just Enough Administration solves a different problem: not “how do I connect securely,” but “what is this person allowed to do once they’re connected.” A classic example: first-line support that needs to restart one specific service in production, but absolutely doesn’t need local admin rights.

PowerShell

# What's allowed inside this role
New-PSRoleCapabilityFile -Path .HelpDesk.psrc `
    -VisibleCmdlets 'Restart-Service', 'Get-Service', 'Clear-DnsClientCache'

# Who gets which set of roles — plus a temporary virtual account
# instead of the operator's real administrative login
New-PSSessionConfigurationFile -Path .HelpDesk.pssc `
    -SessionType RestrictedRemoteServer `
    -RunAsVirtualAccount `
    -RoleDefinitions @{ 'CONTOSOHelpDesk-L1' = @{ RoleCapabilities = 'HelpDesk' } }

Register-PSSessionConfiguration -Path .HelpDesk.pssc -Name 'JEA-HelpDesk' -Force

Now an operator in the HelpDesk-L1 group connects with Enter-PSSession -ComputerName SRV-WEB01 -ConfigurationName JEA-HelpDesk — and physically cannot run anything besides the three allowed commands, no matter how hard they try. Rights aren’t granted to the person permanently — they’re granted to the session, for the duration of the connection, through a temporary virtual account.


The double hop: a trap almost everyone falls into

You connect to SRV-WEB01 and, from inside the remote session, try to reach a network share on SRV-SQL01, or run Invoke-Command against yet another server. You get “Access Denied” — even though the account definitely has rights on both servers.

This is the double-hop problem. The Kerberos ticket you authenticated to SRV-WEB01 with stays right there by default — it isn’t forwarded to the second hop, because Kerberos doesn’t delegate your credentials to a third party by default. This isn’t a WinRM bug — it’s deliberate protocol-level protection: otherwise any server you’d ever logged into could impersonate you to any other server on the network.

There are three real solutions, and they aren’t equivalent:

  1. Pass credentials again on the second hop. The simplest, most underrated option: inside the remote scriptblock, explicitly specify -Credential again for reaching the second server. No delegation needed — it’s just a new, independent authentication. The downside is you need to securely get the credential into the session (e.g. via Export-Clixml encrypted under the account the script actually runs as, not a plaintext password in the script body).
  2. CredSSP. Enabled on the client and on the intermediate server, after which your credentials really are forwarded to the second hop:
    # On the admin's machine
    Enable-WSManCredSSP -Role Client -DelegateComputer SRV-WEB01
    
    # On the intermediate server (SRV-WEB01)
    Enable-WSManCredSSP -Role Server
    
    Invoke-Command -ComputerName SRV-WEB01 -Credential $cred -Authentication Credssp -ScriptBlock {
        Get-ChildItem \SRV-SQL01backups
    }
    
    This works, but the convenience has a real cost: your credentials, in forwardable form, end up sitting in the intermediate server’s memory. If SRV-WEB01 is compromised, an attacker with admin rights on it can replay them further across the network. Use CredSSP only on servers you genuinely trust as much as your own workstation, and only where there’s no way around it.
  3. Kerberos constrained delegation (ideally resource-based). The correct production solution: instead of forwarding the credentials themselves, explicitly allow one server to obtain Kerberos tickets on the user’s behalf to access a specific other server. The modern variant is resource-based delegation, configured on the resource side rather than the source:
    # Configured on SRV-SQL01 (the resource), not on SRV-WEB01:
    # "allow SRV-WEB01 to act on behalf of users toward me"
    Set-ADComputer -Identity 'SRV-SQL01' `
        -PrincipalsAllowedToDelegateToAccount (Get-ADComputer 'SRV-WEB01')
    
    The user’s credentials never leave the domain controller — the intermediary server receives not a password, but a limited ticket scoped to the specific target. This requires rights to modify a computer object in AD, not full domain admin, which by itself lowers the risk of rolling it out.

⚠️ If you see advice that says “just enable CredSSP” with no mention of delegation or trust in the intermediate server — that’s only half the instructions. CredSSP fixes the symptom in one evening; constrained delegation fixes the cause and survives your next security audit.


Safe remoting checklist

  • Inside the domain — Kerberos by default, nothing extra to configure.
  • Outside the domain — an HTTPS listener with a real certificate, not TrustedHosts = "*".
  • Disable Basic authentication everywhere HTTPS isn’t in use.
  • For non-privileged operators — JEA instead of a full administrative login.
  • Close the double-hop gap with constrained delegation; treat CredSSP only as a deliberate, temporary exception.
  • Close persistent sessions (Remove-PSSession) — nobody monitors forgotten wsmprovhost.exe processes on servers until there are fifty of them.

📄 Download this checklist as a PDF — print it or keep it on hand before rolling out remoting in production. (Currently only available in Russian.)


From one-off commands to fleet management

PowerShell Remoting isn’t “a trick for the lazy” — it’s the foundation modern Windows infrastructure automation is built on: DSC, most of the Azure modules, and even Windows Admin Center itself talk to servers over WinRM under the hood. Learn it once — understanding where Kerberos applies and where NTLM takes over, why you need an HTTPS listener, and why the double hop isn’t solved by one settings checkbox — and you stop being the person who walks from server to server, and become the one who dispatches a task to the entire fleet with a single command.


Tags: #powershell #winrm #remoting #windows #security #automation #sysadmin


Want to become tomorrow’s architect instead of just an admin? This article is only the tip of the iceberg. On our Telegram channel “Admin Future” we publish systems-thinking guides, ready-to-use scripts, AI tips, and checklists every day that you won’t find on Google. We don’t just post commands — we’re building a knowledge library for people who want to grow from admin to architect. Subscribe and become the expert in the room: Admin Future

МШ

Maksim Shapovalov

Systems administrator. 17+ years working with infrastructure.

Discuss your project ↗

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top