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.


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. 


# 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 {
  Write-Host "Running command from $secondServerName"
  Invoke-Command -ComputerName $secondServerName -Credential $creds -ScriptBlock {
    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’.


Ansible vs. Chef for Managing Windows

ConfigureRemotingForAnsible.ps1Summary: Ansible is a simple and powerful application/DevOps framework for managing Windows configuration and provisioning. Getting off the ground with Ansible was also fairly straightforward and doesn’t require a large time or infrastructure investment.

These are some initial thoughts about using Ansible to manage the Windows platform (specifically compared to Chef). This analysis does not consider the ways in which Chef might be a better choice than Ansible for Windows management (they do exist and might be the subject of another blog post in the future).

The Ansible pilot was setup to be able to mimic some existing Chef recipes in order to determine time investment and infrastructure requirements as well as Ansible capabilities. The pilot consisted of an Ansible server (CentOS 6 – 64bit) and a Windows server (Windows Server 2012 Standard Edition) to be managed. Findings were as follows (in no particular order):

Note: Ansible Module  = Chef Resource, Ansible Playbook = Chef Recipe

  • Agentless: Ansible does not use an agent to manage Windows, but merely uses Windows’ built in Windows Remote Management (WinRM) protocol and framework.
  • WinRM Configuration: The PowerShell script ConfigureRemotingForAnsible.ps1 needs to be run on the managed node in order to enable communication with the Ansible server. The script basically configures a custom HTTPS listener with a special certificate.
  • PowerShell 3.0: PowerShell 3.0 or above is required by Ansible. With PowerShell 3.0, the following Hotfix KB2842230 may also need to be installed. On the other hand, Chef works well with PowerShell 2.0. PowerShell 3.0 can be easily updated on versions of Windows Server (pre 2012 came with PowerShell 2.0) by installing the Windows Management Framework (WMF) 3.0.
  • Predictable Execution: Ansible playbooks have a single execution phase, rather than Chef’s compile then execute phases, which, in some cases, can make Chef recipes less predictable. For example in Chef, modifying an environmental variable several times on a managed node within a single run_list will produce unexpected results.
  • Fewer Facts: Far fewer Ansible Facts are discovered at runtime than Ohai Attributes for a Windows host (this is not so with Linux). Chef’s Ohai discovers the same mountain of properties on both Windows and Linux.
  • Parameters: Parameters can be passed into the Ansible Playbook from the command line which is useful for changing Playbook behavior when it’s executed.
  • PowerShell: Ansible runs pure PowerShell scripts “as is”. Chef requires PowerShell scripts to be slightly modified by escaping certain characters. Ansible also simply manages the transfer of the script to the managed node, script execution, and script removal.
  • External File Transfers: The Ansible URL module nicely transfers big files via a URL. This is convenient when using Artifactory or Pydio to pull down large binaries for installation or processing.
  • Unzipping Files: The Ansible ZIP module is simple to use and nicely expands compressed files on the managed node.
  • Variable Passing: Variables can be easily passed out of a PowerShell script to the Playbook (or other modules) during runtime. This is useful when PowerShell is used to dynamically gather or process needed data at runtime. Although Chef easily allows for the passing of Attributes into PowerShell scripts, pulling data back out of those scripts is tricky.
  • Windows Update Works! The Windows Update module works (no permissions issues, I don’t know how Ansible is accomplishing this because this is a known issue with Chef). The problem lies in Windows not granting access to certain internal methods when accessed remotely, even with Administrator credentials. To see how this can be overcome with Chef, go to this blog post on the topic.
  • Reboot Management: Ansible playbooks can easily manage reboots since the Playbook is being run from the Ansible server and not the Node.
  • User Input: An Ansible Playbook can take user input during runtime. For example getting runtime credentials when joining a Windows node to a domain.

Windows Update Playbook Example:

# This playbook installs Windows updates
# Run with the following command:
#   ansible-playbook update-win.yml --ask-pass --u Administrator

- name: Configure Server
  hosts: windows
  gather_facts: true
    - name: Install Windows updates
        category_names: ['SecurityUpdates','CriticalUpdates','UpdateRollups','Updates']

    - name: Restart machine
      raw: shutdown /r /f /c "Ansible updates triggered"
      async: 0
      poll: 0
      ignore_errors: true

    - name: Waiting for server to come back
      local_action: wait_for
                    host={{ inventory_hostname }}
      sudo: false

Windows Domain Join Playbook Example:

# This playbook joins Windows to a domain
# Run with the following command:
#   ansible-playbook joindomain-win.yml --ask-pass --u Administrator

- name: Join domain
  hosts: windows
  gather_facts: true
    - name: "user"
      prompt: "Domain Join username"
      private: no
    - name: "password"
      prompt: "Domain Join password"
      private: yes
    - name: Join domain script
      script: "files/join-domain.ps1 -u '{{ user }}' -p '{{ password }}'"
      ignore_errors: true

    - name: Waiting for server to come back
      local_action: wait_for
                    host={{ inventory_hostname }}
      sudo: false

Windows Domain Join PowerShell Script Example:

# Script:: join-domain.ps1
# Joins a sesrver to a domain


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

$domainCheck = (Get-WmiObject -Class win32_computersystem).Domain
if (!($domainCheck -eq "")) {
  Add-Computer -DomainName "" -Credential $psCreds -Force -Restart

  eventcreate /t INFORMATION /ID 1 /L APPLICATION /SO "Ansible-Playbook" /D "joindomain-win: Added to the domain ''."