Introduction to PowerShell Remoting for Server Management

PowerShell Remoting is the built-in mechanism for executing commands and scripts on remote Windows machines. It uses the WS-Management protocol (WinRM) and provides a secure, scriptable channel for managing fleets of Windows Server 2022 machines from a single administrative workstation or CI/CD agent. This guide covers everything from multi-server command execution to JEA constrained endpoints and integration with build pipelines.

Enabling PowerShell Remoting

On each target server, enable remoting with a single command run from an elevated PowerShell session:

Enable-PSRemoting -Force

This command starts the WinRM service, sets it to automatic startup, creates the default HTTP listener on port 5985, and adds the necessary firewall rule. On Windows Server 2022 the firewall rule is added automatically. On machines joined to a domain, the WinRM service is often already running via Group Policy. Verify the listener is active:

winrm enumerate winrm/config/listener
Get-WSManInstance -ResourceURI winrm/config/listener -Enumerate

For workgroup environments (non-domain) add the target machines to the TrustedHosts list on the management machine:

Set-Item WSMan:localhostClientTrustedHosts -Value "192.168.1.10,192.168.1.11,webserver01" -Force

Running Commands on Multiple Servers with Invoke-Command

The most common remoting pattern is using Invoke-Command with an array of computer names. PowerShell fans out the command to all targets simultaneously (up to the throttle limit, default 32):

$computers = @("web01", "web02", "web03", "app01", "app02")

Invoke-Command -ComputerName $computers -ScriptBlock {
    Get-Service W3SVC | Select Name, Status, StartType
} | Sort-Object PSComputerName | Format-Table -AutoSize

Each result object includes a PSComputerName property so you can identify which server returned which data. To run a more complex script block that installs a Windows feature across all app servers:

$appServers = @("app01", "app02", "app03")

Invoke-Command -ComputerName $appServers -ScriptBlock {
    $result = Install-WindowsFeature -Name NET-Framework-45-Core -IncludeManagementTools
    [PSCustomObject]@{
        Server      = $env:COMPUTERNAME
        Success     = $result.Success
        RestartNeeded = $result.RestartNeeded
        FeatureResult = $result.FeatureResult.Name -join ", "
    }
}

Reusing Sessions with New-PSSession

Each Invoke-Command -ComputerName call creates and tears down a session. For multiple operations on the same servers, create persistent sessions with New-PSSession and reuse them:

$cred = Get-Credential -UserName "DOMAINAdminUser" -Message "Enter admin credentials"
$computers = @("web01", "web02", "web03")

# Create persistent sessions
$sessions = New-PSSession -ComputerName $computers -Credential $cred

# Run multiple commands using the same sessions (no repeated authentication)
Invoke-Command -Session $sessions -ScriptBlock {
    Stop-Service W3SVC -Force
}

Invoke-Command -Session $sessions -ScriptBlock {
    # Deploy operation here
    Copy-Item -Path "C:deploystaging*" -Destination "C:inetpubwwwrootapp" -Recurse -Force
}

Invoke-Command -Session $sessions -ScriptBlock {
    Start-Service W3SVC
}

# Always clean up sessions when done
Remove-PSSession -Session $sessions

Persistent sessions maintain state between commands. Variables set in one Invoke-Command call on a session are available in the next call on the same session.

Copying Files Over PS Remoting with Copy-Item

PowerShell 5.0 and later supports Copy-Item over PS sessions, eliminating the need for SMB shares or additional tools to transfer files during remote management operations:

$session = New-PSSession -ComputerName "web01" -Credential $cred

# Copy local file to remote machine
Copy-Item -Path "C:scriptsdeploy.ps1" -Destination "C:scripts" -ToSession $session

# Copy entire directory to remote machine
Copy-Item -Path "C:buildMyApppublish" -Destination "C:deploystaging" `
    -ToSession $session -Recurse -Force

# Copy file FROM remote machine to local
Copy-Item -Path "C:logsapplication.log" -Destination "C:collected-logsweb01-app.log" `
    -FromSession $session

Remove-PSSession $session

Using $using: Scope for Variables in Remote Script Blocks

Variables defined locally on the management machine are not automatically available inside remote script blocks. Use the $using: scope modifier to pass local variables into a remote context:

$appVersion = "2.4.1"
$deployPath = "C:inetpubwwwrootMyApp"
$serviceName = "MyAppService"
$computers = @("app01", "app02")

