P08: Remote Server Management Tool

P08: Remote Server Management Tool

Project Overview

What youโ€™ll build: A comprehensive tool for managing multiple Windows serversโ€”check service status, deploy configuration files, restart services, collect logsโ€”all from your workstation. This is the backbone of enterprise Windows administration.

Attribute Value
Difficulty Level 3: Advanced
Time Estimate 2 weeks
Programming Language PowerShell
Knowledge Area Windows Administration, Networking, Security
Prerequisites Intermediate PowerShell, access to test servers/VMs
Coolness Level Level 1: Pure Corporate Snoozefest
Business Potential 3. The โ€œService & Supportโ€ Model
Main Book โ€œPowerShell in Depthโ€ by Don Jones et al.

Learning Objectives

After completing this project, you will be able to:

  1. Master PowerShell Remoting - Understand the complete WinRM/WSMan architecture and how remote execution actually works under the hood
  2. Manage PSSession lifecycles - Create, reuse, monitor, and cleanup persistent sessions efficiently across server fleets
  3. Execute commands in parallel - Run operations on dozens or hundreds of machines simultaneously with proper throttling
  4. Handle credentials securely - Implement proper credential storage, delegation, and avoid common security pitfalls like CredSSP misuse
  5. Transfer files remotely - Copy configuration files, scripts, and logs to/from remote machines over remoting sessions
  6. Build resilient distributed tools - Handle network failures, timeouts, partial failures, and aggregate errors meaningfully
  7. Design for scale - Structure code that works for 3 servers and 300 servers without refactoring
  8. Implement enterprise logging - Track every operation for audit trails and troubleshooting

Deep Theoretical Foundation

PowerShell Remoting Architecture: WinRM and WSMan

PowerShell Remoting is built on top of WS-Management (WS-Man), an industry-standard SOAP-based protocol for managing systems. Microsoftโ€™s implementation is the Windows Remote Management (WinRM) service.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    PowerShell Remoting Architecture                          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                              โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”‚
โ”‚   โ”‚   Your Workstation   โ”‚                    โ”‚    Remote Server     โ”‚        โ”‚
โ”‚   โ”‚                       โ”‚                    โ”‚                       โ”‚        โ”‚
โ”‚   โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚    HTTPS/5986      โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚        โ”‚
โ”‚   โ”‚  โ”‚ PowerShell.exe  โ”‚ โ”‚    HTTP/5985       โ”‚  โ”‚  WinRM Service  โ”‚ โ”‚        โ”‚
โ”‚   โ”‚  โ”‚                 โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚  โ”‚                 โ”‚ โ”‚        โ”‚
โ”‚   โ”‚  โ”‚ Invoke-Command  โ”‚ โ”‚   WS-Man/SOAP      โ”‚  โ”‚ Listener        โ”‚ โ”‚        โ”‚
โ”‚   โ”‚  โ”‚ Enter-PSSession โ”‚ โ”‚   + Encryption     โ”‚  โ”‚    โ”‚            โ”‚ โ”‚        โ”‚
โ”‚   โ”‚  โ”‚ New-PSSession   โ”‚ โ”‚                    โ”‚  โ”‚    v            โ”‚ โ”‚        โ”‚
โ”‚   โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚                    โ”‚  โ”‚ wsmprovhost.exe โ”‚ โ”‚        โ”‚
โ”‚   โ”‚          โ”‚           โ”‚                    โ”‚  โ”‚ (Host Process)  โ”‚ โ”‚        โ”‚
โ”‚   โ”‚          v           โ”‚                    โ”‚  โ”‚    โ”‚            โ”‚ โ”‚        โ”‚
โ”‚   โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚                    โ”‚  โ”‚    v            โ”‚ โ”‚        โ”‚
โ”‚   โ”‚  โ”‚ WSMan Provider  โ”‚ โ”‚                    โ”‚  โ”‚ PowerShell.exe  โ”‚ โ”‚        โ”‚
โ”‚   โ”‚  โ”‚ (Client-side)   โ”‚ โ”‚                    โ”‚  โ”‚ (Your Script)   โ”‚ โ”‚        โ”‚
โ”‚   โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚                    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚        โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ”‚
โ”‚                                                                              โ”‚
โ”‚   Protocol Layers:                                                           โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚   โ”‚ Application:  PowerShell Remoting Protocol (MS-PSRP)                โ”‚   โ”‚
โ”‚   โ”‚ Transport:    WS-Management (SOAP messages)                          โ”‚   โ”‚
โ”‚   โ”‚ Security:     Kerberos, NTLM, or Certificate Authentication         โ”‚   โ”‚
โ”‚   โ”‚ Encryption:   HTTP + Message Encryption OR HTTPS (TLS)              โ”‚   โ”‚
โ”‚   โ”‚ Network:      TCP 5985 (HTTP) or 5986 (HTTPS)                       โ”‚   โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Why WS-Man over DCOM?

The legacy approach (used by WMIโ€™s Get-WmiObject) used DCOM (Distributed COM), which is:

  • Firewall-hostile (uses dynamic port ranges)
  • Difficult to secure
  • Complex to troubleshoot
  • Not designed for modern distributed systems

WS-Man uses standard HTTPS/HTTP, traverses firewalls cleanly, and provides predictable behavior.

The WinRM Service

On every Windows machine, the WinRM service:

  1. Listens on configured ports (default 5985/5986)
  2. Authenticates incoming connections
  3. Spawns a host process (wsmprovhost.exe) for each session
  4. Manages session lifecycle, timeouts, and resource limits

Understanding the Security Model

Authentication Flow:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                                                                         โ”‚
โ”‚   1. Client connects to WinRM listener                                  โ”‚
โ”‚      โ”‚                                                                  โ”‚
โ”‚      v                                                                  โ”‚
โ”‚   2. Authentication negotiation (SPNEGO)                                โ”‚
โ”‚      โ”œโ”€> Domain environment: Kerberos preferred                         โ”‚
โ”‚      โ””โ”€> Workgroup: NTLM fallback                                       โ”‚
โ”‚          โ”‚                                                              โ”‚
โ”‚          v                                                              โ”‚
โ”‚   3. Authorization check                                                โ”‚
โ”‚      โ”œโ”€> User must be in local Administrators OR                        โ”‚
โ”‚      โ””โ”€> User must be in Remote Management Users group                  โ”‚
โ”‚          โ”‚                                                              โ”‚
โ”‚          v                                                              โ”‚
โ”‚   4. Session established with user's security context                   โ”‚
โ”‚      โ””โ”€> Commands run as the authenticated user                         โ”‚
โ”‚                                                                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Session Management: Enter-PSSession, Invoke-Command, PSSessions

PowerShell provides three distinct approaches to remote execution:

1. Enter-PSSession (Interactive)

# Opens an interactive remote shell
Enter-PSSession -ComputerName Server01

# Your prompt changes:
[Server01]: PS C:\Users\Admin> Get-Process
[Server01]: PS C:\Users\Admin> exit  # Returns to local session

When to use: Troubleshooting, exploration, one-off commands. NOT for automation.

2. Invoke-Command with -ComputerName (Implicit Session)

# Creates connection, runs command, closes connection
Invoke-Command -ComputerName Server01 -ScriptBlock {
    Get-Service -Name "Spooler"
}

Lifecycle:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Connection   โ”‚ --> โ”‚ Command      โ”‚ --> โ”‚ Connection   โ”‚
โ”‚ Established  โ”‚     โ”‚ Execution    โ”‚     โ”‚ Closed       โ”‚
โ”‚ (~500ms)     โ”‚     โ”‚ (~variable)  โ”‚     โ”‚ (~100ms)     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

When to use: Single commands, simple scripts, when session state doesnโ€™t matter.

3. Explicit PSSessions (Persistent)

# Create sessions to multiple servers
$sessions = New-PSSession -ComputerName Server01, Server02, Server03

# Reuse sessions for multiple operations
Invoke-Command -Session $sessions -ScriptBlock { Get-Service }
Invoke-Command -Session $sessions -ScriptBlock { Get-Process }
Invoke-Command -Session $sessions -ScriptBlock { Get-EventLog -LogName System -Newest 10 }

# Copy files (requires sessions)
Copy-Item -Path ".\config.xml" -Destination "C:\App\config.xml" -ToSession $sessions[0]

# Clean up
Remove-PSSession $sessions

Lifecycle:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Sessions     โ”‚ --> โ”‚ Command 1    โ”‚ --> โ”‚ Command 2    โ”‚ --> โ”‚ Sessions     โ”‚
โ”‚ Created Once โ”‚     โ”‚ Execution    โ”‚     โ”‚ Execution    โ”‚     โ”‚ Removed      โ”‚
โ”‚ (~500msร—N)   โ”‚     โ”‚ (~fast)      โ”‚     โ”‚ (~fast)      โ”‚     โ”‚ (~100ms)     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                           โ”‚                     โ”‚
                           v                     v
                     State preserved       State preserved
                     (variables, modules, working directory)

When to use:

  • Multiple commands to the same servers
  • File copy operations (REQUIRED)
  • When you need to preserve state between commands
  • Performance-critical scenarios

