If you’ve only used Ansible on Linux, Windows is a different animal. There’s no SSH, no Bash, and the module names all change. Windows Server relies on WinRM (Windows Remote Management) for connectivity and PowerShell under the hood, which means your inventory files, connection variables, and playbooks all need adjustments.
This guide walks through the full setup: configuring WinRM on a Windows Server 2025 target, installing the required Ansible collections and Python libraries, testing connectivity, and then putting it all to work with real playbooks for IIS deployment, user management, registry edits, firewall rules, and more. Every command and output here was captured from a live Windows Server 2025 Datacenter instance. If you need a refresher on installing Ansible on Linux, start there first.
Tested April 2026 on Windows Server 2025 Datacenter with ansible-core 2.16.14, ansible.windows 3.5.0, community.windows 3.1.0
Prerequisites
Before starting, confirm you have the following ready:
- A Linux control node (Rocky Linux 10 or Ubuntu 24.04) with Ansible installed
- Windows Server 2025 or 2022 target with Administrator access
- Network connectivity between the control node and the Windows Server on port 5985 (HTTP) or 5986 (HTTPS)
- Tested on: ansible-core 2.16.14, Windows Server 2025 Datacenter, Python 3.12
Install Windows Collections and Dependencies
Ansible doesn’t ship with Windows modules by default. You need two collections: ansible.windows for core Windows modules and community.windows for extended functionality like Windows Updates and advanced firewall rules.
Install both collections on the control node:
ansible-galaxy collection install ansible.windows community.windows
Ansible also needs pywinrm, the Python library that handles WinRM communication. Without it, you’ll see this error when trying to connect:
winrm or requests is not installed: No module named 'winrm'
Install pywinrm with pip:
pip3 install pywinrm
Verify the collections are installed:
ansible-galaxy collection list | grep -E "ansible.windows|community.windows"
You should see both collections with their version numbers:
ansible.windows 3.5.0
community.windows 3.1.0
Configure WinRM on Windows Server
WinRM is the protocol Ansible uses to talk to Windows targets, replacing SSH. Windows Server 2025 has WinRM installed but the default configuration blocks Ansible’s basic authentication. You need to enable a few settings in PowerShell on the Windows Server.
Open an elevated PowerShell prompt on the Windows Server and check whether WinRM is running:
Get-Service WinRM
If the service isn’t running, start and enable it:
Enable-PSRemoting -Force
Enable basic authentication. This is disabled by default on Windows Server 2025. The Ansible cheat sheet covers the equivalent Linux connection settings if you need a quick reference:
Set-Item WSMan:\localhost\Service\Auth\Basic -Value $true
For lab and testing environments, allow unencrypted WinRM traffic. This lets Ansible connect over HTTP (port 5985) without requiring certificates:
Set-Item WSMan:\localhost\Service\AllowUnencrypted -Value $true
Production warning: never enable AllowUnencrypted in production. Use HTTPS with certificates instead. The “WinRM Security: Moving to HTTPS” section later in this guide covers the production-safe approach.
Verify the WinRM listener is active:
winrm enumerate winrm/config/Listener
The output should confirm an HTTP listener on port 5985:
Listener
Address = *
Transport = HTTP
Port = 5985
Hostname
Enabled = true
URLPrefix = wsman
Confirm basic auth and unencrypted settings took effect:
winrm get winrm/config/Service
Look for AllowUnencrypted = true and Basic = true in the Auth section. If either shows false, run the Set-Item commands again.
Open the Windows Firewall for WinRM if it isn’t already allowed:
New-NetFirewallRule -DisplayName "WinRM HTTP" -Direction Inbound -Protocol TCP -LocalPort 5985 -Action Allow
Create the Windows Inventory
Ansible needs an inventory file that tells it how to connect to the Windows host. The key difference from Linux inventories is the connection type and transport variables. Create a file called inventory.ini:
sudo vi inventory.ini
Add the following configuration:
[windows]
win2025 ansible_host=10.0.1.15
[windows:vars]
ansible_user=Administrator
ansible_password=SecureP@ss2026
ansible_connection=winrm
ansible_winrm_transport=basic
ansible_winrm_server_cert_validation=ignore
ansible_port=5985
Here’s what each variable does:
- ansible_connection=winrm tells Ansible to use WinRM instead of SSH
- ansible_winrm_transport=basic sets the authentication method (use
ntlmorkerberosin Active Directory environments) - ansible_winrm_server_cert_validation=ignore skips SSL certificate validation, needed when using HTTP or self-signed certificates
- ansible_port=5985 is the default WinRM HTTP port (use 5986 for HTTPS)
For production, store the password in Ansible Vault instead of plain text in the inventory file.
Test Connectivity
The Windows equivalent of Ansible’s ping module is win_ping. Test the connection:
ansible win2025 -i inventory.ini -m win_ping
A successful connection returns:
win2025 | SUCCESS => {
"changed": false,
"ping": "pong"
}
If you see basic: the specified credentials were rejected by the server, WinRM basic auth isn’t enabled on the Windows Server. Go back and run the Set-Item commands from the previous section.
Gather Windows facts to confirm Ansible can pull system information:
ansible win2025 -i inventory.ini -m setup
The output includes architecture, OS version, and hardware details. On Windows Server 2025, you’ll see:
"ansible_architecture": "64-bit",
"ansible_architecture2": "x86_64",
"ansible_os_name": "Microsoft Windows Server 2025 Datacenter"
Deploy IIS Web Server with Ansible
Now for something practical. This playbook installs IIS, creates a custom default page, configures the firewall, and verifies the web server responds. It demonstrates five different Windows modules working together. Create deploy_iis.yml:
sudo vi deploy_iis.yml
Add the playbook content:
---
- name: Deploy IIS on Windows Server 2025
hosts: win2025
gather_facts: true
tasks:
- name: Show Windows version
ansible.builtin.debug:
msg: "{{ ansible_os_name }}"
- name: Display OS info
ansible.builtin.debug:
msg: "{{ ansible_os_name }}"
- name: Install IIS web server
ansible.windows.win_feature:
name: Web-Server
include_management_tools: true
state: present
register: iis_result
- name: Show IIS install result
ansible.builtin.debug:
msg: "IIS installed: {{ iis_result.success }}, Restart needed: {{ iis_result.reboot_required }}"
- name: Create custom default page
ansible.windows.win_copy:
content: |
<html>
<head><title>Managed by Ansible</title></head>
<body><h1>IIS on Windows Server 2025 - Deployed by Ansible</h1></body>
</html>
dest: C:\inetpub\wwwroot\index.html
- name: Ensure IIS is running
ansible.windows.win_service:
name: W3SVC
start_mode: auto
state: started
- name: Open firewall for HTTP
community.windows.win_firewall_rule:
name: "HTTP Inbound"
localport: 80
action: allow
direction: in
protocol: tcp
state: present
enabled: true
- name: Test IIS is responding
ansible.windows.win_uri:
url: http://localhost
return_content: false
register: web_test
- name: Show web response
ansible.builtin.debug:
msg: "HTTP {{ web_test.status_code }} - IIS is serving content"
Run the playbook:
ansible-playbook -i inventory.ini deploy_iis.yml
The first run installs IIS, creates the page, and opens the firewall:
PLAY [Deploy IIS on Windows Server 2025] ***************************************
TASK [Gathering Facts] *********************************************************
ok: [win2025]
TASK [Show Windows version] ****************************************************
ok: [win2025]
TASK [Display OS info] *********************************************************
ok: [win2025] => {
"msg": "Microsoft Windows Server 2025 Datacenter"
}
TASK [Install IIS web server] **************************************************
changed: [win2025]
TASK [Show IIS install result] *************************************************
ok: [win2025] => {
"msg": "IIS installed: True, Restart needed: False"
}
TASK [Create custom default page] **********************************************
changed: [win2025]
TASK [Ensure IIS is running] ***************************************************
ok: [win2025]
TASK [Open firewall for HTTP] **************************************************
changed: [win2025]
TASK [Test IIS is responding] **************************************************
ok: [win2025]
TASK [Show web response] *******************************************************
ok: [win2025] => {
"msg": "HTTP 200 - IIS is serving content"
}
PLAY RECAP *********************************************************************
win2025 : ok=10 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Three tasks changed state on the first run: IIS installation, the custom page, and the firewall rule. Run the same playbook again to prove idempotence:
PLAY RECAP *********************************************************************
win2025 : ok=10 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Zero changes on the second run. Every task recognized that the desired state already existed and skipped the modification. This is what idempotent automation looks like.
Manage Windows Users
The win_user module creates and manages local Windows accounts. This is useful for provisioning service accounts, deployment users, or local admins across multiple servers. Create a user and add it to the Administrators group:
ansible win2025 -i inventory.ini -m ansible.windows.win_user -a "name=deploy_user password=D3pl0y@2026 groups=Administrators state=present"
The output confirms the account was created with the correct group membership:
win2025 | CHANGED => {
"account_disabled": false,
"account_locked": false,
"changed": true,
"fullname": "deploy_user",
"groups": [
{
"name": "Administrators",
"path": "WinNT://WORKGROUP/WIN2025-SRV/Administrators"
}
]
}
For managing multiple users, put the task in a playbook and use a loop with loop: to iterate over a list of usernames.
Windows Features Management
Beyond IIS, the win_feature module handles any Windows Server role or feature. You can install .NET Framework, Hyper-V, DNS Server, Active Directory Domain Services, or any other role available in Server Manager.
List the currently installed features on the target:
ansible win2025 -i inventory.ini -m ansible.windows.win_shell -a "Get-WindowsFeature | Where-Object {$_.InstallState -eq 'Installed'} | Format-Table Name, InstallState"
On our Windows Server 2025 instance with IIS installed, the output shows:
Name InstallState
---- ------------
FileAndStorage-Services Installed
Storage-Services Installed
Web-Server Installed
Web-WebServer Installed
Web-Common-Http Installed
Web-Default-Doc Installed
Web-Static-Content Installed
Web-Http-Logging Installed
Web-Filtering Installed
Web-Mgmt-Tools Installed
Install a new feature, for example .NET Framework 4.5:
ansible win2025 -i inventory.ini -m ansible.windows.win_feature -a "name=NET-Framework-45-Features state=present"
To remove a feature, change state=present to state=absent. The module handles the underlying DISM commands for you.
Registry Management
The win_regedit module reads and writes Windows Registry keys. This catches most people off guard because there’s no equivalent concept on Linux. Common uses include disabling Server Manager at login, configuring Windows Update policies, or setting application defaults.
Disable Server Manager from launching at login:
ansible win2025 -i inventory.ini -m ansible.windows.win_regedit -a "path=HKLM:\SOFTWARE\Microsoft\ServerManager data=1 name=DoNotOpenServerManagerAtLogon type=dword"
The module confirms the registry key was created or modified. For organizing these tasks into reusable components, see the Ansible roles tutorial:
win2025 | CHANGED => {
"changed": true,
"data_changed": false,
"data_type_changed": false
}
You can also use win_regedit to configure Windows Update, set power plans, or adjust security policies through registry keys. The module is idempotent, so running it twice with the same values produces no changes.
Firewall Rules with Ansible
The community.windows.win_firewall_rule module manages Windows Firewall rules without touching netsh commands. Create rules for any port, protocol, or application.
Open RDP (port 3389) and WinRM HTTPS (port 5986) in a playbook:
sudo vi firewall_rules.yml
Add the following content:
---
- name: Configure Windows Firewall
hosts: win2025
tasks:
- name: Allow RDP
community.windows.win_firewall_rule:
name: "RDP Inbound"
localport: 3389
action: allow
direction: in
protocol: tcp
state: present
enabled: true
- name: Allow WinRM HTTPS
community.windows.win_firewall_rule:
name: "WinRM HTTPS"
localport: 5986
action: allow
direction: in
protocol: tcp
state: present
enabled: true
- name: Block Telnet
community.windows.win_firewall_rule:
name: "Block Telnet"
localport: 23
action: block
direction: in
protocol: tcp
state: present
enabled: true
To remove a rule, set state: absent. The module matches rules by the name parameter, so use consistent naming across your playbooks.
Windows Updates
The community.windows.win_updates module can search for, download, and install Windows Updates. This is one of the most powerful automation wins for Windows Server management because patching manually across a fleet is tedious and error-prone.
A basic playbook to install all critical and security updates:
---
- name: Patch Windows Server
hosts: win2025
tasks:
- name: Install security updates
community.windows.win_updates:
category_names:
- SecurityUpdates
- CriticalUpdates
reboot: true
reboot_timeout: 600
register: update_result
- name: Show update results
ansible.builtin.debug:
msg: "Installed {{ update_result.installed_update_count }} updates. Reboot required: {{ update_result.reboot_required }}"
Set reboot: true to allow Ansible to automatically reboot the server if an update requires it. The reboot_timeout value (in seconds) controls how long Ansible waits for the server to come back before timing out. Increase this for servers that take longer to restart.
Hostname and Domain Join
The win_hostname module changes the computer name, and win_domain_membership joins or removes machines from Active Directory. Both typically require a reboot.
Test a hostname change in check mode first (no actual changes):
ansible win2025 -i inventory.ini -m ansible.windows.win_hostname -a "name=WEB-PROD-01" --check
Check mode reports what would change without applying it:
win2025 | CHANGED => {
"changed": true,
"old_name": "WIN2025-SRV",
"reboot_required": true
}
The reboot_required: true means you’d need to add a reboot task after the hostname change in your playbook. For domain joins, the playbook needs domain admin credentials and the domain controller must be reachable from the Windows Server.
WinRM Security: Moving to HTTPS
The basic HTTP setup works for labs, but production environments should use HTTPS (port 5986) with certificate-based authentication. This encrypts all traffic between the Ansible control node and the Windows targets.
Generate a self-signed certificate on the Windows Server:
$cert = New-SelfSignedCertificate -DnsName "win2025.yourdomain.com" -CertStoreLocation Cert:\LocalMachine\My
winrm create winrm/config/Listener?Address=*+Transport=HTTPS "@{Hostname=`"win2025.yourdomain.com`";CertificateThumbprint=`"$($cert.Thumbprint)`"}"
Open the HTTPS port in Windows Firewall:
New-NetFirewallRule -DisplayName "WinRM HTTPS" -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow
Update your inventory to use HTTPS:
[windows]
win2025 ansible_host=10.0.1.15
[windows:vars]
ansible_user=Administrator
ansible_password=SecureP@ss2026
ansible_connection=winrm
ansible_winrm_transport=basic
ansible_winrm_server_cert_validation=ignore
ansible_port=5986
ansible_winrm_scheme=https
For Active Directory environments, Kerberos authentication is the best option. It uses domain credentials and doesn’t require storing passwords in inventory files. You’ll need the pywinrm[kerberos] pip package and a properly configured /etc/krb5.conf on the control node.
Linux vs Windows Ansible: Key Differences
If you’re coming from managing Linux with Ansible, this table covers the main differences you’ll hit when switching to Windows targets. Bookmark this while you’re building your first Windows playbooks.
| Feature | Linux | Windows |
|---|---|---|
| Connection | SSH (port 22) | WinRM (port 5985/5986) |
| Shell | Bash/sh | PowerShell |
| Ping module | ping | win_ping |
| Package install | apt, dnf, yum | win_chocolatey, win_package |
| File copy | copy | win_copy |
| Service management | service, systemd | win_service |
| User management | user | win_user |
| Command execution | shell, command | win_shell, win_command |
| File paths | /etc/nginx/nginx.conf | C:\inetpub\wwwroot\ |
| Privilege escalation | become: true (sudo) | become_method: runas |
| Firewall | firewalld, ufw | win_firewall_rule |
| Facts module | setup | setup (same, different output) |
The biggest gotcha: most Linux modules don’t work on Windows and vice versa. Using copy instead of win_copy on a Windows target silently fails or throws cryptic errors. Check the ansible.windows collection documentation when you’re unsure which module to use.
Troubleshooting
“basic: the specified credentials were rejected by the server”
WinRM basic authentication is disabled by default on Windows Server 2025. Enable it with:
Set-Item WSMan:\localhost\Service\Auth\Basic -Value $true
Set-Item WSMan:\localhost\Service\AllowUnencrypted -Value $true
If you’re using NTLM transport and see ntlm: the specified credentials were rejected by the server, double-check the password. NTLM requires the exact local Administrator password.
“winrm or requests is not installed: No module named ‘winrm'”
The pywinrm Python package is missing on the Ansible control node. Install it:
pip3 install pywinrm
If you’re using a virtual environment for Ansible, make sure you install pywinrm inside the same virtual environment.
Connection timeout on port 5985
This usually means Windows Firewall is blocking the port, or WinRM isn’t listening. On the Windows Server, verify the listener is active:
winrm enumerate winrm/config/Listener
If no listener shows up, re-run Enable-PSRemoting -Force. Also check that no network firewall between the two hosts is blocking TCP 5985.
PowerShell execution policy blocking scripts
Some Windows modules execute PowerShell scripts on the target. If the execution policy is set to Restricted, these will fail. Set it to RemoteSigned:
Set-ExecutionPolicy RemoteSigned -Force
Putting It All Together
Managing Windows with Ansible is entirely viable once WinRM is configured. The initial setup takes more effort than a Linux target, but once the connection works, playbooks follow the same patterns: define the desired state, run the playbook, and let Ansible enforce it. For larger environments, look into Ansible Roles to organize your Windows playbooks into reusable components and the Ansible cheat sheet for quick reference on common tasks.
A few production tips from experience: always use HTTPS for WinRM in production, store credentials in Ansible Vault, and test playbooks with --check mode before running against production servers. Kerberos authentication is worth the setup effort if you’re in an Active Directory environment because it eliminates the need to store passwords anywhere. For a deeper comparison of configuration management tools, see our Ansible vs Chef vs Puppet vs Salt guide.