Python – Ansible for Windows: WinRM HTTPS setup

ansiblehttpspythonsslwinrm

I have some Ansible playbooks I want to run against some Windows hosts. I've followed the various Ansible guides for setting up WinRM and they have worked fine, but the default setups are very insecure and I want something more production ready. However, the instructions for how to do this are incredibly sparse. So far I've done the following:

On my Windows box:

  1. Enabled WinRM using the supplied ConfigureRemotingForAnsible.ps1 script
  2. Configured target machine to to use a valid cert on HTTPS/5986 rather than the self-signed one generated by the above script
  3. Enabled both Kerberos and CredSSP auth methods on the target machine in WinRM. Some of my role steps require CredSSP to work reliably.

So far so good, the Windows side seems to work fine. However, getting Ansible to connect is proving a nightmare. I can't figure out how to get Ansible to trust the HTTPS cert on the target despite adding it. On my Centos 7 'push box' I've done the following:

  1. Install Ansible and pip with the pywinrm, requests_kerberos and requests_credssp modules
  2. Added my CA certificate to both /etc/pki/tls/certs and /etc/pki/ca-cert
  3. Set my inventory to the following:

ansible_user=ADMINISTRATOR@DOMAIN.COM

ansible_password=Password1

ansible_port=5986

ansible_connection=winrm

ansible_winrm_scheme=https

/#ansible_winrm_server_cert_validation=ignore

ansible_winrm_transport=credssp

With certification validation turned on the connection fails with the following error:

fatal: [host.domain.com]: 
UNREACHABLE! => {
"changed": false, 
"msg": "credssp: HTTPSConnectionPool(host='host.domain.com', port=5986): Max retries exceeded with url: /wsman (Caused by SSLError(SSLError(\"bad handshake: Error([('SSL routines', 'tls_process_server_certificate', 'certificate verify failed')],)\",),))", 
"unreachable": true}

With cert validation turned off it works fine.

So my question: How do I get Ansible to trust my CA cert?

Best Answer

This isn't simple. The problem is there are 3 different groups that don't really want to play:

  1. Ansible: doesn't want to be responsible for OpenSSL or Windows details
  2. OpenSSL: doesn't want to be used as a certficate authority
  3. Microsoft: doesn't want you to use anything but Windows tools

First, you will need 3 sets of OpenSSL certs/keys: the self-signed Certificate Authority (CA), the server winRM HTTPS, and the target user.

There is nothing magic about the CA cert/key; it can be generated with the following:

openssl req -new -out caroot.req -keyout caroot.key -days 730 ...
openssl x509 -req -in caroot.req -extensions v3_ca -signkey caroot.key -out cacert.pem -days 730

There are a myriad of other options to these OpenSSL commands which may (or not) work in your specific environment. This is one of those instances of "don't want to be responsible for OpenSSL".

Second, you will need the WinRM HTTPS cert/key. First, you have to generate the server cert. The key detail of generating the cert is that the 'common name' attribute on the cert MUST match the target hostname, as with any HTTPS cert.

openssl req -new -out server.req -keyout server.key -days 730 -nodes ...

Now, here comes the tricky part. You can't just sign the key as-is; the signing process must add a couple of additional attributes to the cert or it won't work. Create a text file I'll call 'attributes.txt' with the following two lines:

subjectAltName=DNS:hostname.mycompany.com,DNS:hostname
extendedKeyUsage=serverAuth

The first line, the 'subjectAltName', is required or the Python/OpenSSL client will reject the key. You need to substitute the proper values for the target hostname. The second line is required by Windows, or Windows will not use this as for an HTTPS server.

Now sign the key with the CA generated earlier:

openssl x509 -req -in server.req -out server.pem -days 730 -CA caroot.pem -CAkey caroot.key -extfile attributes.txt

Another little wrinkle: Windows will not import a server key unless you combine the key and cert into a PKCS#12 file. This can be done with the following command, which creates a PKCS#12 file with no password:

openssl pkcs12 -export -password pass: -inkey server.key -in server.pem -out windows.pfx

This will generate a file 'windows.pfx' which we'll need later.

Bored yet?

