Configuring Ansible To Manage A Windows Host Using WinRM

I recently setup Ansible to communicate with a Windows host. Unlike NIX-based hosts (Linux/Unix), which use SSH by default, Windows hosts are not a good fit for SSH configuration with Ansible. As per the Ansible documentation, “use this (SSH with Windows) feature at your own risk! Using SSH with Windows is experimental, the implementation may make backwards incompatible changes in feature releases. The server side components can be unreliable depending on the version that is installed”. So instead, I choose the WinRM setup. Here’s how I did it.

What Is WinRM

WinRM (Windows Remote Management) is a management protocol used by Windows to remotely communicate with another server. It is a SOAP-based protocol that communicates over HTTP/HTTPS, and is included in all recent Windows operating systems. Since Windows Server 2012, WinRM has been enabled by default, but in most cases extra configuration is required to use WinRM with Ansible.

Certificate Authentication

When connecting to a Windows host, there are several different options that can be used when authenticating with an account. The preferred authentication method used for this demo is certificate based authentication.

Certificate authentication uses certificates as keys similar to SSH key pairs, but the file format and key generation process is different.

Setup WinRM

Install Python 3 on Ansible Host

As you already know, there are two Python versions that are being actively developed. While Python 2 is well-supported and active, Python 3 is considered to be the present and future of the language.

By default RHEL/CentOS 8 doesn’t have an unversioned system-wide python command to avoid locking the users to a specific version of Python. Instead, it gives the user a choice to install, configure, and run a specific Python version. The system tools such as yum use an internal Python binary and libraries.

To install Python 3 on CentOS 8 run the following command:

# dnf install python3

Install PIP on Ansible Host

Pip is Python’s package manager, which is also comes pre-installed, but again, in case Pip is missing on your system, install it using the command. Note, to install the pip version that matches python version.

# dnf install python3-pip

Install Ansible Automation Tool

With all the prerequisites met, install ansible by running the command on CentOS 8.

# pip3 install ansible –user
# ansible --version

To test ansible, first ensure that ssh is up and running

# systemctl status sshd

Next, we need to create the hosts file in the /etc/ansible directory to define host machines.

# mkdir /etc/ansible  
# cd /etc/ansible
# sudo touch hosts

Install pywinrm

Ansible uses the pywinrm package to communicate with Windows servers over WinRM. It is not installed by default with the Ansible package.  Use the command below to install pywinrm. Note, on distributions with multiple python versions, use pip2 or pip2.x, where x matches the python minor version Ansible is running under.

# pip3 install pywinrm

Enable Certificate Authentication On Windows

Certificate authentication is not enabled by default on a Windows host but can be enabled by running the following in PowerShell.

PS> Set-Item -Path WSMan:\localhost\Service\Auth\Certificate -Value $true

Generate Certificate on Windows

A certificate must be generated before it can be mapped to a local user. This can be done using various methods. The preferred method used for this demo is to use PowerShell (using the “New-SelfSignedCertificate” cmdlet) to generate a certificate. This approach only works when being generated from a Windows 10 or Windows Server 2012 R2 host or later.

Copy the code below into a PowerShell script called generate-cert.ps1, then execute the script:

# Set the name of the local user that will have the key mapped
$username = "username"
$output_path = "C:\temp"

# Instead of generating a file, the cert will be added to the personal
# LocalComputer folder in the certificate store
$cert = New-SelfSignedCertificate -Type Custom `
    -Subject "CN=$username" `
    -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.2","2.5.29.17={text}upn=$username@localhost") `
    -KeyUsage DigitalSignature,KeyEncipherment `
    -KeyAlgorithm RSA `
    -KeyLength 2048

# Export the public key
$pem_output = @()
$pem_output += "-----BEGIN CERTIFICATE-----"
$pem_output += [System.Convert]::ToBase64String($cert.RawData) -replace ".{64}", "$&`n"
$pem_output += "-----END CERTIFICATE-----"
[System.IO.File]::WriteAllLines("$output_path\cert.pem", $pem_output)

# Export the private key in a PFX file
[System.IO.File]::WriteAllBytes("$output_path\cert.pfx", $cert.Export("Pfx"))

Convert PFX File on Windows

  • The public key should’ve been exported as C:\temp\cert.pem
  • The private key is a PFX file exported as C:\temp\cert.pfx
  • To convert the PFX file to a private key that pywinrm can use, run the following command with OpenSSL
OpenSSL> pkcs12 -in cert.pfx -nocerts -nodes -out cert_key.pem -passin pass: -passout pass:

Import to Certificate Store on Windows

Once a certificate has been generated to C:\temp, the issuing certificate needs to be imported into the Trusted Root Certificate Authorities of the LocalMachine store, and the client certificate public key must be present in the Trusted People folder of the LocalMachine store. For this example, both the issuing certificate and public key are the same. Simply copy and past the code below into a PowerShell script called import-cert.ps1, then execute the script.

$cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2
$cert.Import("cert.pem")
$store_name = [System.Security.Cryptography.X509Certificates.StoreName]::Root
$store_location = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine
$store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store - 
ArgumentList $store_name, $store_location
$store.Open("MaxAllowed")
$store.Add($cert)
$store.Close()

Map Certificate to Account on Windows

Once the certificate has been imported, map it to the local user account by simply copying the code below into a PowerShell script called map-cert.ps1, then execute the script. Note, change the “username” and “password” in code below:

$username = "Put Your Username"
$password = ConvertTo-SecureString -String "Put Your Password" -AsPlainText -Force
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList
$username, $password
# This is the issuer thumbprint which in the case of a self generated cert
# is the public key thumbprint, additional logic may be required for other
# scenarios
$thumbprint = (Get-ChildItem -Path cert:\LocalMachine\root | Where-Object { $_.Subject -eq
"CN=$username" }).Thumbprint
New-Item -Path WSMan:\localhost\ClientCertificate `
-Subject "$username@localhost" `
-URI * `
-Issuer $thumbprint `
-Credential $credential `
-Force

Copy Public and Private Key

Copy the following keys from Windows target to Ansible Controller

  • Publick Key = C:\temp\cert.pem
  • Private Key = C:\temp\cert_key.pem

Update Ansible Inventory

For this demo, the public and private keys were copied to /root. The following example shows host vars configured in /etc/ansible/hosts for certificate authentication:

[win]
34.239.161.196

[win:vars]
ansible_connection=winrm
ansible_winrm_cert_pem=/root/cert.pem
ansible_winrm_cert_key_pem=/root/cert_key.pem
ansible_winrm_transport=certificate
ansible_winrm_server_cert_validation=ignore

Note, I’ve added “ansible_winrm_server_cert_validation=ignore” because there seems to be some issues with the current version of python that I’m using. I will update this blog once I get to the bottom of this.