Invoke-Command -ComputerName $computers -ScriptBlock {
    Write-Host "$env:COMPUTERNAME: Deploying version $using:appVersion to $using:deployPath"

    Stop-Service -Name $using:serviceName -Force -ErrorAction SilentlyContinue

    # Use the variable in path operations
    $backupPath = "$using:deployPath.backup_$(Get-Date -Format yyyyMMddHHmm)"
    if (Test-Path $using:deployPath) {
        Move-Item $using:deployPath $backupPath
    }

    Start-Service -Name $using:serviceName
}

Parallel Execution with ForEach-Object -Parallel in PowerShell 7

PowerShell 7 (available on Windows Server 2022 as a separate install alongside Windows PowerShell 5.1) adds the -Parallel parameter to ForEach-Object. This enables true thread-based parallel execution without remoting overhead for local operations, or can be combined with remoting:

# Install PowerShell 7 on Windows Server 2022
winget install --id Microsoft.PowerShell --source winget

# Parallel disk space check across many servers (PS 7)
$servers = Get-Content "C:configserver-list.txt"

$results = $servers | ForEach-Object -Parallel {
    $server = $_
    try {
        $disk = Invoke-Command -ComputerName $server -ScriptBlock {
            Get-PSDrive C | Select-Object Used, Free
        } -ErrorAction Stop
        [PSCustomObject]@{
            Server    = $server
            UsedGB    = [math]::Round($disk.Used / 1GB, 2)
            FreeGB    = [math]::Round($disk.Free / 1GB, 2)
            Status    = "OK"
        }
    } catch {
        [PSCustomObject]@{
            Server    = $server
            UsedGB    = 0
            FreeGB    = 0
            Status    = "UNREACHABLE: $_"
        }
    }
} -ThrottleLimit 20 -TimeoutSeconds 30

$results | Sort-Object FreeGB | Format-Table -AutoSize

Running Scripts on Remote Hosts

To run a local script file on one or more remote machines, use Invoke-Command -FilePath. The script file is copied to the remote machine’s temp directory, executed, and the output returned:

Invoke-Command -ComputerName "web01","web02" -FilePath "C:scriptshealth-check.ps1" -Credential $cred

Alternatively, use a script block with a here-string for inline scripts that are long but don’t warrant a separate file:

$scriptBlock = [ScriptBlock]::Create((Get-Content "C:scriptsdeploy-step.ps1" -Raw))
Invoke-Command -Session $sessions -ScriptBlock $scriptBlock

Handling Credentials Securely

Never hardcode credentials in scripts. Use one of these patterns depending on context:

Interactive prompt (manual runs):

$cred = Get-Credential -Message "Enter admin credentials for production servers"

Encrypted credential file (scheduled tasks, non-pipeline automation):

# Create the encrypted file (must be done by the same user/machine that will use it)
$cred = Get-Credential
$cred.Password | ConvertFrom-SecureString | Set-Content "C:secureadmin.cred"

# Load it in the script
$password = Get-Content "C:secureadmin.cred" | ConvertTo-SecureString
$cred = New-Object PSCredential("DOMAINAdminUser", $password)

Windows Credential Manager (cmdkey.exe):

cmdkey /add:web01 /user:DOMAINAdminUser /pass:YourPassword
# WinRM will automatically use stored credentials for that target

CI/CD secret injection — retrieve from environment variable:

$password = ConvertTo-SecureString $env:DEPLOY_PASSWORD -AsPlainText -Force
$cred = New-Object PSCredential($env:DEPLOY_USER, $password)

PowerShell Remoting Over HTTPS

HTTP remoting (port 5985) transmits the Kerberos/NTLM authentication ticket encrypted, but creating an HTTPS listener (port 5986) adds TLS encryption for the entire session. This is required in workgroup environments and recommended across untrusted networks:

# On the target server - create a self-signed cert
$cert = New-SelfSignedCertificate -DnsName "web01.yourdomain.com" -CertStoreLocation Cert:LocalMachineMy

# Create the HTTPS listener
$thumbprint = $cert.Thumbprint
winrm create winrm/config/listener?Address=*+Transport=HTTPS @{Hostname="web01.yourdomain.com"; CertificateThumbprint="$thumbprint"}

