When working with Ansible and Windows, many find WinRM’s limitations both frustrating and tricky to navigate. Managing Windows remotely from a Linux environment presents unique challenges due to these constraints. From my vantage point, OpenSSH emerges as a more stable and speedy alternative compared to WinRM. This might account for its widespread recommendation as a remedy for WinRM’s shortcomings. Fortunately, there’s comprehensive official support and documentation available for OpenSSH on Windows.
Given this backdrop, our objective is to devise a playbook to set up OpenSSH and incorporate a public key. This will enable us to establish a connection from a Linux host using key-based authentication. This playbook assumes WinRM is enabled and properly configured in the target machine. Here’s an overview of our steps:
- Install OpenSSH (client and server) and configure the sshd service to use and specific port and to disable password-based authentication.
- Configure Windows Firewall rules to allow incoming SSH connections.
- Add a public SSH key and configure it.
- As expected, we will use WinRM to perform all these operations.
Code
git clone https://github.com/andreypicado506/ansible_openssh_role.git
Role structure
openssh_playbook
├── configure_openssh.yml
└── openssh_windows
├── tasks
│ ├── configure_openssh.yml
│ ├── main.yml
│ └── set_public_key.yml
└── vars
└── main.yml
Variables
In the vars/main.yml file, we’ve centralized all configurable parameters. This includes details like the location of the public key on your local machine and the SSH port setting. Additionally, if you choose to define a custom SSH port, the firewall rule will be adjusted automatically to accommodate this change.
---
SSHD_SERVICE_CONFIG_FILE_PATH: 'C:\ProgramData\ssh\sshd_config'
PUBLIC_SSH_KEY_LOCAL_PATH: '/home/examples/id_rsa.pub'
PUBLIC_SSH_KEY_LOCAL_FILE_NAME: "{{ PUBLIC_SSH_KEY_LOCAL_PATH.split('/')[-1] }}"
WORKING_DIRECTORY: 'C:\ansible_openssh'
OPENSSH_PORT: 22
OPENSSH_CLIENT_WINDOWS_CAPABILITY_NAME: 'OpenSSH.Client~~~~0.0.1.0'
OPENSSH_SERVER_WINDOWS_CAPABILITY_NAME: 'OpenSSH.Server~~~~0.0.1.0'
OpenSSH installation and configuration tasks
While Windows Server comes pre-packaged with the OpenSSH client, there are instances where it might not be installed. To ensure its presence, you can easily install OpenSSH on Windows using the Add-WindowsCapability cmdlet. After installation, restarting the sshd service ensures all the necessary configuration files are generated. From there, you can tweak them as per your requirements. If you prefer, the step to remove password authentication can be omitted from the playbook. Subsequent steps involve setting up a firewall rule and configuring the ssh-agent. To execute all these actions, simply use the ‘roles‘ keyword in the primary playbook:
---
- name: Installing OpenSSH.Client and OpenSSH.Server
ansible.windows.win_shell: |
Add-WindowsCapability -Online -Name '{{ OPENSSH_CLIENT_WINDOWS_CAPABILITY_NAME }}'
Add-WindowsCapability -Online -Name '{{ OPENSSH_SERVER_WINDOWS_CAPABILITY_NAME }}'
- name: Setting initial config for OpenSSH.Server service (sshd)
ansible.windows.win_service:
name: sshd
state: "{{ item.state }}"
loop:
- { state: started }
- { state: stopped }
- name: Setting OpenSSH.Server Firewall Rule
community.windows.win_firewall_rule:
name: OpenSSH-Server-In-TCP
localport: "{{ OPENSSH_PORT }}"
action: allow
direction: in
enabled: yes
protocol: tcp
- name: Setting properties for sshd-config
community.windows.win_lineinfile:
path: "{{ SSHD_SERVICE_CONFIG_FILE_PATH }}"
backrefs: yes
regexp: '{{ item.regexp }}'
line: '{{ item.line }}'
loop:
- { regexp: '^#Port 22', line: 'Port {{ OPENSSH_PORT }}' }
- { regexp: '^#PasswordAuthentication yes', line: 'PasswordAuthentication no' }
- name: Restart and set sshd and ssh-agent services
ansible.windows.win_service:
name: "{{ item.name }}"
state: started
start_mode: auto
loop:
- { name: sshd }
- { name: ssh-agent}
Setting a public SSH key
Once OpenSSH is installed, our goal is to establish a secure connection to the target server. To achieve this, the playbook configures a public key on Windows. It’s important to note that we’re interacting with the ‘administrators_authorized_keys‘ file, since we’re adding a public key specifically for an administrative user. Procedures for non-administrative users differ and can be found in the official documentation. Proper file permissions are crucial; any misconfiguration here can disrupt key authentication.
- name: Create the working directory
ansible.windows.win_shell: |
New-Item -ItemType Directory -Force -Path "{{ WORKING_DIRECTORY }}"
- name: Copy public ssh key
ansible.windows.win_copy:
src: /home/zaratustra/blog/id_rsa.pub
dest: '{{ WORKING_DIRECTORY }}\id_rsa.pub'
- name: Configuring the right permissions for the key
ansible.windows.win_shell: |
$pubKeyPath = "{{ WORKING_DIRECTORY }}\{{ PUBLIC_SSH_KEY_LOCAL_FILE_NAME }}"
$authorizedKey = Get-Content -Path $pubKeyPath
if (Test-Path -Path C:\ProgramData\ssh\administrators_authorized_keys) {
Add-Content -Path C:\ProgramData\ssh\administrators_authorized_keys -Value $authorizedKey
}
else {
New-Item -ItemType File -Path C:\ProgramData\ssh\administrators_authorized_keys -Force
Add-Content -Path C:\ProgramData\ssh\administrators_authorized_keys -Value $authorizedKey
}
icacls.exe ""C:\ProgramData\ssh\administrators_authorized_keys"" /inheritance:r /grant ""Administrators:F"" /grant ""SYSTEM:F""
Calling the playbook
Invoke the playbook like this:
ansible-playbook -i 192.168.2.39, ~/openssh_playbook/configure_openssh.yml -e SERVER=192.168.2.39
Take note: I specify the IP of the target VM directly in the inventory parameter. However, if your setup requires it, you can easily substitute this with a .yml or .ini inventory. As you might anticipate, the variables located in vars/main.yml can be incorporated during invocation by utilizing the -e parameter.
Once the playbook executes successfully, you’re all set to connect to your target Windows VM via the conventional SSH command. Remember, if you need to specify the private key during this connection, you can do so by employing the -i
parameter.