Multi Hop Windows Remote Management

Summary: There are cases where it’s necessary to use Windows Remote Management (WinRM), also known as WS-Management (WS-Man) to automate Windows Servers (especially Windows Server that are behind a Windows hop server). This is handy when there is no direct network access to the Windows server that need to be reached (typically for security reasons).

In this example, the following command is executed on the ThirdServer (through the FirstServer and then the SecondServer) in order to update a firewall rule to allow the WinRM service to respond to any source computer request (rather than just the local subnet).

Set-NetFirewallRule -Name WINRM-HTTP-In-TCP-PUBLIC -Action "Allow" -Direction "Inbound" -RemoteAddress "Any"

The default configuration for the WinRM firewall rule in Windows Server 2012+ is to only allow WinRM requests that originate from the local subnet of that server. This command changes a firewall rule to open WinRM to respond to requests from any source IP address.

multihopwinrm1

In addition, for environments that require multi-hop access over and to Windows Servers, RDP can be problematic if there are any network bandwidth or latency issues. For actions that don’t require access to the Windows desktop, WinRM is ideal since it is much more efficient and faster.

Note: The authentication token for the session on the ThirdServer may be reduced compared to the access available for the FirstServer. Specifically for access to external resources like network shares. 

MultiHop-ConfigWinRm.ps1

# Version:: 0.1.0 (1/13/2016)
# Script Description:: Expands WinRM scope.
#
# Author(s):: Otto Helweg
#

Write-Host "Configuring WinRM for remote access..."
# Get the necessary credentials for WinRM (usually Administrator level creds)
$creds = Get-Credential
$serverName = "FirstServer"
$secondServerName = "SecondServer"
$thirdServerName = "ThirdServer"

Write-Host "Running command from $serverName"
Invoke-Command -ComputerName $serverName -Credential $creds -ScriptBlock {
  param($secondServerName,$thirdServerName,$creds)
  Write-Host "Running command from $secondServerName"
  Invoke-Command -ComputerName $secondServerName -Credential $creds -ScriptBlock {
    param($thirdServerName,$creds)
    Write-Host "Running command from $thirdServerName"
    Invoke-Command -ComputerName $thirdServerName -Credential $creds -ScriptBlock {
      Set-NetFirewallRule -Name WINRM-HTTP-In-TCP-PUBLIC -Action "Allow" -Direction "Inbound" -RemoteAddress "Any"
    }
  } -ArgumentList $thirdServerName,$creds
} -ArgumentList $secondServerName,$thirdServerName,$creds

Note: The username for the credentials, needs to include the domain or server prefix. If this is a local account, use the ‘local\’ prefix. Therefore a local ‘Administrator’ account should be entered as ‘local\Administrator’.

Enjoy!

Server QA Testing With Pester

Summary: Pester is a PowerShell spin on unit testing (much like ServerSpec) on and for Windows. This example will demonstrate using Pester to test a remote Windows server where the scenario is verifying the quality of a freshly provisioned Windows server. Pester is used to check various settings on this new server that generally verify that the provisioning automation did the right thing.

More information and source code is available at the Pester Git repository here: https://github.com/pester/Pester

The following steps are performed when testing a server:

  1. Pester PowerShell module is downloaded to the remote server and loaded
  2. Pester tests are uploaded to the remote server
  3. PowerShell Pester test functions are uploaded to the remote server
  4. Pester test suite is executed on the remote server and the results are displayed
  5. All Pester tests and modules are removed from the remote server

The following files are used for this automation:

  1. PowerShell control script that automates the tests on a remote server and manages downloading/uploading the necessary files: Test-Server.ps1
  2. Pester test suite that contains a list of the tests to be performed: Azure.tests.ps1
  3. PowerShell functions that perform the Pester tests: Pester-TestFunctions.ps1

Note: The test script specifically breaks out the Windows Update test since it can take a while to perform. PowerShell logic is used in order to determine whether or not this test is performed.

