Set up a Windows 11 PC for Ansible

Install SSHD

PS > Add-WindowsCapability -Online -Name 'OpenSSH.Server~~~~0.0.1.0'

Add ssh key

Add your public key to the administrators authorized_keys file:

PS > "ssh-ed25519 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | set-content 'C:\ProgramData\ssh\administrators_authorized_keys' -Encoding ASCII

Use icacls to restrict the permissions:

PS > icacls.exe "C:\ProgramData\ssh\administrators_authorized_keys" /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"

Configure SSHD

# Disable root login (not sure if this has an effect on windows) PermitRootLogin no # Strict authentication modes StrictModes yes # Enable key authentication PubkeyAuthentication yes # Disable password and other authentication methods PasswordAuthentication no PermityEmptyPasswords no GSSAPIAuthentication no # Default sftp thing Subsystem sftp sftp-server.exe # Use the ProgramData authorized keys file instead of C:\Users\<username>\.ssh\authorized_keys # but only for the administrators group Match Group administrators AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys # Allows our Domain "Workstation Administrators" group to log in AllowGroups contoso.local\Workstation Administrators

Start the SSHD service

PS > Start-Service sshd

Ansible

I have ansible installed inside WSL Debian. Activate the ansible environment and start the ssh agent so ansible can log in with the key. You can configure ansible_ssh_key_file within the hosts but I find the ssh-agent method easier because inside WSL all Windows file have 0777 permissions and therefore ssh refuses to load the key. Take note that with the ssh-add command we are specifiying - for stdin and redirecting the private key file, this is what bypasses the 0777 permission issue.

bash > source ~/ansible-env/bin/activate (ansible-env) bash > eval `ssh-agent` Agent pid 87705 (ansible-env) bash > ssh-add - < id_windows Identity added: (stdin) (<keyname>)

If using fish shell, you have to source the fish version of the environment activation script. I have a fish alias for ssh-agent as it only works with bash normally.

fish > source ~/ansible-env/bin/activate.fish (ansible-env) fish > ssh-agent (ansible-env) fish > ssh-add - < id_windows Identity added: (stdin) (<keyname>)

Configure the ansible inventory as usual. Use the FQDN including the AD domain.

I don't use my regular account, instead I put my workstation administrator account which is a member of our "contoso.local\Workstation Administrators" AD group.

--- all: hosts: DESKTOP-XXXXXX.contoso.local: DESKTOP-YYYYYY.contoso.local: DESKTOP-ZZZZZZ.contoso.local: vars: ansible_user: <workstation administrator account> ansible_connection: ssh ansible_shell_type: cmd

Test the connection with an ad-hoc command. If you have not connected before, you will have to check and accept the host keys.

fish > ansible -i hosts.yml all -m win_ping DESKTOP-XXXXXX.contoso.local | OK => { "changed": true, "ping": "pong" } DESKTOP-YYYYYY.contoso.local | OK => { "changed": true, "ping": "pong" } DESKTOP-ZZZZZZ.contoso.local | OK => { "changed": true, "ping": "pong" }

Test a playbook, I created one which shows the uptime.

--- - name: Get Uptime gather_facts: no hosts: all tasks: - name: Fetch uptime ansible.windows.win_powershell: script: | $bt=(Get-CimInstance Win32_OperatingSystem -Property LastBootUpTime).LastBootUpTime; $ut=(Get-Date).Subtract($bt); [string]::Format("Up {5:0} days {6:0} hours {7:0} minutes, since {0:0000}-{1:00}-{2:00} {3:00}:{4:00}", $bt.Year, $bt.Month, $bt.Hour, $bt.Minute, $ut.Days, $ut.Hours, $ut.Minutes); register: uptime - name: Show uptime ansible.builtin.debug: var: uptime.output

Run the playbook to retrieve the uptime from the computers.

fish > ansible-playbook -i hosts.yml get_uptime.yml PLAY [Get Uptime] **** TASK [Fetch uptime] **** changed: [DESKTOP-XXXXXX.contoso.local] TASK [Show uptime] **** ok: [DESKTOP-XXXXXX.contoso.local] => { "uptime.output": [ "Up 0 days 5 hours 54 minutes, since 2025-03-07 12:35" ] } PLAY RECAP **** DESKTOP-XXXXXX.contoso.local : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0