What triggers this error
You run an Ansible playbook targeting a Windows host and get slapped with:
fatal: [win_host]: UNREACHABLE! => {"msg": "ssl: HTTPSConnectionPool(host='192.168.1.10', port=5986): Max retries exceeded with url: /wsman"}
The connection never even got off the ground. Three things typically cause this: the port isn't open, the SSL handshake blew up, or Ansible doesn't trust the certificate. Pick the one that fits your situation and work through the steps below.
Root causes
- WinRM HTTPS listener not configured on the Windows host
- Port 5986 blocked by Windows Firewall or an upstream network firewall
- Self-signed certificate not trusted by Ansible (missing
validate_certs: false) pywinrmpackage missing or outdated on the Ansible controller- Wrong
ansible_winrm_transportor connection variables in inventory
Quick fix: disable cert validation for testing
Most internal labs and staging environments use self-signed certificates. By default, Ansible refuses those connections outright. Add these lines to your inventory or group_vars to bypass cert validation while you diagnose:
[windows:vars]
ansible_user=Administrator
ansible_password=YourPassword
ansible_connection=winrm
ansible_winrm_scheme=https
ansible_port=5986
ansible_winrm_transport=ntlm
ansible_winrm_server_cert_validation=ignore
Then fire a quick connectivity test:
ansible win_host -m win_ping
If that succeeds, the SSL certificate is your culprit โ nothing else. Skip ahead to the permanent fix section.
Step 1: Verify WinRM is actually running
RDP or console into the Windows machine and run these in PowerShell:
Get-Service WinRM
winrm enumerate winrm/config/listener
You need a listener showing Transport=HTTPS on port 5986. If the output only shows HTTP on port 5985, the HTTPS listener was never set up.
Stand up WinRM HTTPS with a self-signed cert in one go:
# Run on the Windows host as Administrator
$cert = New-SelfSignedCertificate -DnsName "$(hostname)" -CertStoreLocation Cert:\LocalMachine\My
$thumbprint = $cert.Thumbprint
New-WSManInstance -ResourceURI winrm/config/listener `
-SelectorSet @{Address="*"; Transport="HTTPS"} `
-ValueSet @{CertificateThumbprint=$thumbprint}
# Prefer the Ansible-maintained setup script instead:
$url = "https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1"
$file = "$env:temp\ConfigureRemotingForAnsible.ps1"
(New-Object -TypeName System.Net.WebClient).DownloadFile($url, $file)
powershell.exe -ExecutionPolicy ByPass -File $file
The Ansible script handles cert creation, listener setup, and firewall rules in one shot โ worth using if you're setting up more than one host.
Step 2: Open port 5986 in Windows Firewall
New-NetFirewallRule -DisplayName "Ansible WinRM HTTPS" `
-Direction Inbound `
-Protocol TCP `
-LocalPort 5986 `
-Action Allow
Running on AWS, Azure, or GCP? Don't forget the security group or NSG โ Windows Firewall rules mean nothing if the cloud layer is blocking traffic at the VPC level.
From the Ansible controller, test whether port 5986 is actually reachable:
nc -zv 192.168.1.10 5986
# or
curl -k https://192.168.1.10:5986/wsman
Even an HTTP 401 or 500 back from curl means the port is open and WinRM is alive. A connection refused or timeout means the firewall is still blocking it.
Step 3: Install pywinrm on the Ansible controller
No pywinrm, no WinRM connections โ Ansible needs this package to speak the protocol at all:
pip install pywinrm
pip install pywinrm[kerberos] # domain environments using Kerberos
pip install pywinrm[credssp] # if using CredSSP auth
Confirm it's installed and check the version (you want 0.4.x or later):
pip show pywinrm
Permanent fix: trust the Windows certificate
Shipping server_cert_validation=ignore to production is a bad idea. It opens the door to man-in-the-middle attacks on your automation. The real fix is exporting the Windows host certificate and adding it to the trusted store on your Ansible controller.
On the Windows host, export the cert:
$cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object {$_.Subject -match "$(hostname)"}
Export-Certificate -Cert $cert -FilePath C:\winrm_cert.cer -Type CERT
Copy winrm_cert.cer to your Ansible controller (scp, SMB share, whatever you have), then convert and install it:
# Convert .cer (DER format) to PEM
openssl x509 -inform DER -in winrm_cert.cer -out winrm_cert.pem
# Ubuntu/Debian
cp winrm_cert.pem /usr/local/share/ca-certificates/winrm_cert.crt
update-ca-certificates
# RHEL/CentOS
cp winrm_cert.pem /etc/pki/ca-trust/source/anchors/
update-ca-trust extract
Then update your inventory to validate properly:
[windows:vars]
ansible_winrm_server_cert_validation=validate
ansible_winrm_ca_trust_path=/path/to/winrm_cert.pem
Reference: full working inventory
Here's a complete inventory that works with NTLM over HTTPS โ copy and adapt it:
[windows]
win_host ansible_host=192.168.1.10
[windows:vars]
ansible_user=Administrator
ansible_password=YourSecurePassword
ansible_connection=winrm
ansible_winrm_scheme=https
ansible_port=5986
ansible_winrm_transport=ntlm
ansible_winrm_server_cert_validation=ignore
Domain-joined hosts with Active Directory? Switch to Kerberos โ it's more secure and handles token delegation properly:
ansible_winrm_transport=kerberos
ansible_user=user@DOMAIN.COM
Verify the fix
Start with win_ping โ the lowest-overhead connectivity test Ansible has for Windows:
ansible win_host -m win_ping
Expected output:
win_host | SUCCESS => {
"changed": false,
"ping": "pong"
}
Once ping passes, run a real command to confirm the full stack works end-to-end:
ansible win_host -m win_shell -a "Get-ComputerInfo | Select-Object WindowsProductName"
Still seeing the SSL error after all of this? Crank verbosity to four and grep the handshake trace:
ansible win_host -m win_ping -vvvv 2>&1 | grep -i ssl
The handshake output will tell you exactly where it's failing โ certificate CN mismatch, expired cert, untrusted CA, and so on.

