# Expects variable `ssh_users` as list of objects: # ssh_users: # - name: alice # keys: # - "ssh-ed25519 AAAA..." # - "ssh-rsa AAAA..." # - name: bob # keys: # - "ssh-rsa AAAA..." - name: Ensure OpenSSH Server capability is installed (if available) ansible.windows.win_shell: | $cap = Get-WindowsCapability -Online | Where-Object { $_.Name -like 'OpenSSH.Server*' } if ($cap -and $cap.State -ne 'Installed') { Add-WindowsCapability -Online -Name $cap.Name Write-Output 'installed' } else { Write-Output 'present' } args: executable: powershell.exe register: openssh_install changed_when: "'installed' in openssh_install.stdout" - name: Ensure host keys exist (ssh-keygen -A) ansible.windows.win_shell: | $exe = 'C:\Windows\System32\OpenSSH\ssh-keygen.exe' if (Test-Path $exe) { & $exe -A 2>&1 Write-Output 'hostkeys-generated' } else { Write-Output 'ssh-keygen-not-found' exit 2 } register: ssh_keygen changed_when: "'hostkeys-generated' in ssh_keygen.stdout" - name: Restrict private host key file permissions (icacls) and remove provision user access ansible.windows.win_shell: | $current = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name $keys = Get-ChildItem -Path 'C:\ProgramData\ssh' -Filter 'ssh_host_*_key' -File -ErrorAction SilentlyContinue if ($keys -and $keys.Count -gt 0) { foreach ($k in $keys) { icacls.exe $k.FullName /inheritance:r icacls.exe $k.FullName /grant:r "S-1-5-18:(F)" icacls.exe $k.FullName /remove $current 2>$null } Write-Output "restricted: removed access for $current" } else { Write-Output 'no-keys' } args: executable: powershell.exe register: restrict_hostkey_perms changed_when: "'restricted' in restrict_hostkey_perms.stdout" - name: Ensure sshd service is enabled and running ansible.windows.win_service: name: sshd start_mode: auto state: started - name: Ensure firewall allows SSH (TCP/22) community.windows.win_firewall_rule: name: "OpenSSH Server (sshd)" enable: true direction: in action: allow localport: 22 protocol: TCP profile: "Domain,Private" - name: Ensure PubkeyAuthentication is enabled in sshd_config community.windows.win_lineinfile: path: 'C:\ProgramData\ssh\sshd_config' regexp: "^#?PubkeyAuthentication" line: "PubkeyAuthentication yes" create: true backup: true register: sshd_pubkey_changed - name: Ensure AuthorizedKeysFile is set to .ssh/authorized_keys community.windows.win_lineinfile: path: 'C:\ProgramData\ssh\sshd_config' regexp: "^#?AuthorizedKeysFile" line: "AuthorizedKeysFile .ssh/authorized_keys" create: true backup: true register: sshd_authkeys_changed - name: Detect available PowerShell for ssh subsystem ansible.windows.win_shell: | if (Test-Path 'C:\Program Files\PowerShell\7\pwsh.exe') { Write-Output 'C:\Program Files\PowerShell\7\pwsh.exe' } elseif (Test-Path 'C:\Program Files\PowerShell\6\pwsh.exe') { Write-Output 'C:\Program Files\PowerShell\6\pwsh.exe' } elseif (Test-Path 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe') { Write-Output 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' } else { Write-Output '' } register: ssh_subsystem_path changed_when: false - name: Configure sshd Subsystem to allow PowerShell sessions community.windows.win_lineinfile: path: 'C:\ProgramData\ssh\sshd_config' regexp: '^Subsystem\s+powershell' line: 'Subsystem powershell "{{ ssh_subsystem_path.stdout | trim }}" -sshs -NoLogo -NoProfile' create: true backup: true register: sshd_subsystem_changed when: ssh_subsystem_path.stdout != '' - name: Restart sshd if config changed ansible.windows.win_service: name: sshd state: restarted when: sshd_pubkey_changed.changed or sshd_authkeys_changed.changed or sshd_subsystem_changed.changed - name: Ensure users for SSH keys exist (no-op if already present) ansible.windows.win_user: name: "{{ item.name }}" state: present loop: "{{ ssh_users | default([]) }}" loop_control: label: "{{ item.name }}" become: true become_method: runas become_user: "NT AUTHORITY\\SYSTEM" - name: Ensure user .ssh directory exists ansible.windows.win_file: path: "C:\\Users\\{{ item.name }}\\.ssh" state: directory loop: "{{ ssh_users | default([]) }}" loop_control: label: "{{ item.name }}" become: true become_method: runas become_user: "NT AUTHORITY\\SYSTEM" - name: Deploy authorized_keys for each user ansible.windows.win_copy: dest: "C:\\Users\\{{ item.name }}\\.ssh\\authorized_keys" content: "{{ (item['keys'] | default([])) | join('\r\n') }}\r\n" force: true loop: "{{ ssh_users | default([]) }}" loop_control: label: "{{ item.name }}" become: true become_method: runas become_user: "NT AUTHORITY\\SYSTEM" - name: Set ACL on .ssh directory to allow only the user and Administradores (directory) ansible.windows.win_acl: path: "C:\\Users\\{{ item.name }}\\.ssh" user: "{{ item.name }}" rights: FullControl type: allow inheritance: false propagate: false state: present loop: "{{ ssh_users | default([]) }}" loop_control: label: "{{ item.name }}" become: true become_method: runas become_user: "NT AUTHORITY\\SYSTEM" - name: Set ACL on authorized_keys file to allow only the user and Administradores (file) ansible.windows.win_acl: path: "C:\\Users\\{{ item.name }}\\.ssh\\authorized_keys" user: "{{ item.name }}" rights: FullControl type: allow inheritance: false propagate: false state: present loop: "{{ ssh_users | default([]) }}" loop_control: label: "{{ item.name }}" become: true become_method: runas become_user: "NT AUTHORITY\\SYSTEM" - name: Create administrators_authorized_keys from admin users' keys (if any) ansible.windows.win_copy: dest: 'C:\ProgramData\ssh\administrators_authorized_keys' content: "{{ (ssh_users | default([]) | selectattr('admin','equalto',true) | map(attribute='keys') | list | flatten | default([])) | join('\r\n') }}\r\n" force: true when: (ssh_users | default([]) | selectattr('admin','equalto',true) | list) | length > 0 become: true become_method: runas become_user: "NT AUTHORITY\\SYSTEM" - name: Tighten ACLs for administrators_authorized_keys (icacls) ansible.windows.win_shell: | $f = 'C:\ProgramData\ssh\administrators_authorized_keys' $current = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name if (Test-Path $f) { icacls.exe $f /inheritance:r icacls.exe $f /grant:r "S-1-5-18:(F)" "S-1-5-32-544:(F)" icacls.exe $f /remove $current 2>$null Write-Output 'restricted' } else { Write-Output 'no-file' } args: executable: powershell.exe register: restrict_admin_auth_perms changed_when: "'restricted' in restrict_admin_auth_perms.stdout" become: true become_method: runas become_user: "NT AUTHORITY\\SYSTEM" - name: Debug administrators_authorized_keys ACL change ansible.builtin.debug: var: restrict_admin_auth_perms.stdout