Test-Server.ps1

# Version:: 0.1.5 (12/19/2016)
# Script Description:: Configures a server for a specific customer.
#
# Author(s):: Otto Helweg
#

param($serverName)

# Display help
if (($Args -match "-\?|--\?|-help|--help|/\?|/help") -or (!($serverName))) {
  Write-Host "Usage: Test-Server.ps1"
  Write-Host "     -serverName [server name or ip]    (required) Specify a specific server"
  Write-Host "     -u                                 Also test for no updates available"
  Write-Host ""
  Write-Host "Examples:"
  Write-Host "     Test-Server.ps1 -serverName server01 -u"
  Write-Host ""
  exit
}

# Create PowerShell Remoting access creds
$username = "someUser"
$password = "somePassword"

$securePassword = ConvertTo-SecureString -String $password -AsPlainText -Force
$psCreds = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $securePassword

if ($limit -eq "none") {
  $limit = $false
} elseif ($limit) {
  $limit = $limit.Split(",")
}

Write-Host "Working on $serverName"
if ($Args -contains "-u"){
  $tests = "updates,"
} else {
  $tests = ""
}

$output = Invoke-Command -ComputerName $serverName -Credential $psCreds -ScriptBlock { Test-Path "c:\PESTER" }

if (!($output)) {
  $output = Invoke-Command -ComputerName $serverName -Credential $psCreds -ScriptBlock { New-Item -Type Directory "c:\PESTER" -Force }
}

# Set execution policy to allow for running scripts
$execPolicy = Invoke-Command -ComputerName $serverName -Credential $psCreds -ScriptBlock { Get-ExecutionPolicy }
if ($execPolicy -ne "Unrestricted") {
  Write-Host "Temporarily modifying script execution policy"
  Invoke-Command -ComputerName $serverName -Credential $psCreds -ScriptBlock { Set-ExecutionPolicy Unrestricted -Force }
  $policyChanged = $true
}

Invoke-Command -ComputerName $serverName -Credential $psCreds -ScriptBlock {
  New-Item -Type Directory 'C:\PESTER\' -Force
  $Destination = 'C:\PESTER\Pester-master.zip'
  $Source = 'https://github.com/pester/Pester/archive/master.zip'
  $client = new-object System.Net.WebClient
  $client.DownloadFile($Source, $Destination)

  $shell = new-object -com shell.application
  $zip = $shell.NameSpace('C:\PESTER\Pester-master.zip')
  foreach($item in $zip.items()) {
    $shell.Namespace('C:\PESTER').copyhere($item)
  }
}

$filesToTransfer = @("azure.Tests.ps1","Pester-TestFunctions.ps1")
foreach ($file in $filesToTransfer) {
  if (Test-Path ".\$file") {
    Write-Host "Transferring file $file"
    $fileContent = Get-Content ".\$file"
    Invoke-Command -ComputerName $serverName -Credential $psCreds -ScriptBlock { param($content,$fileName); $content | Set-Content -Path "c:\PESTER\$fileName" -Force } -ArgumentList $fileContent,$file
  }
}

