🔷 PowerShell

PowerShell Remoting and JEA (Just Enough Administration)

📅 July 21, 202610 min readITVedas

How WinRM, Invoke-Command, PSSessions, and JEA role capabilities let you administer fleets of servers securely without RDP or standing admin rights.

ADVANCED
⏱ 10 min read
Prerequisites:
Key Facts
  • WinRM listens on TCP 5985 for HTTP and 5986 for HTTPS, not the RDP port 3389
  • PS Remoting is enabled by default on Windows Server since 2012 but must be turned on manually with Enable-PSRemoting on client Windows
  • Invoke-Command runs a script block on one or many machines and returns results; Enter-PSSession opens an interactive shell on exactly one machine
  • JEA endpoints run commands under a temporary virtual account, so the connecting user never needs standing admin rights on the box

Why remoting beats RDP at scale

RDP-and-click administration has a hard ceiling: it is inherently one-to-one. You open a session, you click through a console, and whatever you did lives only in your memory or a change-ticket comment. It does not repeat cleanly, it does not scale to fifty servers, and it leaves no structured record of what actually happened. PowerShell Remoting inverts that model. Instead of moving yourself to the machine, you send a script block to the machine (or to a hundred machines at once) and get structured objects back. The same script that patches one server patches a datacenter, and because the unit of work is a script block rather than a sequence of mouse clicks, it is trivially auditable, testable, and source-controllable.

This matters most once an estate crosses roughly a dozen servers. Below that, RDP fatigue is annoying but survivable. Above it, RDP-and-click stops being an administration strategy and becomes a bottleneck: every task takes N times as long as N grows, and every task is only as consistent as the human doing the clicking that day. Remoting is what lets one admin credibly own two hundred servers instead of twenty.

WinRM: the transport underneath

PowerShell Remoting is not its own network protocol — it rides on WinRM, Microsoft's implementation of the WS-Management (WS-Man) standard. WinRM wraps PowerShell commands and their serialized output in SOAP envelopes and moves them over HTTP or HTTPS. This is a deliberate design choice: because the transport is ordinary HTTP(S), it traverses the same firewalls, proxies, and load balancers that any other web traffic does, without needing a dedicated protocol punch-through the way RDP or raw RPC does.

The defaults matter for firewall rules and for troubleshooting:

Note that neither of these is port 3389 (RDP) or port 445 (SMB) — a common early-career confusion. When Enable-PSRemoting runs, it does three things: it starts the WinRM service and sets it to auto-start, it creates an HTTP listener bound to all IP addresses on 5985, and it opens the "Windows Remote Management (HTTP-In)" firewall rule for the active profile. You can inspect the listener configuration directly:

winrm enumerate winrm/config/listener
Get-Item WSMan:\localhost\Client\TrustedHosts
Test-WSMan -ComputerName SQL01

Test-WSMan is the fastest sanity check when remoting fails — it tells you immediately whether the problem is "WinRM isn't listening on the target" versus something deeper in authentication or authorization.

Client vs. server: why the defaults differ

This trips up more admins than almost anything else in the remoting stack: PS Remoting is on by default on Windows Server (every version since Server 2012) and off by default on Windows client editions (Windows 10, Windows 11). Trying to Invoke-Command against a fresh Windows 11 machine fails until someone runs Enable-PSRemoting -Force on it, while a freshly deployed Windows Server instance already has a listener waiting.

The reasoning is exposure surface, not an oversight. Servers are provisioned specifically to be administered remotely and typically live behind network segmentation, so an open management listener is expected and desired. Client machines are end-user devices that roam onto coffee-shop Wi-Fi, home networks, and other untrusted segments — leaving a remote-administration listener open by default on every laptop in the world would be a meaningful attack surface increase for very little benefit to the average user. When you deliberately want a client machine reachable for remoting (a build agent, a lab VM, a kiosk you manage centrally), you opt in explicitly:

# On the target client machine, run elevated:
Enable-PSRemoting -Force

# If the machines aren't domain-joined (workgroup or cross-forest),
# you also need to tell the *initiating* machine to trust the target:
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "SQL01,WEB0*" -Force

