|
|
|
@@ -0,0 +1,213 @@
|
|
|
|
|
# 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
|