Session Decision Matrix

Scenario Best Approach Why
Single command, single server Invoke-Command -ComputerName Simple, no cleanup needed
Single command, many servers Invoke-Command -ComputerName Parallel by default
Multiple commands, same servers New-PSSession + Invoke-Command -Session Reuse connections
File copy operations New-PSSession + Copy-Item -ToSession Required for file ops
Interactive exploration Enter-PSSession Real-time feedback
Long-running operations New-PSSession with error handling Control over lifecycle

Parallel Execution: ForEach-Object -Parallel and -ThrottleLimit

The Problem with Serial Execution

# This is SLOW - each server waits for the previous
$servers = @("Server01", "Server02", "Server03", "Server04", "Server05")

foreach ($server in $servers) {
    Invoke-Command -ComputerName $server -ScriptBlock {
        Start-Sleep -Seconds 2  # Simulating work
        Get-Service
    }
}
# Total time: 5 servers ร— 2 seconds = 10+ seconds

Invoke-Commandโ€™s Built-in Parallelism

# This is FAST - all servers execute simultaneously
$servers = @("Server01", "Server02", "Server03", "Server04", "Server05")

Invoke-Command -ComputerName $servers -ScriptBlock {
    Start-Sleep -Seconds 2
    Get-Service
}
# Total time: ~2-3 seconds (parallel execution)

Controlling Parallelism with -ThrottleLimit

# Default ThrottleLimit is 32 concurrent connections
# Lower it when servers can't handle simultaneous load:
Invoke-Command -ComputerName $largeServerList -ThrottleLimit 10 -ScriptBlock {
    # Heavy operation
    Get-ChildItem -Path "C:\" -Recurse -File | Measure-Object
}

ThrottleLimit Guidelines

Server Count ThrottleLimit Rationale
1-10 Default (32) No throttling needed
10-50 20-30 Slight throttling
50-200 10-20 Prevent network saturation
200+ 5-10 Heavy throttling, consider batching

ForEach-Object -Parallel (PowerShell 7+)

# Modern parallel processing with more control
$servers | ForEach-Object -Parallel {
    $server = $_

    # Each iteration runs in its own runspace
    $result = Invoke-Command -ComputerName $server -ScriptBlock {
        Get-Service -Name "Spooler"
    }

    # Return structured result
    [PSCustomObject]@{
        Server = $server
        SpoolerStatus = $result.Status
        Timestamp = Get-Date
    }
} -ThrottleLimit 10

Understanding the Execution Model

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚               Invoke-Command -ComputerName Server1,Server2,Server3          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                              โ”‚
โ”‚   PowerShell Engine                                                          โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚   โ”‚                    Runspace Pool (ThrottleLimit)                     โ”‚   โ”‚
โ”‚   โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                  โ”‚   โ”‚
โ”‚   โ”‚  โ”‚ Runspace 1  โ”‚  โ”‚ Runspace 2  โ”‚  โ”‚ Runspace 3  โ”‚  ...             โ”‚   โ”‚
โ”‚   โ”‚  โ”‚             โ”‚  โ”‚             โ”‚  โ”‚             โ”‚                  โ”‚   โ”‚
โ”‚   โ”‚  โ”‚ Server01    โ”‚  โ”‚ Server02    โ”‚  โ”‚ Server03    โ”‚                  โ”‚   โ”‚
โ”‚   โ”‚  โ”‚ connection  โ”‚  โ”‚ connection  โ”‚  โ”‚ connection  โ”‚                  โ”‚   โ”‚
โ”‚   โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                  โ”‚   โ”‚
โ”‚   โ”‚        โ”‚                โ”‚                โ”‚                          โ”‚   โ”‚
โ”‚   โ”‚        v                v                v                          โ”‚   โ”‚
โ”‚   โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚   โ”‚
โ”‚   โ”‚  โ”‚                  Results Aggregation                         โ”‚   โ”‚   โ”‚
โ”‚   โ”‚  โ”‚  PSComputerName property added to each output object        โ”‚   โ”‚   โ”‚
โ”‚   โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚   โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Credential Management and Delegation (CredSSP)

The Double-Hop Problem

When you connect to a remote server and try to access a third resource (network share, another server, SQL database), your credentials donโ€™t automatically forward:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Your PC  โ”‚ -------> โ”‚ Server01 โ”‚ ---X---> โ”‚ FileShareโ”‚
โ”‚          โ”‚ Creds OK โ”‚          โ”‚ No Creds โ”‚          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                           โ”‚
                           โ””โ”€โ”€ "Access Denied" when trying to reach FileShare

Why does this happen?

Kerberos and NTLM donโ€™t forward credentials by defaultโ€”this is a security feature! Your password hash on Server01 canโ€™t be used to authenticate elsewhere.

Solution 1: CredSSP (Credential Security Support Provider)

# Enable CredSSP on CLIENT (your workstation)
Enable-WSManCredSSP -Role Client -DelegateComputer "*.contoso.com" -Force

# Enable CredSSP on SERVER (target servers)
Enable-WSManCredSSP -Role Server -Force

# Use CredSSP authentication
$cred = Get-Credential
Invoke-Command -ComputerName Server01 -Authentication CredSSP -Credential $cred -ScriptBlock {
    # Now can access network resources
    Get-ChildItem "\\FileServer\Share"
}

CredSSP Security Warning

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                           CredSSP SECURITY RISKS                             โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                              โ”‚
โ”‚  CredSSP sends your ACTUAL CREDENTIALS to the remote server.                โ”‚
โ”‚                                                                              โ”‚
โ”‚  This means:                                                                 โ”‚
โ”‚  - A compromised server can capture your credentials                        โ”‚
โ”‚  - Your credentials exist in memory on the remote server                    โ”‚
โ”‚  - Credential theft attacks (Mimikatz) can harvest them                     โ”‚
โ”‚                                                                              โ”‚
โ”‚  ONLY use CredSSP when:                                                      โ”‚
โ”‚  1. You trust the remote server completely                                  โ”‚
โ”‚  2. The server has proper security hardening                                โ”‚
โ”‚  3. You need double-hop and have no alternative                             โ”‚
โ”‚  4. You're using a service account, not your admin credentials              โ”‚
โ”‚                                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Solution 2: Kerberos Constrained Delegation (Preferred)

Configured in Active Directory, allows specific services to delegate to specific resources without exposing credentials:

Administrator configures in AD:
Server01 can delegate to โ†’ FileServer (CIFS service only)

Solution 3: Resource-Based Constrained Delegation (RBCD)

Modern approach where the resource controls who can delegate to it:

# On FileServer, allow Server01 to access it via delegation
Set-ADComputer -Identity FileServer -PrincipalsAllowedToDelegateToAccount Server01$

Secure Credential Storage

# NEVER store credentials in plain text!

# Method 1: Export-Clixml (encrypted to current user on current machine)
$cred = Get-Credential
$cred | Export-Clixml -Path "$HOME\ServerCred.xml"

# Load later (only works for same user on same machine)
$cred = Import-Clixml -Path "$HOME\ServerCred.xml"

# Method 2: Windows Credential Manager
# Store
[void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime]
$vault = New-Object Windows.Security.Credentials.PasswordVault
$credential = New-Object Windows.Security.Credentials.PasswordCredential("ServerManager", "admin", "password")
$vault.Add($credential)

# Method 3: Azure Key Vault or HashiCorp Vault for enterprise

File Copying to/from Remote Machines

Copy-Item -ToSession and -FromSession

# Copy TO remote server
$session = New-PSSession -ComputerName Server01
Copy-Item -Path "C:\Local\config.xml" -Destination "C:\Remote\config.xml" -ToSession $session

# Copy FROM remote server
Copy-Item -Path "C:\Remote\logs\" -Destination "C:\Local\CollectedLogs\" -FromSession $session -Recurse

# Copy entire folders
Copy-Item -Path "C:\DeployPackage\" -Destination "C:\App\" -ToSession $session -Recurse -Force

Remove-PSSession $session

Understanding the Transfer Mechanism

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        File Transfer over PS Remoting                        โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                              โ”‚
โ”‚   Copy-Item -ToSession                                                       โ”‚
โ”‚                                                                              โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚   โ”‚ Local File   โ”‚ ------> โ”‚ Serialized via WS-Man    โ”‚ ------> โ”‚ Remote  โ”‚ โ”‚
โ”‚   โ”‚ C:\config.xmlโ”‚ Chunked โ”‚ (SOAP messages, base64)  โ”‚         โ”‚ File    โ”‚ โ”‚
โ”‚   โ”‚              โ”‚ ~512KB  โ”‚ Encrypted in transit     โ”‚         โ”‚         โ”‚ โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚                                                                              โ”‚
โ”‚   Performance Characteristics:                                               โ”‚
โ”‚   - Slower than SMB for large files (SOAP overhead)                         โ”‚
โ”‚   - Works through firewalls (single port)                                   โ”‚
โ”‚   - Encrypted by default                                                    โ”‚
โ”‚   - No need for file shares                                                 โ”‚
โ”‚                                                                              โ”‚
โ”‚   Best Practices:                                                            โ”‚
โ”‚   - Use for config files, scripts (<10MB)                                   โ”‚
โ”‚   - For large files, consider BITS or direct SMB                            โ”‚
โ”‚   - Compress before transfer: Compress-Archive                              โ”‚
โ”‚                                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Handling Large Transfers