Write-Host "Performing the following additional tests: $tests"
$output = Invoke-Command -ComputerName $serverName -Credential $psCreds -ScriptBlock { param($tests); Set-Location 'C:\PESTER\'; .\azure.Tests.ps1 -tests $tests } -ArgumentList $tests

$output = Invoke-Command -ComputerName $serverName -Credential $psCreds -ScriptBlock { Remove-Item 'C:\PESTER\' -Recurse -Force }
# Reset the script execution policy
if ($policyChanged) {
  Invoke-Command -ComputerName $serverName -Credential $psCreds -ScriptBlock { Set-ExecutionPolicy $Args -Force } -ArgumentList $execPolicy
}

Azure.test.ps1

param($tests)
########## BEGIN SCRIPT HEADER ##########
$TITLE = "DATAPIPE OLDCASTLE PESTER TESTS"
#Authors: Otto Helweg
$Global:Version = "1.0.5"
$Date = "12/10/2016"
##########  END SCRIPT HEADER  ##########

<#
.SYNOPSIS
========================================================================================
AUTOMATION
========================================================================================
These are a set of tests to verify the proper configuration of a Windows Server
#>

Import-Module .\Pester-master\Pester.psm1
. ".\Pester-TestFunctions.ps1"

$extraTests = $tests.Split(",")

if (!($blockSize)) {
  $blockSize = "65536"
}

Describe "$TITLE" {
  It "BigFix should be installed" {
    Test-InstallBES | Should Be "IBM Endpoint Manager Client"
  }

  if ($extraTests -contains "updates") {
    It "No Windows updates should be available" {
      Test-InstallWindowsUpdates | Should Be 0
    }
  }

  It "Firewall should be disabled" {
    Test-DisableWindowsFW | Should Be "Disabled"
  }

  It "RDP session count should be 0" {
    Test-EnableMultipleRDP | Should Be 0
  }

  It "Windows Update check should be disabled" {
    Test-DisableWindowsUpdateCheck | Should Be 1
  }

 It "Volume notification should be disabled" {
   Test-DisableVolumeNotification | Should Be 1
 }

 It "IEESC should be disabled" {
   Test-DisableIEESC | Should Be "Disabled"
 }

  It "UAC should be disabled" {
    Test-DisableUAC | Should Be 0
  }

  It "Should be domain joined" {
    Test-DomainJoin | Should Be 1
  }

  if ($extraTests -like "*drive*") {
    foreach ($test in $extraTests) {
      if ($test -like "*drive*") {
        $driveLetter = $test.Substring(($test.Length - 1),1)
        if ($driveLetter) {
          It "$($driveLetter): volume should exist" {
            Test-DriveLetter $driveLetter | Should Be 1
          }

          It "$($driveLetter): volume should have $blockSize byte blocks" {
            Test-DriveBlockSize $driveLetter $blockSize | Should Be 1
          }
        }
      }
    }
  }

  It ".Net 3.5 should be installed" {
    Test-DotNet35 | Should Be 1
  }

  if ($extraTests -contains "sql") {
    It "SQL should be installed and running" {
      Test-SQLRunning | Should Be 1
    }
    It "SQL remote backup storage should be configured" {
      Test-SQLBackupStorage | Should Be 1
    }
    It "SQL should be configured" {
      Test-SQLConfig | Should Be 1
    }
  }
}

Pester-TestFunctions.ps1

Note: Notice that these test functions are written in PowerShell and executed locally on the remote server. They are examples of the various ways PowerShell can be used to check the state of a server (e.g. Registry check, WMI query, etc.)

########## BEGIN SCRIPT HEADER ##########
$TITLE = "PESTER TEST FUNCTIONS"
#Authors: Otto Helweg
$Global:Version = "1.0.5"
$Date = "12/10/2016"
##########  END SCRIPT HEADER  ##########

<#
.SYNOPSIS
========================================================================================
AUTOMATION
========================================================================================
This is a suite of test functions called by Pester in order to verify the configuration of a Windows Sever
by using Pester tests. These can also be called directly by 'dot sourcing' this file by incluiding this command:
  . ".\WindowsPSM-TestFunctions.ps1"

These functions are named to mimic their sister functions defined in the 'WindowsPSM.psm1' PowerShell Module

#>


function Test-InstallBES {
  $wmiOutput = Get-WmiObject -Query "select * from Win32_Product where Name = 'IBM Enoint Manager Client'"
  $($wmiOutput.Name)
}

function Test-InstallWindowsUpdates {
  $UpdateSession = New-Object -com Microsoft.Update.Session
  $UpdateSearcher = $UpdateSession.CreateupdateSearcher()
  $SearchResult =  $UpdateSearcher.Search("IsAssigned=1 and IsHidden=0 and IsInstalled=0")
  $UpdateLowNumber = 0
  $UpdateHighNumber = 2
  $searchResult.Updates.Count
}

function Test-DisableWindowsFW {
  $firewallState = "Disabled"
  foreach ($profile in $fwProfile) {
    $netshOutput = netsh advfirewall show $profile state
    foreach ($item in $netshOutput) {
      if (($item -like "State*") -and (!($item -like "*OFF"))) {
        $firewallState = "Enabled"
      }
    }
  }
  $firewallState
}

function Test-EnableMultipleRDP {
  $sessionCount = 1
  $sessionCount = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server").fSingleSessionPerUser
  $sessionCount
}

function Test-DisableWindowsUpdateCheck {
  $updateCheck = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\").AUOptions
  $updateCheck
}

function Test-DisableVolumeNotification {
  $volumeNotification = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer").HideSCAVolume
  $volumeNotification
}

function Test-DisableIEESC {
  if ((((Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A7-37EF-4b3f-8CFC-4F3A74704073}").IsInstalled) -eq '0') -and ((((Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A8-37EF-4b3f-8CFC-4F3A74704073}").IsInstalled -eq 0)))) {
    $ieescState = "Disabled"
  }
  $ieescState
}