# Or using PowerShell cmdlets
New-WSManInstance -ResourceURI "winrm/config/listener" -SelectorSet @{Address="*";Transport="HTTPS"} `
    -ValueSet @{Hostname="web01.yourdomain.com"; CertificateThumbprint=$thumbprint}

# Open the firewall port
New-NetFirewallRule -DisplayName "PSRemoting HTTPS" -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow

Connect using HTTPS from the management machine:

$sessionOption = New-PSSessionOption -SkipCACheck  # For self-signed certs only
$session = New-PSSession -ComputerName "web01.yourdomain.com" -UseSSL -SessionOption $sessionOption -Credential $cred

Just Enough Administration (JEA)

JEA creates constrained PS remoting endpoints where users can only run specific allowed commands, even if they connect with elevated credentials. This is ideal for helpdesk staff who need to restart services or read logs without full admin access. Create a Role Capability file:

New-Item -Path "C:JEARoleCapabilities" -ItemType Directory -Force

New-PSRoleCapabilityFile -Path "C:JEARoleCapabilitiesWebOps.psrc" `
    -VisibleCmdlets @{
        Name = "Restart-Service"
        Parameters = @{ Name = "Name"; ValidateSet = "W3SVC","AppFabricWorkflowManagementService" }
    },
    "Get-Service",
    "Get-EventLog",
    "Get-Process" `
    -VisibleExternalCommands "C:WindowsSystem32iisreset.exe"

Create a Session Configuration file and register the endpoint:

New-PSSessionConfigurationFile -Path "C:JEAWebOpsJEA.pssc" `
    -SessionType RestrictedRemoteServer `
    -RunAsVirtualAccount `
    -RoleDefinitions @{
        "DOMAINWebOpsTeam" = @{ RoleCapabilities = "WebOps" }
    }

Register-PSSessionConfiguration -Name "WebOpsJEA" `
    -Path "C:JEAWebOpsJEA.pssc" `
    -Force

# Users in WebOpsTeam connect to this limited endpoint
Enter-PSSession -ComputerName web01 -ConfigurationName WebOpsJEA -Credential $webOpsCred

Transcript Logging for Remote Sessions

Enable transcript logging on the session configuration to record all commands and output for compliance and auditing. Add the TranscriptDirectory to the session configuration file:

New-PSSessionConfigurationFile -Path "C:JEAWebOpsJEA.pssc" `
    -SessionType RestrictedRemoteServer `
    -RunAsVirtualAccount `
    -TranscriptDirectory "C:PSTransWebOps" `
    -RoleDefinitions @{ "DOMAINWebOpsTeam" = @{ RoleCapabilities = "WebOps" } }

Enable module logging and script block logging via Group Policy or registry for all sessions:

# Enable script block logging
$regPath = "HKLM:SOFTWAREPoliciesMicrosoftWindowsPowerShellScriptBlockLogging"
New-Item -Path $regPath -Force
Set-ItemProperty -Path $regPath -Name EnableScriptBlockLogging -Value 1

# Enable transcription for all PS sessions
$regPath2 = "HKLM:SOFTWAREPoliciesMicrosoftWindowsPowerShellTranscription"
New-Item -Path $regPath2 -Force
Set-ItemProperty -Path $regPath2 -Name EnableTranscripting -Value 1
Set-ItemProperty -Path $regPath2 -Name OutputDirectory -Value "C:PSTransAllSessions"

PowerShell Remoting in CI/CD Pipelines

From a GitHub Actions self-hosted runner on Windows, a deployment script using PS remoting looks like this:

# In GitHub Actions workflow
- name: Deploy via PowerShell Remoting
  shell: pwsh
  run: |
    $password = ConvertTo-SecureString "${{ secrets.DEPLOY_PASS }}" -AsPlainText -Force
    $cred = New-Object PSCredential("${{ secrets.DEPLOY_USER }}", $password)
    $servers = @("web01","web02")
    $sessions = New-PSSession -ComputerName $servers -Credential $cred -UseSSL -SessionOption (New-PSSessionOption -SkipCACheck)
    try {
        Copy-Item -Path ".publish*" -Destination "C:deploystaging" -ToSession $sessions -Recurse -Force
        Invoke-Command -Session $sessions -ScriptBlock {
            Stop-WebSite -Name "MyApp"
            Remove-Item -Path "C:inetpubwwwrootMyApp*" -Recurse -Force
            Copy-Item -Path "C:deploystaging*" -Destination "C:inetpubwwwrootMyApp" -Recurse -Force
            Start-WebSite -Name "MyApp"
        }
    } finally {
        Remove-PSSession $sessions
    }

Summary

PowerShell Remoting on Windows Server 2022 is the backbone of automated server management at scale. Using Invoke-Command for one-off operations, persistent sessions for multi-step workflows, $using: scope for variable passing, and PS 7’s -Parallel for throughput, administrators can manage dozens of servers as efficiently as one. Layering JEA for least-privilege access, HTTPS listeners for network security, and transcript logging for auditability creates a remoting architecture that satisfies both operational and compliance requirements.