# For large deployments, compress first
$source = "C:\DeployPackage"
$archive = "$env:TEMP\deploy.zip"

# Compress locally
Compress-Archive -Path "$source\*" -DestinationPath $archive -Force

# Transfer single file
Copy-Item -Path $archive -Destination "C:\Temp\deploy.zip" -ToSession $session

# Extract on remote
Invoke-Command -Session $session -ScriptBlock {
    Expand-Archive -Path "C:\Temp\deploy.zip" -DestinationPath "C:\App\" -Force
    Remove-Item "C:\Temp\deploy.zip"
}

Security Considerations

Network Security

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                          Security Checklist                                  โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                              โ”‚
โ”‚  TRANSPORT SECURITY:                                                         โ”‚
โ”‚  [ ] Use HTTPS (port 5986) for cross-network/internet                       โ”‚
โ”‚  [ ] HTTP (port 5985) acceptable on trusted internal networks               โ”‚
โ”‚  [ ] Messages encrypted regardless of HTTP/HTTPS                            โ”‚
โ”‚                                                                              โ”‚
โ”‚  AUTHENTICATION:                                                             โ”‚
โ”‚  [ ] Prefer Kerberos (domain environments)                                  โ”‚
โ”‚  [ ] Avoid Basic authentication (sends password in clear)                   โ”‚
โ”‚  [ ] Use certificate authentication for workgroup scenarios                 โ”‚
โ”‚                                                                              โ”‚
โ”‚  AUTHORIZATION:                                                              โ”‚
โ”‚  [ ] Limit who can remote (Remote Management Users group)                   โ”‚
โ”‚  [ ] Consider JEA (Just Enough Administration) for delegation               โ”‚
โ”‚  [ ] Audit remote sessions (Event Log)                                      โ”‚
โ”‚                                                                              โ”‚
โ”‚  CREDENTIAL HANDLING:                                                        โ”‚
โ”‚  [ ] Never store passwords in scripts                                       โ”‚
โ”‚  [ ] Use encrypted credential files or vaults                               โ”‚
โ”‚  [ ] Avoid CredSSP unless absolutely necessary                              โ”‚
โ”‚  [ ] Use service accounts, not personal admin accounts                      โ”‚
โ”‚                                                                              โ”‚
โ”‚  MONITORING:                                                                 โ”‚
โ”‚  [ ] Log all remote operations                                              โ”‚
โ”‚  [ ] Forward WinRM events to SIEM                                           โ”‚
โ”‚  [ ] Alert on unusual remote session patterns                               โ”‚
โ”‚                                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

WinRM Configuration Hardening

# View current configuration
winrm get winrm/config

# Key security settings
winrm set winrm/config/service '@{AllowUnencrypted="false"}'
winrm set winrm/config/service/auth '@{Basic="false"}'
winrm set winrm/config/service/auth '@{Kerberos="true"}'

# Limit maximum concurrent operations per user
winrm set winrm/config/winrs '@{MaxConcurrentUsers="10"}'
winrm set winrm/config/winrs '@{MaxShellsPerUser="5"}'

Complete Project Specification

Functional Requirements

ID Requirement Priority Description
F1 Multi-server connection Must Have Connect to 1-100+ servers via PS Remoting
F2 Service status query Must Have Check service status across fleet
F3 Service control Must Have Start/stop/restart services remotely
F4 File deployment Must Have Copy configuration files to servers
F5 Log collection Must Have Gather log files from remote servers
F6 Arbitrary script execution Must Have Run custom scripts on remote machines
F7 Credential management Should Have Secure credential storage and retrieval
F8 Parallel execution Should Have Concurrent operations with throttling
F9 Session pooling Should Have Reuse connections efficiently
F10 Operation logging Should Have Audit trail of all operations
F11 Server inventory Should Have Maintain list of managed servers
F12 Per-server error tracking Should Have Report errors per server, not fail-all
F13 Connection validation Nice to Have Test connectivity before operations
F14 Rollback support Nice to Have Undo file deployments
F15 Progress reporting Nice to Have Real-time progress for long operations

Non-Functional Requirements

ID Requirement Metric
NF1 Performance 10 servers queried in < 10 seconds
NF2 Performance 100 servers queried in < 60 seconds
NF3 Reliability Handle 20% server failures gracefully
NF4 Timeout Individual server timeout: 30 seconds
NF5 Security Credentials never in logs or output
NF6 Compatibility Works with domain and workgroup machines
NF7 Logging All operations logged with timestamp
NF8 Modularity Installable as PowerShell module

Real World Outcome

When complete, youโ€™ll have a professional server management toolkit:

Multi-Server Query Examples

# Query SQL services across database servers
Get-ServiceStatus -ComputerName $DBServers -ServiceName "SQL*" -Credential $cred

# Output:
# ComputerName  ServiceName           DisplayName              Status   StartType
# ------------  -----------           -----------              ------   ---------
# DBSRV01       MSSQLSERVER           SQL Server (MSSQLSERVER) Running  Automatic
# DBSRV01       SQLSERVERAGENT        SQL Server Agent         Running  Automatic
# DBSRV02       MSSQLSERVER           SQL Server (MSSQLSERVER) Stopped  Automatic  <-- ALERT!
# DBSRV02       SQLSERVERAGENT        SQL Server Agent         Stopped  Automatic
# DBSRV03       MSSQLSERVER           SQL Server (MSSQLSERVER) Running  Automatic
# DBSRV03       SQLSERVERAGENT        SQL Server Agent         Running  Automatic
# DBSRV04       ERROR                 Connection Failed        -        -

Parallel Execution Output

# Deploy configuration to 50 web servers
$result = Deploy-ConfigFile -ComputerName $WebServers `
                            -SourcePath ".\web.config" `
                            -DestinationPath "C:\inetpub\wwwroot\web.config" `
                            -Backup `
                            -Credential $cred

# Real-time progress:
# [=============================>....................] 58% - 29/50 servers complete
#
# Deploying to WEB01... Success
# Deploying to WEB02... Success
# Deploying to WEB03... Failed (Access Denied)
# Deploying to WEB04... Success
# ...

$result | Where-Object Status -eq 'Failed' | Format-Table ComputerName, Error

# ComputerName  Error
# ------------  -----
# WEB03         Access is denied. (Exception from HRESULT: 0x80070005)
# WEB17         The WinRM client cannot process the request (timeout)
# WEB32         The network path was not found

Error Handling Across Servers

# Restart services with comprehensive error handling
$restartResult = Restart-RemoteService -ComputerName $AppServers `
                                       -ServiceName "MyAppService" `
                                       -Wait `
                                       -TimeoutSeconds 120 `
                                       -Credential $cred

# Summary output:
#
# Remote Service Restart Results
# ==============================
# Total Servers:    25
# Successful:       22
# Failed:           3
# Duration:         45 seconds
#
# Failures:
# ---------
# APPSRV05: Service 'MyAppService' not found
# APPSRV12: Failed to stop service within timeout
# APPSRV19: Connection refused (WinRM not running)
#
# All successful servers now have MyAppService in 'Running' state.

Credential Prompt Handling

# First run - prompts for credentials
$manager = New-ServerManager -ServerListPath ".\servers.json" -SaveCredential

# Credential prompt appears:
# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
# โ”‚ Windows PowerShell credential request              โ”‚
# โ”‚                                                    โ”‚
# โ”‚ Enter your credentials for server management.     โ”‚
# โ”‚                                                    โ”‚
# โ”‚ User name: [DOMAIN\AdminUser_____________]        โ”‚
# โ”‚ Password:  [**************************___]        โ”‚
# โ”‚                                                    โ”‚
# โ”‚              [  OK  ]   [ Cancel ]                โ”‚
# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

# Credentials saved encrypted to $HOME\ServerManager_cred.xml

# Subsequent runs - no prompt
$manager = New-ServerManager -ServerListPath ".\servers.json"
# Loads saved credentials automatically

Log Collection Workflow

# Collect application logs from last 24 hours
$logs = Get-RemoteLogs -ComputerName $WebServers `
                       -LogPath "C:\Logs\Application*.log" `
                       -Since (Get-Date).AddHours(-24) `
                       -LocalDestination "C:\CollectedLogs\$(Get-Date -Format 'yyyy-MM-dd')" `
                       -Credential $cred

# Progress:
# Collecting from WEB01... 3 files (12.4 MB)
# Collecting from WEB02... 5 files (28.1 MB)
# Collecting from WEB03... 2 files (4.2 MB)
# ...
#
# Summary:
# Total files collected: 145
# Total size: 342.7 MB
# Stored in: C:\CollectedLogs\2025-01-15\

# View collected files
$logs | Group-Object ComputerName |
    Select-Object Name, Count, @{N='TotalMB';E={[math]::Round(($_.Group | Measure-Object Size -Sum).Sum/1MB, 2)}}