function Test-DisableUAC {
  $uacState = 1
  $uacState = (Get-ItemProperty "HKLM:\Software\Microsoft\Windows\CurrentVersion\policies\system").EnableLUA
  $uacState
}

function Test-DomainJoin {
  $domainCheck = (Get-WmiObject -Class win32_computersystem).Domain
  if ($domainCheck) {
    $true
  }
}

function Test-DriveLetter($driveLetter) {
  $volOutput = Get-Volume $driveLetter -erroraction silentlycontinue
  if ($volOutput) {
    $true
  }
}

function Test-DriveBlockSize($driveLetter,$blockSize) {
  $wql = "SELECT Label, Blocksize, Name FROM Win32_Volume WHERE FileSystem='NTFS' AND Name='$($driveLetter):\\'"
  $diskInfo = Get-WmiObject -Query $wql -ComputerName '.' | Select-Object Label, Blocksize, Name
  if ($diskInfo.BlockSize -eq "$blockSize") {
    $true
  }
}

function Test-DotNet35 {
  if (Get-WindowsFeature -Name "NET-Framework-Core") {
    $true
  }
}

function Test-SQLRunning {
  $output = Get-Service -Name "MSSQLSERVER" -ErrorAction SilentlyContinue
  if ($output.Status -eq "Running") {
    $true
  }
}

Output will look something like:

PS C:\pester> .\Test-Server.ps1 -servername server01 -u
Working on server01
Temporarily modifying script execution policy
    Directory: C:\
Mode                LastWriteTime         Length Name                                PSComputerName
----                -------------         ------ ----                                --------------
d----          1/9/2017   3:55 PM                PESTER                              10.10.10.10
Transferring file Azure.tests.ps1
Transferring file Pester-TestFunctions.ps1
Performing the following additional tests: updates,driveu,drivel,sql,
Describing PESTER TEST FUNCTIONS
 [+] No Windows updates should be available 6.98s
 [+] Firewall should be disabled 179ms
 [+] RDP session count should be 0 16ms
 [+] Windows Update check should be disabled 12ms
 [+] UAC should be disabled 12ms
 [+] Should be domain joined 41ms
 [+] u: volume should exist 3.58s
 [+] u: volume should have 65536 byte blocks 100ms
 [+] l: volume should exist 184ms
 [+] l: volume should have 65536 byte blocks 15ms
 [+] .Net 3.5 should be installed 325ms
 [+] SQL should be installed and running 16ms
 [+] SQL remote backup storage should be configured 92ms
 [+] SQL should be configured 22ms

Enjoy!