PowerShell Remoting
Remote execution and session management.
WinRM Configuration and Prerequisites
# Enable remoting (run as Administrator)
# Creates WinRM listener, opens firewall, starts WinRM service
Enable-PSRemoting -Force
# Check WinRM service status
Get-Service WinRM | Select-Object Status, StartType
# Verify WinRM listener configuration
winrm enumerate winrm/config/listener
# View current WinRM settings
Get-WSManInstance -ResourceURI winrm/config
# Configure WinRM for HTTPS (recommended for production)
# Step 1: Create certificate request
$hostname = [System.Net.Dns]::GetHostByName($env:COMPUTERNAME).HostName
$cert = New-SelfSignedCertificate -DnsName $hostname -CertStoreLocation Cert:\LocalMachine\My
# Step 2: Create HTTPS listener
New-WSManInstance -ResourceURI winrm/config/listener -SelectorSet @{
Transport = "HTTPS"
Address = "*"
} -ValueSet @{
Hostname = $hostname
CertificateThumbprint = $cert.Thumbprint
}
# Step 3: Open firewall for HTTPS (5986)
New-NetFirewallRule -Name "WinRM-HTTPS" -DisplayName "WinRM HTTPS" `
-Enabled True -Direction Inbound -Protocol TCP -LocalPort 5986 `
-Action Allow -Profile Domain,Private
# Verify listeners
Get-WSManInstance -ResourceURI winrm/config/listener -Enumerate |
Select-Object Transport, Address, Port, CertificateThumbprint
# TrustedHosts for workgroup environments (less secure)
# WARNING: Only use when domain trust isn't available
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "server1.domain.com,server2.domain.com"
# View TrustedHosts
Get-Item WSMan:\localhost\Client\TrustedHosts
# Allow all hosts (DANGEROUS - development only)
# Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*"
# Clear TrustedHosts
# Clear-Item WSMan:\localhost\Client\TrustedHosts
# Test WinRM connectivity before connecting
Test-WSMan -ComputerName server1.inside.domusdigitalis.dev
# Test with authentication
Test-WSMan -ComputerName home-dc01.inside.domusdigitalis.dev -Authentication Default
WinRM Ports:
-
5985 - HTTP (default, domain-joined systems)
-
5986 - HTTPS (recommended for non-domain)
Interactive Sessions (Enter-PSSession)
# Basic interactive session
Enter-PSSession -ComputerName home-dc01.inside.domusdigitalis.dev
# With explicit credentials
$cred = Get-Credential
Enter-PSSession -ComputerName home-dc01 -Credential $cred
# Using HTTPS
Enter-PSSession -ComputerName home-dc01 -UseSSL
# Skip certificate validation (self-signed certs)
$sessionOption = New-PSSessionOption -SkipCACheck -SkipCNCheck
Enter-PSSession -ComputerName home-dc01 -UseSSL -SessionOption $sessionOption
# Connect to specific configuration endpoint
Enter-PSSession -ComputerName home-dc01 -ConfigurationName "Microsoft.PowerShell"
# Connect using SSH (PowerShell 7+)
Enter-PSSession -HostName home-dc01 -UserName AdminErosado
# Exit interactive session
Exit-PSSession
# Or just type: exit
# Inside the session, your prompt changes:
# [home-dc01]: PS C:\Users\AdminErosado\Documents>
# Common operations inside session
[home-dc01]: PS> Get-Process | Sort-Object CPU -Descending | Select-Object -First 10
[home-dc01]: PS> Get-Service | Where-Object Status -eq Running
[home-dc01]: PS> Get-EventLog -LogName System -Newest 20
When to use Enter-PSSession:
-
Interactive troubleshooting on single server
-
Running commands that require interaction
-
Exploring remote system state
-
NOT for automation (use Invoke-Command instead)
Remote Command Execution (Invoke-Command)
# Run command on single computer
Invoke-Command -ComputerName home-dc01 -ScriptBlock {
Get-Service | Where-Object Status -eq Running | Measure-Object
}
# Run on multiple computers (parallel by default)
$servers = @("home-dc01", "ise-01", "vault-01")
Invoke-Command -ComputerName $servers -ScriptBlock {
[PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
Uptime = (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
FreeMemoryGB = [math]::Round((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory / 1MB, 2)
}
}
# Run on computers from file
$servers = Get-Content C:\Scripts\servers.txt
Invoke-Command -ComputerName $servers -ScriptBlock { hostname }
# Run on computers from AD
$servers = Get-ADComputer -Filter { OperatingSystem -like "*Server*" } |
Select-Object -ExpandProperty Name
Invoke-Command -ComputerName $servers -ScriptBlock { Get-WindowsFeature | Where-Object Installed }
# Pass variables to remote session using $using:
$serviceName = "NTDS"
Invoke-Command -ComputerName home-dc01 -ScriptBlock {
Get-Service -Name $using:serviceName
}
# Pass multiple variables
$logName = "Security"
$hours = 24
Invoke-Command -ComputerName home-dc01 -ScriptBlock {
Get-EventLog -LogName $using:logName -After (Get-Date).AddHours(-$using:hours) |
Where-Object EventID -eq 4624
}
# Run local script on remote computers
Invoke-Command -ComputerName $servers -FilePath C:\Scripts\Get-SystemInfo.ps1
# Throttle parallel execution (default 32)
Invoke-Command -ComputerName $servers -ThrottleLimit 10 -ScriptBlock {
# Intensive operations - limit to 10 concurrent
Get-ChildItem C:\ -Recurse -ErrorAction SilentlyContinue | Measure-Object
}
# AsJob for background execution
$job = Invoke-Command -ComputerName $servers -ScriptBlock {
# Long-running operation
Get-EventLog -LogName Security -Newest 10000
} -AsJob
# Check job status
Get-Job $job.Id
# Wait for job and get results
$results = $job | Wait-Job | Receive-Job
# With credentials
$cred = Get-Credential
Invoke-Command -ComputerName home-dc01 -Credential $cred -ScriptBlock {
Get-ADUser -Filter * | Measure-Object
}
# Error handling
Invoke-Command -ComputerName $servers -ScriptBlock {
Get-Service "NonExistent" -ErrorAction Stop
} -ErrorAction SilentlyContinue -ErrorVariable remoteErrors
# Check which computers failed
$remoteErrors | ForEach-Object {
Write-Warning "Failed on $($_.TargetObject): $($_.Exception.Message)"
}
Invoke-Command Output:
Results include PSComputerName property automatically - identifies source server.
PSSession Management
# Create persistent session (reusable connection)
$session = New-PSSession -ComputerName home-dc01
# Create multiple sessions
$sessions = New-PSSession -ComputerName home-dc01, ise-01, vault-01
# View sessions
Get-PSSession
# View session details
Get-PSSession | Select-Object Id, Name, ComputerName, State, Availability
# Use existing session
Invoke-Command -Session $session -ScriptBlock { Get-Process }
# Enter existing session
Enter-PSSession -Session $session
# Benefits of persistent sessions:
# 1. Variables persist between commands
Invoke-Command -Session $session -ScriptBlock { $data = Get-Process }
Invoke-Command -Session $session -ScriptBlock { $data | Measure-Object } # $data still exists!
# 2. Import remote modules to local session
$session = New-PSSession -ComputerName home-dc01
Import-PSSession -Session $session -Module ActiveDirectory
# Now AD cmdlets run remotely but appear local
Get-ADUser -Filter * # Actually runs on home-dc01
# 3. Copy files via session
Copy-Item -Path C:\Scripts\config.ps1 -Destination C:\Scripts\ -ToSession $session
Copy-Item -Path C:\Logs\app.log -Destination C:\LocalLogs\ -FromSession $session
# Name sessions for easier management
$dcSession = New-PSSession -ComputerName home-dc01 -Name "DC01-Admin"
$iseSession = New-PSSession -ComputerName ise-01 -Name "ISE-Mgmt"
# Get session by name
Get-PSSession -Name "DC01-Admin"
# Disconnect session (keeps running on server)
Disconnect-PSSession -Session $session
# Reconnect later (even from different client)
Connect-PSSession -ComputerName home-dc01 -Name "DC01-Admin"
# View disconnected sessions
Get-PSSession -ComputerName home-dc01 -State Disconnected
# Remove session
Remove-PSSession -Session $session
# Remove all sessions
Get-PSSession | Remove-PSSession
# Session with timeout
$sessionOption = New-PSSessionOption -IdleTimeout (New-TimeSpan -Hours 2).TotalMilliseconds
$session = New-PSSession -ComputerName home-dc01 -SessionOption $sessionOption
Session States:
-
Opened - Active, ready for commands
-
Disconnected - Running on server, not connected to client
-
Broken - Connection lost, cannot reconnect
-
Closed - Terminated
Credentials Handling
# Interactive prompt (masks password)
$cred = Get-Credential
# Prompt with pre-filled username
$cred = Get-Credential -UserName "INSIDE\AdminErosado" -Message "Enter admin password"
# Create credential from secure string (automation)
$password = ConvertTo-SecureString "P@ssw0rd" -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential("INSIDE\AdminErosado", $password)
# Read password from encrypted file (safer automation)
# Step 1: Save password once (run interactively)
Read-Host "Enter password" -AsSecureString |
ConvertFrom-SecureString |
Out-File C:\Scripts\Credentials\admin.txt
# Step 2: Read in automation scripts
$password = Get-Content C:\Scripts\Credentials\admin.txt | ConvertTo-SecureString
$cred = New-Object PSCredential("INSIDE\AdminErosado", $password)
# IMPORTANT: Encrypted password is tied to user AND machine
# Cannot be decrypted on different machine or by different user
# Export credential for same user on same machine
$cred | Export-Clixml -Path C:\Scripts\Credentials\admin.xml
# Import credential
$cred = Import-Clixml -Path C:\Scripts\Credentials\admin.xml
# Credential delegation (CredSSP) - for multi-hop scenarios
# WARNING: Credentials cached on intermediate server - security risk
# Step 1: Enable on client
Enable-WSManCredSSP -Role Client -DelegateComputer "*.inside.domusdigitalis.dev"
# Step 2: Enable on server
Enable-WSManCredSSP -Role Server
# Use CredSSP
Invoke-Command -ComputerName server1 -Credential $cred -Authentication CredSSP -ScriptBlock {
# Can access network resources from this session
Get-ChildItem \\nas-01\share
}
# Check CredSSP status
Get-WSManCredSSP
Credential Best Practices:
-
Never store plaintext passwords in scripts
-
Use
Export-Clixml/Import-Clixmlfor local automation -
Use secrets management (Azure Key Vault, HashiCorp Vault) for production
-
CredSSP only when absolutely necessary (double-hop problem)
CIM Sessions (Alternative to WMI)
# CIM (Common Information Model) - modern replacement for WMI
# Uses WS-Man (same as WinRM) by default
# Create CIM session
$cimSession = New-CimSession -ComputerName home-dc01
# Multiple computers
$cimSessions = New-CimSession -ComputerName home-dc01, ise-01, vault-01
# Query using CIM session
Get-CimInstance -CimSession $cimSession -ClassName Win32_OperatingSystem |
Select-Object CSName, Caption, LastBootUpTime
# Query across multiple sessions
Get-CimInstance -CimSession $cimSessions -ClassName Win32_LogicalDisk |
Where-Object DriveType -eq 3 |
Select-Object PSComputerName, DeviceID,
@{N='SizeGB';E={[math]::Round($_.Size/1GB,2)}},
@{N='FreeGB';E={[math]::Round($_.FreeSpace/1GB,2)}},
@{N='PercentFree';E={[math]::Round($_.FreeSpace/$_.Size*100,1)}}
# CIM with DCOM (for older systems without WinRM)
$dcomOption = New-CimSessionOption -Protocol Dcom
$cimSession = New-CimSession -ComputerName oldserver -SessionOption $dcomOption
# CIM with explicit credentials
$cred = Get-Credential
$cimSession = New-CimSession -ComputerName home-dc01 -Credential $cred
# Useful CIM queries
# Services
Get-CimInstance -CimSession $cimSession -ClassName Win32_Service |
Where-Object { $_.StartMode -eq 'Auto' -and $_.State -ne 'Running' }
# Installed software
Get-CimInstance -CimSession $cimSession -ClassName Win32_Product |
Select-Object Name, Version, Vendor |
Sort-Object Name
# Pending reboot check
Get-CimInstance -CimSession $cimSession -Namespace root\ccm\ClientSDK -ClassName CCM_ClientUtilities -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty DetermineifRebootPending
# Windows Update history
$updateSession = Get-CimInstance -CimSession $cimSession -ClassName Win32_QuickFixEngineering |
Sort-Object InstalledOn -Descending |
Select-Object -First 10 HotFixID, Description, InstalledOn
# Remove CIM session
Remove-CimSession -CimSession $cimSession
# Remove all CIM sessions
Get-CimSession | Remove-CimSession
CIM vs WMI:
-
CIM is PowerShell 3.0+ (WMI is legacy)
-
CIM uses WS-Man by default (WMI uses DCOM)
-
CIM supports multiple sessions natively
-
CIM cmdlets:
-CimInstance,-CimSession -
WMI cmdlets:
Get-WmiObject(deprecated)
SSH-Based Remoting (PowerShell 7+)
# SSH remoting - works cross-platform (Windows/Linux/macOS)
# Requires: PowerShell 7+, SSH server with PowerShell subsystem
# Configure SSH server for PowerShell remoting (server-side)
# Add to sshd_config:
# Subsystem powershell c:/progra~1/powershell/7/pwsh.exe -sshs -NoLogo -NoProfile
# Basic SSH session
Enter-PSSession -HostName home-dc01.inside.domusdigitalis.dev -UserName AdminErosado
# With SSH key authentication (recommended)
Enter-PSSession -HostName home-dc01 -UserName AdminErosado -KeyFilePath ~/.ssh/id_ed25519
# Invoke-Command over SSH
Invoke-Command -HostName home-dc01 -UserName AdminErosado -ScriptBlock {
Get-Process | Sort-Object CPU -Descending | Select-Object -First 5
}
# Multiple hosts over SSH
$hosts = @(
@{ HostName = "home-dc01"; UserName = "AdminErosado" }
@{ HostName = "vault-01.inside.domusdigitalis.dev"; UserName = "evanusmodestus" }
)
Invoke-Command -SSHConnection $hosts -ScriptBlock {
[PSCustomObject]@{
Host = hostname
OS = $PSVersionTable.OS
PSVersion = $PSVersionTable.PSVersion
}
}
# Create SSH session
$session = New-PSSession -HostName home-dc01 -UserName AdminErosado
# Mix WinRM and SSH sessions
$winrmSession = New-PSSession -ComputerName server1
$sshSession = New-PSSession -HostName server2 -UserName admin
Invoke-Command -Session $winrmSession, $sshSession -ScriptBlock {
"Running on: $env:COMPUTERNAME via $($PSVersionTable.PSRemotingProtocolVersion)"
}
SSH vs WinRM:
| Feature | WinRM | SSH | |---------|-------|-----| | Windows→Windows | ✓ | ✓ | | Windows→Linux | ✗ | ✓ | | Linux→Windows | ✗ | ✓ | | Authentication | Kerberos/NTLM | Keys/Password | | Default Port | 5985/5986 | 22 | | CredSSP (double-hop) | ✓ | ✗ |
Just Enough Administration (JEA)
# JEA limits what users can do via remoting
# Principle of least privilege for remote administration
# Step 1: Create role capability file (what commands are allowed)
# Save as: C:\JEA\Roles\DNSAdmin.psrc
New-PSRoleCapabilityFile -Path C:\JEA\Roles\DNSAdmin.psrc -ModulesToImport DnsServer -VisibleCmdlets @(
'Get-DnsServer',
'Get-DnsServerZone',
'Get-DnsServerResourceRecord',
@{
Name = 'Add-DnsServerResourceRecordA'
Parameters = @{ Name = 'ZoneName'; ValidateSet = 'inside.domusdigitalis.dev' }
}
) -VisibleFunctions @(
'Get-Date',
'Write-Output'
)
# Step 2: Create session configuration file
# Save as: C:\JEA\Config\DNSAdmin.pssc
New-PSSessionConfigurationFile -Path C:\JEA\Config\DNSAdmin.pssc `
-SessionType RestrictedRemoteServer `
-RunAsVirtualAccount `
-RoleDefinitions @{
'INSIDE\DNS-Admins' = @{ RoleCapabilities = 'DNSAdmin' }
} `
-TranscriptDirectory C:\JEA\Transcripts `
-LogDirectory C:\JEA\Logs
# Step 3: Register configuration
Register-PSSessionConfiguration -Name DNSAdmin -Path C:\JEA\Config\DNSAdmin.pssc -Force
# Step 4: Connect using JEA endpoint
Enter-PSSession -ComputerName dns-server -ConfigurationName DNSAdmin
# Users can only run allowed commands
# Attempts to run other commands fail
# List available commands in JEA session
Get-Command
# View JEA configurations on server
Get-PSSessionConfiguration | Where-Object { $_.PSObject.Properties['RoleDefinitions'] }
# Test JEA configuration
Test-PSSessionConfigurationFile -Path C:\JEA\Config\DNSAdmin.pssc
# Unregister JEA endpoint
Unregister-PSSessionConfiguration -Name DNSAdmin -Force
# View transcripts (audit trail)
Get-ChildItem C:\JEA\Transcripts | Sort-Object LastWriteTime -Descending | Select-Object -First 5
JEA Best Practices:
-
Run as virtual account (no real credentials exposed)
-
Always enable transcription (audit trail)
-
Whitelist specific cmdlets, not modules
-
Use parameter constraints for sensitive operations
Infrastructure Patterns
# Server inventory with parallel queries
$servers = Get-ADComputer -Filter { OperatingSystem -like "*Server*" } |
Select-Object -ExpandProperty Name
$inventory = Invoke-Command -ComputerName $servers -ThrottleLimit 20 -ScriptBlock {
$os = Get-CimInstance Win32_OperatingSystem
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
$disk = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'"
[PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
OS = $os.Caption
LastBoot = $os.LastBootUpTime
CPUCores = $cpu.NumberOfCores
MemoryGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 1)
DiskFreeGB = [math]::Round($disk.FreeSpace / 1GB, 1)
DiskTotalGB = [math]::Round($disk.Size / 1GB, 1)
}
} -ErrorVariable failedServers -ErrorAction SilentlyContinue
# Export inventory
$inventory | Export-Csv -Path C:\Reports\ServerInventory.csv -NoTypeInformation
# Report failed servers
$failedServers | ForEach-Object {
Write-Warning "Failed: $($_.TargetObject) - $($_.Exception.Message)"
}
# Windows Update compliance check
$updateResults = Invoke-Command -ComputerName $servers -ScriptBlock {
$updates = (New-Object -ComObject Microsoft.Update.Session).CreateUpdateSearcher()
$pending = $updates.Search("IsInstalled=0 and Type='Software'").Updates
[PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
PendingUpdates = $pending.Count
CriticalUpdates = ($pending | Where-Object MsrcSeverity -eq "Critical").Count
LastChecked = Get-Date
}
}
$updateResults | Where-Object PendingUpdates -gt 0 |
Sort-Object CriticalUpdates -Descending
# Service health check across servers
$services = @("NTDS", "DNS", "Kerberos", "W32Time")
$healthCheck = Invoke-Command -ComputerName $servers -ScriptBlock {
param($svcList)
foreach ($svc in $svcList) {
$service = Get-Service -Name $svc -ErrorAction SilentlyContinue
[PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
Service = $svc
Status = if ($service) { $service.Status } else { "NotFound" }
StartType = if ($service) { $service.StartType } else { "N/A" }
}
}
} -ArgumentList (,$services)
$healthCheck | Where-Object Status -ne "Running" | Format-Table
# Bulk certificate expiration check
$certResults = Invoke-Command -ComputerName $servers -ScriptBlock {
Get-ChildItem Cert:\LocalMachine\My |
Where-Object { $_.NotAfter -lt (Get-Date).AddDays(30) } |
Select-Object @{N='ComputerName';E={$env:COMPUTERNAME}},
Subject, Thumbprint,
@{N='ExpiresIn';E={($_.NotAfter - (Get-Date)).Days}}
}
$certResults | Sort-Object ExpiresIn | Format-Table
# Event log collection (security audit)
$startTime = (Get-Date).AddDays(-1)
$securityEvents = Invoke-Command -ComputerName $servers -ScriptBlock {
param($start)
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4625, 4648, 4624 # Failed/Explicit/Successful logons
StartTime = $start
} -MaxEvents 100 -ErrorAction SilentlyContinue |
Select-Object TimeCreated, Id,
@{N='Computer';E={$env:COMPUTERNAME}},
@{N='User';E={$_.Properties[5].Value}},
@{N='Source';E={$_.Properties[18].Value}}
} -ArgumentList $startTime
# Failed logon report
$securityEvents | Where-Object Id -eq 4625 |
Group-Object User |
Sort-Object Count -Descending |
Select-Object Name, Count
# Disk space alert script
$threshold = 10 # GB
$lowDiskServers = Invoke-Command -ComputerName $servers -ScriptBlock {
param($thresh)
Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" |
Where-Object { ($_.FreeSpace / 1GB) -lt $thresh } |
Select-Object @{N='ComputerName';E={$env:COMPUTERNAME}},
DeviceID,
@{N='FreeGB';E={[math]::Round($_.FreeSpace/1GB,1)}},
@{N='TotalGB';E={[math]::Round($_.Size/1GB,1)}}
} -ArgumentList $threshold
if ($lowDiskServers) {
$lowDiskServers | Format-Table
# Send-MailMessage -To admin@domain.com -Subject "Low Disk Alert" ...
}
Troubleshooting Remoting
# Test WinRM connectivity
Test-WSMan -ComputerName home-dc01
# Detailed connection test
Test-NetConnection -ComputerName home-dc01 -Port 5985 -InformationLevel Detailed
# Check WinRM service
Invoke-Command -ComputerName home-dc01 -ScriptBlock {
Get-Service WinRM | Select-Object Status, StartType
} -ErrorAction SilentlyContinue
# Common error: "Access is denied"
# Solutions:
# 1. User must be local admin on remote machine
# 2. Check UAC remote restrictions
Get-ItemProperty HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System -Name LocalAccountTokenFilterPolicy -ErrorAction SilentlyContinue
# Fix: Allow remote local admin access
Set-ItemProperty HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System -Name LocalAccountTokenFilterPolicy -Value 1
# Common error: "The WinRM client cannot process the request"
# Check TrustedHosts
Get-Item WSMan:\localhost\Client\TrustedHosts
# Add server to TrustedHosts
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "server1,server2" -Force
# Common error: "The WinRM service is not running"
# Fix on remote server:
Get-Service WinRM | Start-Service
Set-Service WinRM -StartupType Automatic
# Common error: "The SSL certificate is invalid"
# Option 1: Use HTTP instead of HTTPS
Enter-PSSession -ComputerName home-dc01 -Port 5985
# Option 2: Skip certificate validation
$sessionOption = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
Enter-PSSession -ComputerName home-dc01 -UseSSL -SessionOption $sessionOption
# Firewall check
Invoke-Command -ComputerName home-dc01 -ScriptBlock {
Get-NetFirewallRule -Name "WINRM-HTTP-In-TCP*" |
Select-Object Name, Enabled, Direction, Action
}
# Enable firewall rules for WinRM
Enable-NetFirewallRule -Name "WINRM-HTTP-In-TCP"
Enable-NetFirewallRule -Name "WINRM-HTTP-In-TCP-PUBLIC"
# Kerberos issues (domain joined)
# Check SPN registration
setspn -L home-dc01
# Check time sync (Kerberos requires <5 min skew)
Invoke-Command -ComputerName home-dc01 -ScriptBlock { Get-Date }
Get-Date
# View WinRM configuration
winrm get winrm/config
# Reset WinRM to defaults
winrm quickconfig -force
# Diagnostic trace
$traceFile = "C:\temp\winrm-trace.etl"
Start-Trace -Name WinRM -OutputFile $traceFile -Provider Microsoft-Windows-WinRM
# Reproduce issue
Stop-Trace -Name WinRM
# View WinRM event logs
Get-WinEvent -LogName Microsoft-Windows-WinRM/Operational -MaxEvents 20 |
Format-Table TimeCreated, LevelDisplayName, Message -Wrap
Common Gotchas
# WRONG: Variables don't pass to remote session automatically
$service = "NTDS"
Invoke-Command -ComputerName home-dc01 -ScriptBlock {
Get-Service $service # ERROR: $service is $null
}
# CORRECT: Use $using: scope modifier
$service = "NTDS"
Invoke-Command -ComputerName home-dc01 -ScriptBlock {
Get-Service $using:service
}
# WRONG: Passing arrays without unrolling
$services = @("NTDS", "DNS", "Kerberos")
Invoke-Command -ComputerName home-dc01 -ArgumentList $services -ScriptBlock {
param($svcList)
# $svcList is just "NTDS", not the full array!
}
# CORRECT: Wrap array in array
Invoke-Command -ComputerName home-dc01 -ArgumentList (,$services) -ScriptBlock {
param($svcList)
# $svcList is @("NTDS", "DNS", "Kerberos")
}
# WRONG: Expecting local module to work remotely
Import-Module DnsServer
Invoke-Command -ComputerName home-dc01 -ScriptBlock {
Get-DnsServerZone # ERROR: Module not loaded in remote session
}
# CORRECT: Import module inside script block
Invoke-Command -ComputerName home-dc01 -ScriptBlock {
Import-Module DnsServer
Get-DnsServerZone
}
# WRONG: Session dies between commands
$session = New-PSSession -ComputerName home-dc01
Invoke-Command -ComputerName home-dc01 -ScriptBlock { $x = 1 } # Different session!
Invoke-Command -ComputerName home-dc01 -ScriptBlock { $x } # $null
# CORRECT: Reuse the same session
$session = New-PSSession -ComputerName home-dc01
Invoke-Command -Session $session -ScriptBlock { $x = 1 }
Invoke-Command -Session $session -ScriptBlock { $x } # Returns 1
# WRONG: Output objects lose type fidelity
Invoke-Command -ComputerName home-dc01 -ScriptBlock {
Get-Process
} | Get-Member # Deserialized objects - some methods missing
# Deserialized objects:
# - Properties work
# - Methods may not work (no live connection to remote object)
# - TypeName becomes "Deserialized.System.Diagnostics.Process"
# CORRECT: Do operations remotely if you need methods
Invoke-Command -ComputerName home-dc01 -ScriptBlock {
Get-Process notepad | Stop-Process # Works - method called remotely
}
# WRONG: Double-hop fails without CredSSP
Invoke-Command -ComputerName server1 -ScriptBlock {
Get-ChildItem \\fileserver\share # Fails - no credential delegation
}
# CORRECT: Use CredSSP (security risk) or copy files first
# Option 1: CredSSP
Enable-WSManCredSSP -Role Client -DelegateComputer server1
Invoke-Command -ComputerName server1 -Authentication CredSSP -Credential $cred -ScriptBlock {
Get-ChildItem \\fileserver\share
}
# Option 2: Copy files through session
$session = New-PSSession -ComputerName server1
Copy-Item -Path \\fileserver\share\file.txt -Destination C:\temp\ -ToSession $session
User Sessions
whoami
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
Get-LocalUser | Select-Object Name, Enabled, LastLogon, PasswordLastSet
Get-LocalGroup | Select-Object Name, Description
Get-LocalGroupMember -Group "Administrators"
$password = ConvertTo-SecureString "P@ssw0rd!" -AsPlainText -Force
New-LocalUser -Name "testuser" -Password $password -FullName "Test User" -Description "Test account"
Add-LocalGroupMember -Group "Remote Desktop Users" -Member "testuser"
Remove-LocalGroupMember -Group "Administrators" -Member "testuser"
Disable-LocalUser -Name "testuser"
Enable-LocalUser -Name "testuser"
Remove-LocalUser -Name "testuser"
$password = ConvertTo-SecureString "NewP@ssw0rd!" -AsPlainText -Force
Set-LocalUser -Name "testuser" -Password $password
query user
logoff 2
Get-CimInstance Win32_UserProfile | Select-Object LocalPath, LastUseTime, Special |
Where-Object { -not $_.Special }
Get-ChildItem Env: | Sort-Object Name
$env:PATH -split ';'
[Environment]::SetEnvironmentVariable("MY_VAR", "my_value", "User")
[Environment]::SetEnvironmentVariable("MY_VAR", "my_value", "Machine")