Next we need another key/cert for the user login. This is assuming that we're going to be using a local user on the system, not a domain user. (You're on your own for that.) Generating the key is almost the same as for the server key, except that the 'commom name' attribute (CN) MUST match the target username. (In later usage, I'll call our user 'ansible').

openssl req -new -out winlogin.req -keyout winlogin.key -days 730

This private key is going to be the login key, analogous to an SSH private key. Next, we must sign the req. As with the server key, we need to add some extended attributes, or it won't work. Our 'attributes.txt' file must contain the following lines:

subjectAltName=otherName:1.3.6.1.4.1.311.20.2.3;UTF8:ansible@localhost
extendedKeyUsage=clientAuth

The oddball identifier in the 'altNames' is some proprietary Microsoft thingy, which must contain the 'username@localhost'. As I mentioned before, I'm using a local user 'ansible'. The extendedKeyUsage attribute is required or Windows will not allow the key to be used for user authentication. Finally, we sign the cert:

openssl x509 -req -in winlogin.req -out winlogin.pem -days 730 -CA caroot.pem -CAkey caroot.key -extfile attributes.txt

Just so you know, I'm starting to get on my own nerves here. We're almost there. Finally, copy the CA cert (caroot.pem), the server PKCS#12 cert/key (windows.pfx), and the user cert (winlogin.pem) to the target windows system somewhere. Run the following PowerShell script from the same directory. It will create the local ansible user and import all of the SSL artifacts into their proper target locations. Sorry if this isn't commercial quality code. This also has a hardcoded password for the ansible user, but that is irrelevant and can be freely changed. If this actually runs successfully, and you survive the shock, the file artifacts (the PFX and PEM files) can be removed from the Windows server.

Also note that this script adds a firewall rule at the end. This, of course, will have to be appropriately modified for your environment.

# master script to enable winRM via HTTPS and allow certificate-based 
# authentication

# add the local 'ansible' user
$username = "ansible"
$password = ConvertTo-SecureString -string "Ans!b123" -AsPlainText -Force
New-LocalUser -Name $username -AccountNeverExpires -Description "Ansible Remote Management" -Password $password
Add-LocalGroupMember -Member "ansible" -Group "Administrators"
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $password

# import the Ansible root CA; certlm should show this in the 
# 'Trusted Root Certification Authorities' folder
$caroot = Import-Certificate -FilePath ".\caroot.pem" -CertStoreLocation "Cert:\LocalMachine\Root"

# import the user cert; certlm should show this in the 'Trusted People' folder
$userkey = Import-Certificate -FilePath ".\winlogin.pem" -CertStoreLocation "Cert:\LocalMachine\TrustedPeople"
New-Item -Path WSMan:\localhost\ClientCertificate -subject "ansible@localhost" -URI "*" -Issuer $caroot.Thumbprint -Credential $credential -Force

# import the server certs - should appear in 'Personal' folder. The PFX file
# must contain both the cert and private key
$srvcert = Get-ChildItem -Path ".\windows.pfx" | Import-PFXCertificate -CertStoreLocation "Cert:\LocalMachine\MY" -Exportable

# Now create the winRM instance
$selector_set = @{Address= "*" Transport = "HTTPS" }

$value_set =  @{CertificateThumbprint = $srvcert.Thumbprint}

New-WSManInstance -ResourceURI "winrm/config/Listener" -SelectorSet $selector_set -ValueSet $value_set
Set-Item -Path WSMan:\localhost\Service\Auth\Certificate -value $true

# add a firewall rule
New-NetFirewallRule -DisplayName "Ansible WinRM" -Direction Inbound -Protocol "TCP" -LocalPort "5986" -Action Allow -RemoteAddress @("192.16.2.3")

Okay, now that Windows is done, we should be ready for Ansible. Within your inventory, you will need the following variables to apply to the Windows systems:

ansible_port: 5986
ansible_connection: winrm
ansible_winrm_ca_trust_path: /path/to/caroot.pem
ansible_winrm_transport: certficate
ansible_winrm_cert_pem: /path/to/winlogin.pem
ansible_winrm_cert_key_pem: /path/to/winlogin.key

At this point, Ansible should connect to the Windows server using HTTPS and certificate authentication. I hope I hit all of the steps, it was a while ago I set this up.

Related Topic