TrustedHosts only matters outside a domain. Inside Active Directory, Kerberos mutual authentication already establishes trust between domain-joined machines, so TrustedHosts stays empty and remoting works using the caller's domain credentials without extra configuration — another reason server-to-server remoting inside a domain "just works" while workgroup and cross-forest scenarios need manual trust setup.

Invoke-Command vs. Enter-PSSession

These are the two entry points into remoting, and they solve different problems. Confusing them is the single most common remoting mistake among admins moving off RDP.

Enter-PSSession opens an interactive remote shell against exactly one computer. Your prompt changes to show the remote machine name, and every command you type from that point executes there until you run Exit-PSSession. It is the direct analog of SSH-ing into a box — useful for exploratory troubleshooting where you don't yet know what you're looking for.

Enter-PSSession -ComputerName WEB01
# Prompt becomes: [WEB01]: PS C:\Users\admin>
Get-Service -Name W3SVC
Get-EventLog -LogName System -Newest 20
Exit-PSSession

Invoke-Command is fire-and-forget: you hand it a script block and a list of one or more target computers, it runs the block on all of them in parallel, and it returns the results to your local session as deserialized objects, tagged with a PSComputerName property so you can tell which result came from which machine. This is the workhorse for anything scripted, scheduled, or run against more than one server.

$servers = 'WEB01','WEB02','WEB03','SQL01'

Invoke-Command -ComputerName $servers -ScriptBlock {
    [PSCustomObject]@{
        Service     = 'W3SVC'
        Status      = (Get-Service -Name W3SVC -ErrorAction SilentlyContinue).Status
        DiskFreeGB  = [math]::Round(
            (Get-PSDrive -Name C).Free / 1GB, 1
        )
    }
} -ErrorAction SilentlyContinue |
    Select-Object PSComputerName, Service, Status, DiskFreeGB |
    Format-Table -AutoSize

Run against four servers, that one call fans out over four simultaneous WinRM connections and comes back with a single table — no loop, no manual RDP into each box. Invoke-Command also accepts -ThrottleLimit to cap how many concurrent connections it opens (default 32), which matters when you point it at a few hundred machines at once and don't want to flood the network or the source machine's connection pool.

The rule of thumb: default to Invoke-Command. Reach for Enter-PSSession only when you're genuinely exploring interactively on a single box and don't yet know what commands you'll need to run.

PSSessions and connection reuse

Both cmdlets above, used with -ComputerName, create a connection, run the command, and tear the connection down — every single call. That's fine for one-off queries but wasteful when you're running several related commands against the same machine, because each call re-pays the TCP handshake, WinRM negotiation, and authentication cost. It also means each call starts with a fresh, stateless remote runspace — any variable you set in one call is gone by the next.

New-PSSession solves both problems by creating a persistent connection you hold onto and reuse:

$session = New-PSSession -ComputerName SQL01

# First call: check current state
Invoke-Command -Session $session -ScriptBlock {
    Get-Process -Name sqlservr | Select-Object Id, WorkingSet
}

# Set a variable that persists in the remote runspace
Invoke-Command -Session $session -ScriptBlock {
    $global:svc = Get-Service -Name MSSQLSERVER
}

# Second call, same connection, same remote state, no reconnect:
Invoke-Command -Session $session -ScriptBlock {
    $svc | Restart-Service -Force
    Start-Sleep -Seconds 5
    $svc.Refresh()
    $svc.Status
}

Remove-PSSession $session

A single PSSession can also be passed to Enter-PSSession -Session $session if you want to drop into it interactively after running scripted steps against it — the same underlying connection, whether you're driving it interactively or programmatically. You can list what's open with Get-PSSession and should always Remove-PSSession when finished; orphaned sessions consume memory on the target until they time out (the default idle timeout is four hours).

For genuinely large fan-out work, Invoke-Command -ComputerName $servers -AsJob combines this with background execution, returning a job you poll with Receive-Job instead of blocking your console while every server responds.

Just Enough Administration (JEA)

Everything above assumes the connecting user is someone you'd trust with full administrative control of the target — because by default, that's exactly what remoting grants. A user who can open a PSSession on a domain admin's behalf can run anything on that box that an interactive admin could.