# Name    Count TotalMB
# ----    ----- -------
# WEB01       3   12.40
# WEB02       5   28.10
# WEB03       2    4.20
# ...

Solution Architecture

Component Diagram

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                            Server Management Module                                   โ”‚
โ”‚                                 ServerManager.psm1                                    โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                                      โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚   โ”‚                              PUBLIC FUNCTIONS                               โ”‚    โ”‚
โ”‚   โ”‚  (Exported in .psd1 manifest, callable by users)                           โ”‚    โ”‚
โ”‚   โ”‚                                                                             โ”‚    โ”‚
โ”‚   โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”‚    โ”‚
โ”‚   โ”‚  โ”‚ Get-ServiceStatus โ”‚  โ”‚ Deploy-ConfigFile โ”‚  โ”‚ Get-RemoteLogs    โ”‚       โ”‚    โ”‚
โ”‚   โ”‚  โ”‚                   โ”‚  โ”‚                   โ”‚  โ”‚                   โ”‚       โ”‚    โ”‚
โ”‚   โ”‚  โ”‚ Query services    โ”‚  โ”‚ Copy files to     โ”‚  โ”‚ Collect files     โ”‚       โ”‚    โ”‚
โ”‚   โ”‚  โ”‚ across servers    โ”‚  โ”‚ remote servers    โ”‚  โ”‚ from servers      โ”‚       โ”‚    โ”‚
โ”‚   โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ”‚    โ”‚
โ”‚   โ”‚                                                                             โ”‚    โ”‚
โ”‚   โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”‚    โ”‚
โ”‚   โ”‚  โ”‚ Start-Remote      โ”‚  โ”‚ Stop-Remote       โ”‚  โ”‚ Restart-Remote    โ”‚       โ”‚    โ”‚
โ”‚   โ”‚  โ”‚ Service           โ”‚  โ”‚ Service           โ”‚  โ”‚ Service           โ”‚       โ”‚    โ”‚
โ”‚   โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ”‚    โ”‚
โ”‚   โ”‚                                                                             โ”‚    โ”‚
โ”‚   โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”‚    โ”‚
โ”‚   โ”‚  โ”‚ Invoke-Remote     โ”‚  โ”‚ Test-Server       โ”‚  โ”‚ New-Server        โ”‚       โ”‚    โ”‚
โ”‚   โ”‚  โ”‚ Script            โ”‚  โ”‚ Connection        โ”‚  โ”‚ Manager           โ”‚       โ”‚    โ”‚
โ”‚   โ”‚  โ”‚ Run arbitrary PS  โ”‚  โ”‚ Validate remoting โ”‚  โ”‚ Initialize module โ”‚       โ”‚    โ”‚
โ”‚   โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ”‚    โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                        โ”‚                                             โ”‚
โ”‚                                        โ”‚ calls                                       โ”‚
โ”‚                                        v                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚   โ”‚                             PRIVATE FUNCTIONS                               โ”‚    โ”‚
โ”‚   โ”‚  (Internal use only, not exported)                                         โ”‚    โ”‚
โ”‚   โ”‚                                                                             โ”‚    โ”‚
โ”‚   โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”‚    โ”‚
โ”‚   โ”‚  โ”‚ Get-ServerSession โ”‚  โ”‚ Write-Operation   โ”‚  โ”‚ ConvertTo-        โ”‚       โ”‚    โ”‚
โ”‚   โ”‚  โ”‚                   โ”‚  โ”‚ Log               โ”‚  โ”‚ ServerResult      โ”‚       โ”‚    โ”‚
โ”‚   โ”‚  โ”‚ Session pool mgmt โ”‚  โ”‚ Audit logging     โ”‚  โ”‚ Normalize output  โ”‚       โ”‚    โ”‚
โ”‚   โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ”‚    โ”‚
โ”‚   โ”‚                                                                             โ”‚    โ”‚
โ”‚   โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”‚    โ”‚
โ”‚   โ”‚  โ”‚ Get-Saved         โ”‚  โ”‚ Save-             โ”‚  โ”‚ Test-             โ”‚       โ”‚    โ”‚
โ”‚   โ”‚  โ”‚ Credential        โ”‚  โ”‚ Credential        โ”‚  โ”‚ SessionHealth     โ”‚       โ”‚    โ”‚
โ”‚   โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ”‚    โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                        โ”‚                                             โ”‚
โ”‚                                        โ”‚ uses                                        โ”‚
โ”‚                                        v                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚   โ”‚                            MODULE STATE                                     โ”‚    โ”‚
โ”‚   โ”‚  $script:SessionPool   - Hash table of active PSSessions                   โ”‚    โ”‚
โ”‚   โ”‚  $script:Credential    - Cached credential object                          โ”‚    โ”‚
โ”‚   โ”‚  $script:Config        - Module configuration                              โ”‚    โ”‚
โ”‚   โ”‚  $script:LogPath       - Path to operation log                             โ”‚    โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                                                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                   MODULE FILES                                       โ”‚
โ”‚                                                                                      โ”‚
โ”‚   ServerManager/                                                                     โ”‚
โ”‚   โ”œโ”€โ”€ ServerManager.psd1          # Module manifest                                 โ”‚
โ”‚   โ”œโ”€โ”€ ServerManager.psm1          # Main module (dot-sources others)                โ”‚
โ”‚   โ”œโ”€โ”€ Public/                                                                        โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ Get-ServiceStatus.ps1                                                     โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ Start-RemoteService.ps1                                                   โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ Stop-RemoteService.ps1                                                    โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ Restart-RemoteService.ps1                                                 โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ Deploy-ConfigFile.ps1                                                     โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ Get-RemoteLogs.ps1                                                        โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ Invoke-RemoteScript.ps1                                                   โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ Test-ServerConnection.ps1                                                 โ”‚
โ”‚   โ”‚   โ””โ”€โ”€ New-ServerManager.ps1                                                     โ”‚
โ”‚   โ”œโ”€โ”€ Private/                                                                       โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ Get-ServerSession.ps1                                                     โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ Write-OperationLog.ps1                                                    โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ ConvertTo-ServerResult.ps1                                                โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ Get-SavedCredential.ps1                                                   โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ Save-Credential.ps1                                                       โ”‚
โ”‚   โ”‚   โ””โ”€โ”€ Test-SessionHealth.ps1                                                    โ”‚
โ”‚   โ”œโ”€โ”€ Config/                                                                        โ”‚
โ”‚   โ”‚   โ””โ”€โ”€ servers.json            # Default server inventory                        โ”‚
โ”‚   โ””โ”€โ”€ Logs/                                                                          โ”‚
โ”‚       โ””โ”€โ”€ operations.log          # Operation audit log                             โ”‚
โ”‚                                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Session Reuse Strategy

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                              Session Pool Management                                  โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                                      โ”‚
โ”‚   User calls: Get-ServiceStatus -ComputerName Server01,Server02,Server03             โ”‚
โ”‚                                                                                      โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚   โ”‚                         Get-ServerSession                                    โ”‚    โ”‚
โ”‚   โ”‚                                                                             โ”‚    โ”‚
โ”‚   โ”‚  1. Check $script:SessionPool for existing sessions                         โ”‚    โ”‚
โ”‚   โ”‚                                                                             โ”‚    โ”‚
โ”‚   โ”‚     SessionPool = @{                                                        โ”‚    โ”‚
โ”‚   โ”‚       "Server01" = [PSSession] State=Opened                                 โ”‚    โ”‚
โ”‚   โ”‚       "Server02" = [PSSession] State=Closed (stale!)                        โ”‚    โ”‚
โ”‚   โ”‚     }                                                                       โ”‚    โ”‚
โ”‚   โ”‚                                                                             โ”‚    โ”‚
โ”‚   โ”‚  2. For each requested server:                                              โ”‚    โ”‚
โ”‚   โ”‚                                                                             โ”‚    โ”‚
โ”‚   โ”‚     Server01: Found in pool, state=Opened -> REUSE                          โ”‚    โ”‚
โ”‚   โ”‚     Server02: Found in pool, state=Closed -> REMOVE, CREATE NEW             โ”‚    โ”‚
โ”‚   โ”‚     Server03: Not in pool -> CREATE NEW                                     โ”‚    โ”‚
โ”‚   โ”‚                                                                             โ”‚    โ”‚
โ”‚   โ”‚  3. Update pool and return sessions                                         โ”‚    โ”‚
โ”‚   โ”‚                                                                             โ”‚    โ”‚
โ”‚   โ”‚     SessionPool = @{                                                        โ”‚    โ”‚
โ”‚   โ”‚       "Server01" = [PSSession] State=Opened (reused)                        โ”‚    โ”‚
โ”‚   โ”‚       "Server02" = [PSSession] State=Opened (new)                           โ”‚    โ”‚
โ”‚   โ”‚       "Server03" = [PSSession] State=Opened (new)                           โ”‚    โ”‚
โ”‚   โ”‚     }                                                                       โ”‚    โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                                                      โ”‚
โ”‚   Session Lifecycle:                                                                 โ”‚
โ”‚                                                                                      โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”‚
โ”‚   โ”‚ Created โ”‚ -> โ”‚ Used    โ”‚ -> โ”‚ Used    โ”‚ -> โ”‚ Idle    โ”‚ -> โ”‚ Removed โ”‚          โ”‚
โ”‚   โ”‚         โ”‚    โ”‚ Cmd 1   โ”‚    โ”‚ Cmd 2   โ”‚    โ”‚ Timeout โ”‚    โ”‚         โ”‚          โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ”‚
โ”‚                                                                                      โ”‚
โ”‚   Cleanup Triggers:                                                                  โ”‚
โ”‚   - Explicit: Clear-ServerSessions                                                  โ”‚
โ”‚   - Automatic: Module unload                                                        โ”‚
โ”‚   - Health check: Stale session detected                                            โ”‚
โ”‚                                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Error Aggregation Approach

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                              Error Handling Strategy                                  โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                                      โ”‚
โ”‚   Principle: PARTIAL SUCCESS IS SUCCESS                                              โ”‚
โ”‚   - Never fail entirely because one server is unreachable                           โ”‚
โ”‚   - Report per-server status                                                        โ”‚
โ”‚   - Let the caller decide how to handle failures                                    โ”‚
โ”‚                                                                                      โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚   โ”‚                     Invoke-Command Error Handling                            โ”‚    โ”‚
โ”‚   โ”‚                                                                             โ”‚    โ”‚
โ”‚   โ”‚  $invokeParams = @{                                                         โ”‚    โ”‚
โ”‚   โ”‚      ComputerName = $servers                                                โ”‚    โ”‚
โ”‚   โ”‚      ScriptBlock = { ... }                                                  โ”‚    โ”‚
โ”‚   โ”‚      ErrorAction = 'SilentlyContinue'    # Don't throw                      โ”‚    โ”‚
โ”‚   โ”‚      ErrorVariable = 'remoteErrors'       # Capture errors                  โ”‚    โ”‚
โ”‚   โ”‚  }                                                                          โ”‚    โ”‚
โ”‚   โ”‚                                                                             โ”‚    โ”‚
โ”‚   โ”‚  $results = Invoke-Command @invokeParams                                    โ”‚    โ”‚
โ”‚   โ”‚                                                                             โ”‚    โ”‚
โ”‚   โ”‚  # Results contain successful responses with PSComputerName                 โ”‚    โ”‚
โ”‚   โ”‚  # remoteErrors contains failure details                                    โ”‚    โ”‚
โ”‚   โ”‚                                                                             โ”‚    โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                                                      โ”‚
โ”‚   Result Object Structure:                                                           โ”‚
โ”‚                                                                                      โ”‚
โ”‚   [PSCustomObject]@{                                                                 โ”‚
โ”‚       ComputerName = "Server01"                                                     โ”‚
โ”‚       Status       = "Success" | "Failed" | "Warning"                               โ”‚
โ”‚       Data         = <operation-specific data>                                      โ”‚
โ”‚       Error        = $null | "Error message"                                        โ”‚
โ”‚       Timestamp    = Get-Date                                                       โ”‚
โ”‚       Duration     = [TimeSpan]                                                     โ”‚
โ”‚   }                                                                                  โ”‚
โ”‚                                                                                      โ”‚
โ”‚   Error Categories:                                                                  โ”‚
โ”‚                                                                                      โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚   โ”‚ Connection Errors  โ”‚ WinRM not running, firewall, DNS failure              โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค   โ”‚
โ”‚   โ”‚ Auth Errors        โ”‚ Access denied, invalid credentials                     โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค   โ”‚
โ”‚   โ”‚ Execution Errors   โ”‚ Script threw exception, timeout                        โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค   โ”‚
โ”‚   โ”‚ Resource Errors    โ”‚ Service not found, file not exists                     โ”‚   โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Phased Implementation Guide

