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-Clixml for 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

Current user info
whoami
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name
Check if running as admin
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
Get local users
Get-LocalUser | Select-Object Name, Enabled, LastLogon, PasswordLastSet
Get local groups
Get-LocalGroup | Select-Object Name, Description
Get Administrators group members
Get-LocalGroupMember -Group "Administrators"
Create local user
$password = ConvertTo-SecureString "P@ssw0rd!" -AsPlainText -Force
New-LocalUser -Name "testuser" -Password $password -FullName "Test User" -Description "Test account"
Add user to group
Add-LocalGroupMember -Group "Remote Desktop Users" -Member "testuser"
Remove user from group
Remove-LocalGroupMember -Group "Administrators" -Member "testuser"
Disable user account
Disable-LocalUser -Name "testuser"
Enable user account
Enable-LocalUser -Name "testuser"
Delete user account
Remove-LocalUser -Name "testuser"
Reset user password
$password = ConvertTo-SecureString "NewP@ssw0rd!" -AsPlainText -Force
Set-LocalUser -Name "testuser" -Password $password
Get logged on users
query user
Log off user session
logoff 2
Get user profile info
Get-CimInstance Win32_UserProfile | Select-Object LocalPath, LastUseTime, Special |
    Where-Object { -not $_.Special }
Get current user environment variables
Get-ChildItem Env: | Sort-Object Name
Get specific environment variable
$env:PATH -split ';'
Set environment variable (user)
[Environment]::SetEnvironmentVariable("MY_VAR", "my_value", "User")
Set environment variable (machine - Admin)
[Environment]::SetEnvironmentVariable("MY_VAR", "my_value", "Machine")