JEA changes that equation. It lets you publish a custom remoting endpoint on a machine that constrains a connecting user to a specific, named list of cmdlets, functions, and even specific parameter values — regardless of what rights that user's own account actually has. The canonical example: a helpdesk technician needs to restart the Print Spooler or a specific IIS app pool when it hangs, and nothing else. Without JEA, you either give them full local admin (too much) or make them file a ticket every time (too slow). With JEA, they connect to a JEA endpoint, and the only cmdlet the session exposes is Restart-Service, constrained to that one service name.

The mechanism that makes this safe, not just cosmetic, is the virtual account. When a user connects to a JEA endpoint, PowerShell does not run their commands under their own identity — it spins up a temporary local (or group-managed domain) account that exists only for the lifetime of that session, has exactly the local group memberships you configured, and disappears when the session ends. The connecting user's own account never touches an admin group at all. Combined with mandatory PowerShell transcription (JEA endpoints log every command executed to a transcript file by default), you get both the restriction and a full audit trail of who did what through the constrained endpoint.

Building and registering a JEA endpoint

A JEA endpoint is defined by two files working together: a role capability file (.psrc) that lists what's allowed, and a session configuration file (.pssc) that maps security groups to role capabilities and wires the whole thing to a virtual account.

The role capability file is where the real restriction lives. VisibleCmdlets is not just a name whitelist — each entry can pin down exactly which parameter values are legal, which is what turns "can restart services" into "can restart exactly one service":

# SpoolerHelpdesk.psrc
@{
    Author      = 'ITVedas'
    Description = 'Helpdesk tier 1: restart the print spooler only, nothing else'

    VisibleCmdlets = @(
        @{
            Name       = 'Restart-Service'
            Parameters = @(
                @{ Name = 'Name'; ValidateSet = 'Spooler' }
                @{ Name = 'Force' }
            )
        },
        @{
            Name       = 'Get-Service'
            Parameters = @(
                @{ Name = 'Name'; ValidateSet = 'Spooler' }
            )
        }
    )

    VisibleFunctions = @()
    VisibleAliases   = @()
}

Any parameter not listed under a cmdlet is unavailable in the session, and ValidateSet hard-blocks any value outside the allowed list — the technician cannot type Restart-Service -Name MSSQLSERVER and have it silently work; the endpoint will not expose that value as legal input at all. The session configuration file then binds a security group to this role capability and specifies the virtual account's group membership:

# SpoolerHelpdesk.pssc
@{
    SchemaVersion         = '2.0.0.0'
    SessionType           = 'RestrictedRemoteServer'
    RunAsVirtualAccount   = $true
    RoleDefinitions       = @{
        'CONTOSO\HelpdeskTier1' = @{ RoleCapabilities = 'SpoolerHelpdesk' }
    }
    TranscriptDirectory   = 'C:\ProgramData\JEAConfig\Transcripts'
}

Registering the endpoint makes it available as a named connection target, separate from the default administrative remoting endpoint:

Register-PSSessionConfiguration `
    -Name 'SpoolerHelpdesk' `
    -Path 'C:\ProgramData\JEAConfig\SpoolerHelpdesk.pssc' `
    -Force

# A helpdesk technician then connects not to the default endpoint,
# but explicitly to the JEA one:
Enter-PSSession -ComputerName WEB01 -ConfigurationName SpoolerHelpdesk

Inside that session, Get-Command shows only Restart-Service and Get-Service — no Get-Process, no file system access, no way to reach anything else on the box, even though the connection succeeded and the WinRM transport is identical to a full administrative session. The restriction is enforced server-side by the constrained runspace, not by client-side politeness, so it holds even against a technician actively trying to escape the sandbox. That combination — narrow capability, ephemeral elevated identity, and mandatory transcription — is what makes JEA the right answer for delegating specific server tasks without widening your admin group.

Key Takeaways

  • Use Invoke-Command for fan-out, scripted, unattended work against many machines at once
  • Use Enter-PSSession only when you genuinely need an interactive back-and-forth session on a single host
  • Reuse a PSSession object across multiple Invoke-Command calls to avoid paying the connection and authentication cost repeatedly
  • JEA role capability files whitelist exact cmdlets and even exact parameter values, not just "this user is an admin or not"
  • Every JEA session is logged via PowerShell transcription, giving you an audit trail of exactly what a constrained user ran