Phase 1: Single Server Remoting (3-4 hours)

Goal: Establish the foundationโ€”connect to one remote machine and execute commands.

What Youโ€™ll Learn:

  • WinRM configuration on both ends
  • Basic Invoke-Command syntax
  • Credential handling fundamentals

Steps:

  1. Configure WinRM on target server
    # Run on target server (as Administrator)
    Enable-PSRemoting -Force
    winrm quickconfig -Force
    
    # Verify listener
    winrm enumerate winrm/config/listener
    
  2. Configure trusted hosts (for workgroup)
    # On your workstation (if not domain-joined)
    Set-Item WSMan:\localhost\Client\TrustedHosts -Value "Server01" -Force
    
  3. Create basic connection test
    function Test-RemoteConnection {
        param(
            [Parameter(Mandatory)]
            [string]$ComputerName,
            [PSCredential]$Credential
        )
    
        $params = @{
            ComputerName = $ComputerName
            ScriptBlock = {
                [PSCustomObject]@{
                    Hostname = $env:COMPUTERNAME
                    User = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
                    PSVersion = $PSVersionTable.PSVersion.ToString()
                    Time = Get-Date
                }
            }
        }
        if ($Credential) { $params['Credential'] = $Credential }
    
        try {
            $result = Invoke-Command @params -ErrorAction Stop
            Write-Host "Connected to $($result.Hostname) as $($result.User)" -ForegroundColor Green
            return $result
        }
        catch {
            Write-Error "Failed to connect: $_"
            return $null
        }
    }
    
  4. Create simple service query
    function Get-RemoteService {
        param(
            [Parameter(Mandatory)]
            [string]$ComputerName,
            [string]$ServiceName = "*",
            [PSCredential]$Credential
        )
    
        $params = @{
            ComputerName = $ComputerName
            ScriptBlock = {
                param($Name)
                Get-Service -Name $Name | Select-Object Name, DisplayName, Status
            }
            ArgumentList = $ServiceName
        }
        if ($Credential) { $params['Credential'] = $Credential }
    
        Invoke-Command @params
    }
    

Verification:

$cred = Get-Credential
Test-RemoteConnection -ComputerName "Server01" -Credential $cred
Get-RemoteService -ComputerName "Server01" -ServiceName "Spooler" -Credential $cred

Phase 2: Multi-Server with Sessions (4-5 hours)

Goal: Scale to multiple servers with efficient session management.

What Youโ€™ll Learn:

  • New-PSSession for persistent connections
  • Session reuse patterns
  • Handling multiple targets in parallel

Steps:

  1. Create session pool manager
    # Module-scoped state
    $script:SessionPool = @{}
    
    function Get-ServerSession {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory)]
            [string[]]$ComputerName,
            [PSCredential]$Credential
        )
    
        $sessions = @()
        $newServers = @()
    
        foreach ($server in $ComputerName) {
            # Check for existing valid session
            if ($script:SessionPool.ContainsKey($server)) {
                $existing = $script:SessionPool[$server]
                if ($existing.State -eq 'Opened') {
                    Write-Verbose "Reusing session for $server"
                    $sessions += $existing
                    continue
                }
                # Session is stale, remove it
                Write-Verbose "Removing stale session for $server"
                $script:SessionPool.Remove($server)
            }
            $newServers += $server
        }
    
        # Create new sessions for servers not in pool
        if ($newServers.Count -gt 0) {
            Write-Verbose "Creating sessions for: $($newServers -join ', ')"
    
            $sessionParams = @{
                ComputerName = $newServers
                ErrorAction = 'SilentlyContinue'
                ErrorVariable = 'sessionErrors'
            }
            if ($Credential) { $sessionParams['Credential'] = $Credential }
    
            $newSessions = New-PSSession @sessionParams
    
            foreach ($session in $newSessions) {
                $script:SessionPool[$session.ComputerName] = $session
                $sessions += $session
            }
    
            # Report failures
            foreach ($err in $sessionErrors) {
                Write-Warning "Failed to create session: $($err.TargetObject) - $($err.Exception.Message)"
            }
        }
    
        return $sessions
    }
    
    function Clear-ServerSessions {
        [CmdletBinding()]
        param()
    
        if ($script:SessionPool.Count -gt 0) {
            Write-Verbose "Closing $($script:SessionPool.Count) sessions"
            $script:SessionPool.Values | Remove-PSSession -ErrorAction SilentlyContinue
            $script:SessionPool.Clear()
        }
    }
    
  2. Create multi-server service status
    function Get-ServiceStatus {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
            [string[]]$ComputerName,
            [string]$ServiceName = "*",
            [PSCredential]$Credential,
            [int]$ThrottleLimit = 32
        )
    
        begin {
            $allServers = @()
        }
    
        process {
            $allServers += $ComputerName
        }
    
        end {
            $sessions = Get-ServerSession -ComputerName $allServers -Credential $Credential
    
            if ($sessions.Count -eq 0) {
                Write-Warning "No sessions available"
                return
            }
    
            $results = Invoke-Command -Session $sessions -ThrottleLimit $ThrottleLimit -ScriptBlock {
                param($Pattern)
                Get-Service -Name $Pattern -ErrorAction SilentlyContinue |
                    Select-Object Name, DisplayName, Status, StartType
            } -ArgumentList $ServiceName -ErrorAction SilentlyContinue -ErrorVariable remoteErrors
    
            # Transform results
            foreach ($result in $results) {
                [PSCustomObject]@{
                    ComputerName = $result.PSComputerName
                    ServiceName = $result.Name
                    DisplayName = $result.DisplayName
                    Status = $result.Status
                    StartType = $result.StartType
                }
            }
    
            # Report errors
            foreach ($err in $remoteErrors) {
                [PSCustomObject]@{
                    ComputerName = $err.TargetObject
                    ServiceName = 'ERROR'
                    DisplayName = 'Connection/Execution Failed'
                    Status = '-'
                    StartType = '-'
                    Error = $err.Exception.Message
                }
            }
        }
    }
    

