An Ansible role to install OpenSSH on Windows

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.

| Theme: UPortfolio