Verification:

$servers = @("Server01", "Server02", "Server03")
$cred = Get-Credential

# First call creates sessions
Get-ServiceStatus -ComputerName $servers -ServiceName "Spooler" -Credential $cred

# Second call reuses sessions (check verbose output)
Get-ServiceStatus -ComputerName $servers -ServiceName "W32Time" -Credential $cred -Verbose

# Cleanup
Clear-ServerSessions

Phase 3: Parallel Execution (3-4 hours)

Goal: Optimize for large server fleets with proper throttling and progress reporting.

What Youโ€™ll Learn:

  • ForEach-Object -Parallel (PowerShell 7)
  • Custom progress reporting
  • Timeout handling

Steps:

  1. Add progress reporting
    function Invoke-ParallelServerCommand {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory)]
            [string[]]$ComputerName,
            [Parameter(Mandatory)]
            [scriptblock]$ScriptBlock,
            [object[]]$ArgumentList,
            [PSCredential]$Credential,
            [int]$ThrottleLimit = 10,
            [int]$TimeoutSeconds = 60,
            [switch]$ShowProgress
        )
    
        $total = $ComputerName.Count
        $completed = 0
        $results = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
    
        # PowerShell 7+ parallel execution
        if ($PSVersionTable.PSVersion.Major -ge 7) {
            $ComputerName | ForEach-Object -Parallel {
                $server = $_
                $cred = $using:Credential
                $sb = $using:ScriptBlock
                $args = $using:ArgumentList
                $resultBag = $using:results
    
                $invokeParams = @{
                    ComputerName = $server
                    ScriptBlock = $sb
                    ErrorAction = 'Stop'
                }
                if ($args) { $invokeParams['ArgumentList'] = $args }
                if ($cred) { $invokeParams['Credential'] = $cred }
    
                try {
                    $output = Invoke-Command @invokeParams
                    $resultBag.Add([PSCustomObject]@{
                        ComputerName = $server
                        Status = 'Success'
                        Data = $output
                        Error = $null
                    })
                }
                catch {
                    $resultBag.Add([PSCustomObject]@{
                        ComputerName = $server
                        Status = 'Failed'
                        Data = $null
                        Error = $_.Exception.Message
                    })
                }
            } -ThrottleLimit $ThrottleLimit
        }
        else {
            # PowerShell 5.1 fallback - use runspaces or Invoke-Command's built-in parallelism
            $sessions = Get-ServerSession -ComputerName $ComputerName -Credential $Credential
    
            $output = Invoke-Command -Session $sessions -ScriptBlock $ScriptBlock `
                -ArgumentList $ArgumentList -ThrottleLimit $ThrottleLimit `
                -ErrorAction SilentlyContinue -ErrorVariable errors
    
            foreach ($item in $output) {
                $results.Add([PSCustomObject]@{
                    ComputerName = $item.PSComputerName
                    Status = 'Success'
                    Data = $item
                    Error = $null
                })
            }
    
            foreach ($err in $errors) {
                $results.Add([PSCustomObject]@{
                    ComputerName = $err.TargetObject
                    Status = 'Failed'
                    Data = $null
                    Error = $err.Exception.Message
                })
            }
        }
    
        return $results
    }
    
  2. Add timeout wrapper
    function Invoke-WithTimeout {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory)]
            [scriptblock]$ScriptBlock,
            [int]$TimeoutSeconds = 30,
            [string]$OperationName = "Operation"
        )
    
        $job = Start-Job -ScriptBlock $ScriptBlock
    
        $completed = $job | Wait-Job -Timeout $TimeoutSeconds
    
        if ($completed) {
            $result = Receive-Job -Job $job
            Remove-Job -Job $job
            return $result
        }
        else {
            Stop-Job -Job $job
            Remove-Job -Job $job
            throw "$OperationName timed out after $TimeoutSeconds seconds"
        }
    }
    

Phase 4: File Deployment (4-5 hours)

Goal: Implement secure file transfer to remote servers.

What Youโ€™ll Learn:

  • Copy-Item -ToSession mechanics
  • Backup strategies
  • Verification after transfer

Steps:

  1. Create file deployment function
    function Deploy-ConfigFile {
        [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')]
        param(
            [Parameter(Mandatory)]
            [string[]]$ComputerName,
    
            [Parameter(Mandatory)]
            [ValidateScript({ Test-Path $_ -PathType Leaf })]
            [string]$SourcePath,
    
            [Parameter(Mandatory)]
            [string]$DestinationPath,
    
            [PSCredential]$Credential,
    
            [switch]$Backup,
    
            [switch]$Force,
    
            [switch]$Verify
        )
    
        $results = @()
        $sessions = Get-ServerSession -ComputerName $ComputerName -Credential $Credential
    
        foreach ($session in $sessions) {
            $server = $session.ComputerName
    
            if (-not $PSCmdlet.ShouldProcess($server, "Deploy $SourcePath to $DestinationPath")) {
                continue
            }
    
            try {
                # Create backup if requested
                if ($Backup) {
                    $backupResult = Invoke-Command -Session $session -ScriptBlock {
                        param($Path)
                        if (Test-Path $Path) {
                            $backupPath = "$Path.$(Get-Date -Format 'yyyyMMddHHmmss').bak"
                            Copy-Item -Path $Path -Destination $backupPath -Force
                            return $backupPath
                        }
                        return $null
                    } -ArgumentList $DestinationPath
    
                    if ($backupResult) {
                        Write-Verbose "Backed up existing file to $backupResult on $server"
                    }
                }
    
                # Ensure destination directory exists
                Invoke-Command -Session $session -ScriptBlock {
                    param($Path)
                    $dir = Split-Path -Path $Path -Parent
                    if (-not (Test-Path $dir)) {
                        New-Item -ItemType Directory -Path $dir -Force | Out-Null
                    }
                } -ArgumentList $DestinationPath
    
                # Copy file
                Copy-Item -Path $SourcePath -Destination $DestinationPath -ToSession $session -Force:$Force
    
                # Verify if requested
                $verified = $true
                if ($Verify) {
                    $sourceHash = (Get-FileHash -Path $SourcePath -Algorithm SHA256).Hash
                    $destHash = Invoke-Command -Session $session -ScriptBlock {
                        param($Path)
                        (Get-FileHash -Path $Path -Algorithm SHA256).Hash
                    } -ArgumentList $DestinationPath
    
                    $verified = ($sourceHash -eq $destHash)
                }
    
                $results += [PSCustomObject]@{
                    ComputerName = $server
                    Status = if ($verified) { 'Success' } else { 'VerificationFailed' }
                    DestinationPath = $DestinationPath
                    BackupPath = $backupResult
                    Verified = $verified
                    Error = $null
                }
            }
            catch {
                $results += [PSCustomObject]@{
                    ComputerName = $server
                    Status = 'Failed'
                    DestinationPath = $DestinationPath
                    BackupPath = $null
                    Verified = $false
                    Error = $_.Exception.Message
                }
            }
        }
    
        return $results
    }
    
  2. Create log collection function
    function Get-RemoteLogs {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory)]
            [string[]]$ComputerName,
    
            [Parameter(Mandatory)]
            [string]$LogPath,
    
            [DateTime]$Since,
    
            [string]$LocalDestination = ".\CollectedLogs",
    
            [PSCredential]$Credential,
    
            [switch]$Compress
        )
    
        # Create local destination
        $timestamp = Get-Date -Format 'yyyy-MM-dd_HHmmss'
        $destRoot = Join-Path $LocalDestination $timestamp
    
        if (-not (Test-Path $destRoot)) {
            New-Item -ItemType Directory -Path $destRoot -Force | Out-Null
        }
    
        $sessions = Get-ServerSession -ComputerName $ComputerName -Credential $Credential
        $results = @()
    
        foreach ($session in $sessions) {
            $server = $session.ComputerName
            $serverDir = Join-Path $destRoot $server
    
            if (-not (Test-Path $serverDir)) {
                New-Item -ItemType Directory -Path $serverDir -Force | Out-Null
            }
    
            try {
                # Get list of matching files
                $files = Invoke-Command -Session $session -ScriptBlock {
                    param($Path, $Since)
                    Get-ChildItem -Path $Path -File -ErrorAction SilentlyContinue |
                        Where-Object { -not $Since -or $_.LastWriteTime -gt $Since } |
                        Select-Object FullName, Name, Length, LastWriteTime
                } -ArgumentList $LogPath, $Since
    
                Write-Verbose "Found $($files.Count) files on $server"
    
                foreach ($file in $files) {
                    try {
                        $localPath = Join-Path $serverDir $file.Name
                        Copy-Item -Path $file.FullName -Destination $localPath -FromSession $session
    
                        $results += [PSCustomObject]@{
                            ComputerName = $server
                            FileName = $file.Name
                            Size = $file.Length
                            LastModified = $file.LastWriteTime
                            LocalPath = $localPath
                            Status = 'Collected'
                            Error = $null
                        }
                    }
                    catch {
                        $results += [PSCustomObject]@{
                            ComputerName = $server
                            FileName = $file.Name
                            Size = $file.Length
                            LastModified = $file.LastWriteTime
                            LocalPath = $null
                            Status = 'Failed'
                            Error = $_.Exception.Message
                        }
                    }
                }
    
                # Compress if requested
                if ($Compress -and $files.Count -gt 0) {
                    $archivePath = "$serverDir.zip"
                    Compress-Archive -Path "$serverDir\*" -DestinationPath $archivePath -Force
                    Remove-Item -Path $serverDir -Recurse -Force
                    Write-Verbose "Compressed logs from $server to $archivePath"
                }
            }
            catch {
                $results += [PSCustomObject]@{
                    ComputerName = $server
                    FileName = 'ERROR'
                    Size = 0
                    LastModified = $null
                    LocalPath = $null
                    Status = 'Failed'
                    Error = $_.Exception.Message
                }
            }
        }
    
        return $results
    }
    

Phase 5: Logging and Error Handling (3-4 hours)

Goal: Add enterprise-grade logging and comprehensive error management.

What Youโ€™ll Learn:

  • Structured logging patterns
  • Error categorization
  • Audit trail requirements

Steps:

  1. Create operation logger
    $script:LogPath = Join-Path $PSScriptRoot "Logs\operations.log"
    
    function Write-OperationLog {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory)]
            [string]$Operation,
    
            [Parameter(Mandatory)]
            [string]$Target,
    
            [ValidateSet('Info', 'Warning', 'Error', 'Success')]
            [string]$Level = 'Info',
    
            [string]$Message,
    
            [string]$User = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
        )
    
        $logDir = Split-Path $script:LogPath -Parent
        if (-not (Test-Path $logDir)) {
            New-Item -ItemType Directory -Path $logDir -Force | Out-Null
        }
    
        $entry = [PSCustomObject]@{
            Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff'
            User = $User
            Operation = $Operation
            Target = $Target
            Level = $Level
            Message = $Message
        }
    
        $logLine = $entry | ConvertTo-Json -Compress
        Add-Content -Path $script:LogPath -Value $logLine
    
        # Also write to verbose stream
        $color = switch ($Level) {
            'Error'   { 'Red' }
            'Warning' { 'Yellow' }
            'Success' { 'Green' }
            default   { 'White' }
        }
        Write-Host "[$($entry.Timestamp)] $Level - $Operation on $Target: $Message" -ForegroundColor $color
    }
    
  2. Create service restart with full logging
    function Restart-RemoteService {
        [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
        param(
            [Parameter(Mandatory)]
            [string[]]$ComputerName,
    
            [Parameter(Mandatory)]
            [string]$ServiceName,
    
            [PSCredential]$Credential,
    
            [switch]$Wait,
    
            [int]$TimeoutSeconds = 120,
    
            [switch]$Force
        )
    
        $results = @()
        $sessions = Get-ServerSession -ComputerName $ComputerName -Credential $Credential
    
        Write-OperationLog -Operation "RestartService" -Target "$ServiceName on $($ComputerName -join ',')" -Level Info -Message "Starting service restart operation"
    
        foreach ($session in $sessions) {
            $server = $session.ComputerName
    
            if (-not $PSCmdlet.ShouldProcess("$ServiceName on $server", "Restart service")) {
                continue
            }
    
            Write-OperationLog -Operation "RestartService" -Target "$ServiceName on $server" -Level Info -Message "Initiating restart"
    
            try {
                $restartResult = Invoke-Command -Session $session -ScriptBlock {
                    param($SvcName, $ShouldWait, $Timeout)
    
                    $svc = Get-Service -Name $SvcName -ErrorAction Stop
                    $originalStatus = $svc.Status
    
                    # Stop service
                    Stop-Service -Name $SvcName -Force -ErrorAction Stop
    
                    if ($ShouldWait) {
                        $svc.WaitForStatus('Stopped', [TimeSpan]::FromSeconds($Timeout / 2))
                    }
    
                    # Start service
                    Start-Service -Name $SvcName -ErrorAction Stop
    
                    if ($ShouldWait) {
                        $svc.WaitForStatus('Running', [TimeSpan]::FromSeconds($Timeout / 2))
                    }
    
                    # Return final status
                    $finalSvc = Get-Service -Name $SvcName
                    [PSCustomObject]@{
                        Name = $finalSvc.Name
                        DisplayName = $finalSvc.DisplayName
                        OriginalStatus = $originalStatus
                        CurrentStatus = $finalSvc.Status
                    }
                } -ArgumentList $ServiceName, $Wait.IsPresent, $TimeoutSeconds
    
                $results += [PSCustomObject]@{
                    ComputerName = $server
                    ServiceName = $restartResult.Name
                    DisplayName = $restartResult.DisplayName
                    OriginalStatus = $restartResult.OriginalStatus
                    CurrentStatus = $restartResult.CurrentStatus
                    Status = 'Success'
                    Error = $null
                }
    
                Write-OperationLog -Operation "RestartService" -Target "$ServiceName on $server" -Level Success -Message "Service restarted. Now: $($restartResult.CurrentStatus)"
            }
            catch {
                $results += [PSCustomObject]@{
                    ComputerName = $server
                    ServiceName = $ServiceName
                    DisplayName = $null
                    OriginalStatus = $null
                    CurrentStatus = 'Unknown'
                    Status = 'Failed'
                    Error = $_.Exception.Message
                }
    
                Write-OperationLog -Operation "RestartService" -Target "$ServiceName on $server" -Level Error -Message $_.Exception.Message
            }
        }
    
        # Summary
        $successful = ($results | Where-Object Status -eq 'Success').Count
        $failed = ($results | Where-Object Status -eq 'Failed').Count
    
        Write-OperationLog -Operation "RestartService" -Target $ServiceName -Level Info -Message "Completed. Success: $successful, Failed: $failed"
    
        return $results
    }
    

Testing Strategy

Unit Tests

Test ID Test Case Steps Expected Result
UT01 Single server connection Test-ServerConnection -ComputerName localhost Returns connection info
UT02 Invalid server Test-ServerConnection -ComputerName โ€œnonexistentโ€ Returns error, no exception
UT03 Service query pattern Get-ServiceStatus -ComputerName localhost -ServiceName โ€œSpoolerโ€ Returns spooler status
UT04 Wildcard service query Get-ServiceStatus -ComputerName localhost -ServiceName โ€œW*โ€ Returns multiple services
UT05 Session caching Call Get-ServerSession twice Second call reuses session
UT06 Stale session handling Force close session, call function New session created

Integration Tests

Test ID Test Case Steps Expected Result
IT01 Multi-server query Query 3+ servers Results from all servers
IT02 Partial failure Include one invalid server Results from valid servers, error for invalid
IT03 File deployment Deploy config to 2 servers File exists on both
IT04 File with backup Deploy with -Backup Original file backed up
IT05 Log collection Collect logs from 2 servers Files in local folder, organized by server
IT06 Service restart Restart service, verify Service running after restart
IT07 Parallel execution Query 10 servers Completes in < 30 seconds

Performance Tests

Test ID Test Case Target Actual
PT01 10 servers, service query < 10 seconds ย 
PT02 50 servers, service query < 30 seconds ย 
PT03 100 servers, service query < 60 seconds ย 
PT04 File deploy (1MB) to 10 servers < 60 seconds ย 
PT05 Session creation 20 servers < 15 seconds ย 

Test Environment Setup

# Create test VMs (Hyper-V example)
$vmNames = @("TestSrv01", "TestSrv02", "TestSrv03")

foreach ($name in $vmNames) {
    # Create VM
    New-VM -Name $name -MemoryStartupBytes 2GB -NewVHDPath "C:\VMs\$name.vhdx" -NewVHDSizeBytes 40GB

    # Configure after OS install:
    # Enable-PSRemoting -Force
    # Set-NetFirewallRule -Name "WINRM-HTTP-In-TCP" -Enabled True
}

Common Pitfalls and Debugging Tips

Problem 1: WinRM Not Configured

Symptoms:

The WinRM client cannot process the request. If the authentication scheme
is different from Kerberos, or if the client computer is not joined to a domain...

Solutions:

# On target server (as Admin)
Enable-PSRemoting -Force

# If in workgroup, on your workstation:
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "Server01,Server02" -Force

# Verify
Test-WSMan -ComputerName Server01

Problem 2: Firewall Blocking

Symptoms:

WinRM cannot complete the operation. Verify that the specified computer name
is valid, that the computer is accessible over the network...

Solutions:

# On target server
Set-NetFirewallRule -Name "WINRM-HTTP-In-TCP" -Enabled True

# Or create rule
New-NetFirewallRule -Name "WinRM HTTP" -DisplayName "WinRM HTTP" -Direction Inbound -LocalPort 5985 -Protocol TCP -Action Allow

Problem 3: Credential Issues

Symptoms:

Access is denied.

Solutions:

# Verify group membership on target
Invoke-Command -ComputerName Server01 -ScriptBlock {
    net localgroup "Remote Management Users"
    net localgroup "Administrators"
}

# Add user if needed (on target)
Add-LocalGroupMember -Group "Remote Management Users" -Member "DOMAIN\User"

Problem 4: Session Timeouts

Symptoms:

The session state is not valid for this operation.

Solutions:

# Increase idle timeout (on target)
winrm set winrm/config/winrs '@{IdleTimeout="7200000"}'  # 2 hours

# In code, implement session health check
function Test-SessionHealth {
    param([System.Management.Automation.Runspaces.PSSession]$Session)

    if ($Session.State -ne 'Opened') { return $false }

    try {
        Invoke-Command -Session $Session -ScriptBlock { 1 } -ErrorAction Stop
        return $true
    }
    catch {
        return $false
    }
}

Problem 5: Double-Hop Authentication

Symptoms:

Access is denied when accessing network resources from remote session.

Solutions:

# Option 1: CredSSP (use with caution)
Enable-WSManCredSSP -Role Client -DelegateComputer "*.domain.com" -Force
Enable-WSManCredSSP -Role Server -Force  # On target

Invoke-Command -ComputerName Server01 -Authentication CredSSP -Credential $cred -ScriptBlock {
    Get-ChildItem "\\FileServer\Share"
}

# Option 2: Pass credentials explicitly
Invoke-Command -ComputerName Server01 -ScriptBlock {
    param($cred)
    New-PSDrive -Name "Z" -PSProvider FileSystem -Root "\\FileServer\Share" -Credential $cred
    Get-ChildItem "Z:\"
    Remove-PSDrive "Z"
} -ArgumentList $cred

Problem 6: Large Data Transfer Failures

Symptoms:

The message could not be transferred within the allotted time.

Solutions:

# Increase message size limits (on target)
winrm set winrm/config '@{MaxEnvelopeSizekb="8192"}'

# In code, compress before transfer
Compress-Archive -Path ".\LargeFolder" -DestinationPath ".\archive.zip"
Copy-Item ".\archive.zip" -Destination "C:\Temp\" -ToSession $session
Invoke-Command -Session $session -ScriptBlock {
    Expand-Archive -Path "C:\Temp\archive.zip" -DestinationPath "C:\Destination"
}

Debugging Checklist

[ ] Can you ping the target server?
[ ] Is WinRM service running on target? (Get-Service WinRM)
[ ] Is firewall allowing 5985/5986?
[ ] Are you using correct credentials?
[ ] Is user in Remote Management Users or Administrators?
[ ] Is TrustedHosts configured (workgroup)?
[ ] Is DNS resolving the hostname correctly?
[ ] Are you hitting session limits on the target?
[ ] Check WinRM event logs: Get-WinEvent -LogName Microsoft-Windows-WinRM/Operational

Extensions and Challenges

Easy Extensions

  1. HTML Report Generation: Add -GenerateReport switch to produce styled HTML output
  2. Email Notifications: Send alerts when services are down or deployments fail
  3. Scheduled Execution: Create scheduled task wrapper for regular health checks
  4. Custom Service List: Configuration file for monitored services per environment

Medium Extensions

  1. Rolling Restarts: Implement staged service restarts (restart 10% at a time)
  2. Rollback Capability: Store deployment history, enable rollback to previous config
  3. Dependency Tracking: Restart dependent services in correct order
  4. Real-time Dashboard: PowerShell Universal dashboard showing server status

Advanced Extensions

  1. Ansible-style Playbooks: Define multi-step operations in YAML, execute declaratively
  2. Drift Detection: Compare server configs against golden baseline
  3. Integration with ITSM: Create ServiceNow tickets for failures
  4. Kubernetes-style Desired State: Define desired state, tool enforces it continuously

Challenge Projects

  1. Blue/Green Deployment: Implement zero-downtime deployment with health checks
  2. Configuration Management: Build a mini-DSC alternative
  3. Security Hardening Tool: Audit and apply security baselines remotely
  4. Disaster Recovery: Automate full server rebuild from config

Books That Will Help

Topic Book Chapter Why It Matters
PowerShell Remoting Fundamentals Learn PowerShell in a Month of Lunches (4th Ed) by Don Jones & Travis Plunk Ch. 10-13: Remoting The definitive introduction to PowerShell remoting, covers WinRM setup, session management, and troubleshooting
Advanced Remoting Patterns PowerShell in Depth (2nd Ed) by Don Jones, Richard Siddaway, Jeffrey Hicks Ch. 10: โ€œRemote Controlโ€ Deep dive into session management, background jobs, and enterprise remoting patterns
Remoting Security PowerShell in Depth (2nd Ed) by Don Jones, Richard Siddaway, Jeffrey Hicks Ch. 11: โ€œSessionsโ€ Credential handling, delegation, and JEA implementation
WinRM Architecture Windows Server Administration Fundamentals by Microsoft Press Remoting sections Understanding WS-Man, WinRM service internals, and configuration
Windows Security Model Windows Security Internals by James Forshaw Ch. 4-5: โ€œSecurity Descriptorsโ€ and โ€œAccess Tokensโ€ Understanding why credential delegation is complex and how Windows security works
Authentication Protocols Windows Security Internals by James Forshaw Ch. 8: โ€œKerberosโ€ Deep understanding of Kerberos, NTLM, and why double-hop is problematic
Module Development The PowerShell Scripting and Toolmaking Book by Don Jones & Jeff Hicks Ch. 15-18 Building professional, distributable PowerShell modules
Error Handling at Scale Windows PowerShell Cookbook (3rd Ed) by Lee Holmes Error handling recipes Practical patterns for robust error management
Enterprise Automation PowerShell for Sysadmins by Adam Bertram Ch. 8-10: Remote management Real-world scenarios and best practices

Self-Assessment Checklist

Before considering this project complete, verify you can:

Core Functionality

  • Connect to remote servers using PowerShell Remoting
  • Query service status across multiple servers simultaneously
  • Start, stop, and restart services on remote machines
  • Copy configuration files to remote servers
  • Collect log files from remote servers to local machine
  • Execute arbitrary scripts on remote machines

Session Management

  • Create and reuse PSSession objects efficiently
  • Detect and replace stale sessions automatically
  • Clean up sessions properly on script completion
  • Explain the difference between implicit and explicit sessions

Parallel Execution

  • Run operations on 10+ servers in parallel
  • Configure appropriate ThrottleLimit for your environment
  • Explain why parallel is faster than serial execution
  • Handle individual server timeouts without blocking others

Security

  • Store and retrieve credentials securely
  • Explain why CredSSP is risky
  • Configure WinRM for secure operation
  • Credentials never appear in logs or output

Error Handling

  • Handle partial failures gracefully (some servers succeed, some fail)
  • Report errors per-server without failing entire operation
  • Log all operations with timestamps for audit trail
  • Provide meaningful error messages to users

Architecture

  • Module is properly structured (Public/Private folders)
  • Functions support pipeline input
  • WhatIf/Confirm support for destructive operations
  • Module can be installed and imported cleanly

Knowledge Verification

  • Can explain how WinRM/WSMan works
  • Can troubleshoot โ€œaccess deniedโ€ errors
  • Can explain the double-hop problem and solutions
  • Can set up remoting between two machines from scratch

Quick Reference

Essential Commands

# Enable remoting on a server
Enable-PSRemoting -Force

# Test remoting connection
Test-WSMan -ComputerName Server01

# Interactive session
Enter-PSSession -ComputerName Server01 -Credential $cred

# Run command on multiple servers
Invoke-Command -ComputerName Server01,Server02 -ScriptBlock { Get-Service }

# Create persistent sessions
$sessions = New-PSSession -ComputerName Server01,Server02

# Copy file to remote
Copy-Item -Path ".\file.txt" -Destination "C:\Temp\" -ToSession $session

# Copy file from remote
Copy-Item -Path "C:\Logs\*.log" -Destination ".\Logs\" -FromSession $session

# Clean up sessions
Remove-PSSession $sessions

Common WinRM Configuration

# View configuration
winrm get winrm/config

# Quick configure
winrm quickconfig

# Set trusted hosts (client side, workgroup)
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "Server01,Server02" -Force

# Increase limits
winrm set winrm/config '@{MaxEnvelopeSizekb="8192"}'
winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="1024"}'

Troubleshooting Commands

# Check WinRM service
Get-Service WinRM

# View WinRM events
Get-WinEvent -LogName Microsoft-Windows-WinRM/Operational -MaxEvents 50

# Test connectivity
Test-NetConnection -ComputerName Server01 -Port 5985

# Verify session state
Get-PSSession | Select-Object ComputerName, State, Availability