e9e28a5ca8
- Created model files for Qwen3.5 with different context lengths. - Added a new Ansible host variable file for CI server configuration. - Updated GPU server host variables to include Ollama models and model files. - Enhanced playbooks for Podman to manage Ollama models, including copying model files and checking existing models. - Introduced a new playbook for setting up Qwen3.6 with specific configurations. - Updated Windows SSH tasks to improve security and configuration management. - Added a Python utility to parse Ollama model names from command output. - Modified Terraform configurations to include new DNS entries and proxy settings. - Improved Nginx proxy configurations with timeout settings.
374 lines
12 KiB
YAML
374 lines
12 KiB
YAML
# 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: Setting defaults for Windows OpenSSH Server installation
|
|
ansible.builtin.set_fact:
|
|
__ssh_default_ciphers__:
|
|
- chacha20-poly1305@openssh.com
|
|
- aes256-gcm@openssh.com
|
|
- aes256-ctr
|
|
__ssh_default_hostkey_algorithms__:
|
|
- ssh-ed25519-cert-v01@openssh.com
|
|
- ssh-rsa-cert-v01@openssh.com
|
|
- ssh-ed25519
|
|
- ssh-rsa
|
|
- ecdsa-sha2-nistp521-cert-v01@openssh.com
|
|
- ecdsa-sha2-nistp384-cert-v01@openssh.com
|
|
- ecdsa-sha2-nistp256-cert-v01@openssh.com
|
|
- ecdsa-sha2-nistp521
|
|
- ecdsa-sha2-nistp384
|
|
- ecdsa-sha2-nistp256
|
|
- sk-ecdsa-sha2-nistp256@openssh.com
|
|
__ssh_default_kex_algorithms__:
|
|
- mlkem768x25519-sha256
|
|
- sntrup761x25519-sha512@openssh.com
|
|
- curve25519-sha256
|
|
- curve25519-sha256@libssh.org
|
|
- ecdh-sha2-nistp521
|
|
- ecdh-sha2-nistp384
|
|
- ecdh-sha2-nistp256
|
|
- diffie-hellman-group-exchange-sha256
|
|
__ssh_default_macs__:
|
|
- hmac-sha2-512-etm@openssh.com
|
|
- hmac-sha2-256-etm@openssh.com
|
|
- hmac-sha2-512
|
|
- hmac-sha2-256
|
|
__ssh_default_auth_pubkey_accepted_algorithms__:
|
|
- ecdsa-sha2-nistp521
|
|
- ecdsa-sha2-nistp384
|
|
- ecdsa-sha2-nistp256
|
|
- ssh-ed25519
|
|
- rsa-sha2-512
|
|
- rsa-sha2-256
|
|
|
|
- name: Override defaults with any provided variables
|
|
ansible.builtin.set_fact:
|
|
__ssh_users__: "{{ ssh_users | default([]) }}"
|
|
__ssh_ciphers__: "{{ ssh_ciphers | default(__ssh_default_ciphers__) }}"
|
|
__ssh_hostkey_algorithms__: "{{ ssh_hostkey_algorithms | default(__ssh_default_hostkey_algorithms__) }}"
|
|
__ssh_kex_algorithms__: "{{ ssh_kex_algorithms | default(__ssh_default_kex_algorithms__) }}"
|
|
__ssh_macs__: "{{ ssh_macs | default(__ssh_default_macs__) }}"
|
|
__ssh_max_auth_tries__: "{{ ssh_max_auth_tries | default(10) }}"
|
|
__ssh_max_sessions__: "{{ ssh_max_sessions | default(3) }}"
|
|
__ssh_pubkey_accepted_algorithms__: "{{ ssh_pubkey_accepted_algorithms | default(__ssh_default_auth_pubkey_accepted_algorithms__) }}"
|
|
|
|
- name: Get OpenSSH Server capability state
|
|
ansible.windows.win_shell: |
|
|
$cap = Get-WindowsCapability -Online | Where-Object { $_.Name -like 'OpenSSH.Server*' } | Select-Object -First 1
|
|
if ($cap) {
|
|
$cap.State.ToString().Trim()
|
|
} else {
|
|
'NotPresent'
|
|
}
|
|
args:
|
|
executable: powershell.exe
|
|
register: openssh_capability_state
|
|
changed_when: false
|
|
|
|
- name: Install OpenSSH Server capability when not installed
|
|
ansible.windows.win_shell: |
|
|
$cap = Get-WindowsCapability -Online | Where-Object { $_.Name -like 'OpenSSH.Server*' } | Select-Object -First 1
|
|
if (-not $cap) {
|
|
throw 'OpenSSH.Server capability not found on this host.'
|
|
}
|
|
|
|
Add-WindowsCapability -Online -Name $cap.Name
|
|
args:
|
|
executable: powershell.exe
|
|
register: openssh_install
|
|
when: openssh_capability_state.stdout | trim != 'Installed'
|
|
|
|
- name: Check whether OpenSSH host keys already exist
|
|
ansible.windows.win_shell: |
|
|
$keys = Get-ChildItem -Path 'C:\ProgramData\ssh' -Filter 'ssh_host_*_key' -File -ErrorAction SilentlyContinue
|
|
if ($keys -and $keys.Count -gt 0) {
|
|
Write-Output 'present'
|
|
} else {
|
|
Write-Output 'missing'
|
|
}
|
|
args:
|
|
executable: powershell.exe
|
|
register: ssh_hostkeys_state
|
|
changed_when: false
|
|
become: true
|
|
become_method: ansible.builtin.runas
|
|
become_user: "NT AUTHORITY\\SYSTEM"
|
|
|
|
- name: Generate OpenSSH host keys when missing (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"
|
|
when: ssh_hostkeys_state.stdout | trim == 'missing'
|
|
become: true
|
|
become_method: ansible.builtin.runas
|
|
become_user: "NT AUTHORITY\\SYSTEM"
|
|
|
|
- name: Ensure OpenSSH private host keys are owned by SYSTEM
|
|
ansible.windows.win_shell: |
|
|
$changed = $false
|
|
$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) {
|
|
$before = (Get-Acl -Path $k.FullName).Owner
|
|
icacls.exe $k.FullName /setowner "*S-1-5-18" /C | Out-Null
|
|
$after = (Get-Acl -Path $k.FullName).Owner
|
|
if ($before -ne $after) {
|
|
$changed = $true
|
|
}
|
|
}
|
|
if ($changed) {
|
|
Write-Output 'hostkey-owner-changed'
|
|
} else {
|
|
Write-Output 'hostkey-owner-unchanged'
|
|
}
|
|
} else {
|
|
Write-Output 'no-keys'
|
|
}
|
|
register: ssh_hostkey_owner
|
|
changed_when: "'hostkey-owner-changed' in ssh_hostkey_owner.stdout"
|
|
become: true
|
|
become_method: ansible.builtin.runas
|
|
become_user: "NT AUTHORITY\\SYSTEM"
|
|
|
|
- name: Restrict private host key file permissions (icacls) and remove provision user access
|
|
ansible.windows.win_shell: |
|
|
$changed = $false
|
|
$removePrincipal = "{{ ansible_user | default('') }}"
|
|
$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) {
|
|
$before = (Get-Acl -Path $k.FullName).Sddl
|
|
icacls.exe $k.FullName /inheritance:r
|
|
icacls.exe $k.FullName /grant:r "S-1-5-18:(F)"
|
|
if ($removePrincipal -and $removePrincipal -notmatch '^(?i:NT AUTHORITY\\SYSTEM|SYSTEM)$') {
|
|
icacls.exe $k.FullName /remove "$removePrincipal" 2>$null
|
|
}
|
|
$after = (Get-Acl -Path $k.FullName).Sddl
|
|
if ($before -ne $after) {
|
|
$changed = $true
|
|
}
|
|
}
|
|
if ($changed) {
|
|
Write-Output 'restricted-changed'
|
|
} else {
|
|
Write-Output 'restricted-unchanged'
|
|
}
|
|
} else {
|
|
Write-Output 'no-keys'
|
|
}
|
|
args:
|
|
executable: powershell.exe
|
|
register: restrict_hostkey_perms
|
|
changed_when: "'restricted-changed' in restrict_hostkey_perms.stdout"
|
|
become: true
|
|
become_method: ansible.builtin.runas
|
|
become_user: "NT AUTHORITY\\SYSTEM"
|
|
|
|
- name: Restrict C:\ProgramData\ssh folder write permissions
|
|
ansible.windows.win_shell: |
|
|
$p = 'C:\ProgramData\ssh'
|
|
if (Test-Path $p) {
|
|
$before = (Get-Acl -Path $p).Sddl
|
|
icacls.exe $p /inheritance:r | Out-Null
|
|
icacls.exe $p /grant:r "S-1-5-18:(OI)(CI)(F)" "S-1-5-32-544:(OI)(CI)(F)" | Out-Null
|
|
icacls.exe $p /remove "S-1-5-32-545" 2>$null
|
|
$after = (Get-Acl -Path $p).Sddl
|
|
if ($before -ne $after) {
|
|
Write-Output 'ssh-dir-acl-changed'
|
|
} else {
|
|
Write-Output 'ssh-dir-acl-unchanged'
|
|
}
|
|
} else {
|
|
Write-Output 'no-dir'
|
|
}
|
|
args:
|
|
executable: powershell.exe
|
|
register: ssh_dir_acl
|
|
changed_when: "'ssh-dir-acl-changed' in ssh_dir_acl.stdout"
|
|
become: true
|
|
become_method: ansible.builtin.runas
|
|
become_user: "NT AUTHORITY\\SYSTEM"
|
|
|
|
- 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 SSH banner file is present
|
|
ansible.windows.win_copy:
|
|
src: "windows/issue.net"
|
|
dest: 'C:\ProgramData\ssh\issue.net'
|
|
force: true
|
|
become: true
|
|
become_method: ansible.builtin.runas
|
|
become_user: "NT AUTHORITY\\SYSTEM"
|
|
|
|
- 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: Set ssh subsystem executable path fact
|
|
ansible.builtin.set_fact:
|
|
__ssh_subsystem_path__: "{{ ssh_subsystem_path.stdout | trim }}"
|
|
|
|
- name: Detect supported SSH key exchange algorithms
|
|
ansible.windows.win_shell: |
|
|
$ssh = 'C:\Windows\System32\OpenSSH\ssh.exe'
|
|
if (Test-Path $ssh) {
|
|
& $ssh -Q kex
|
|
} else {
|
|
Write-Output ''
|
|
}
|
|
args:
|
|
executable: powershell.exe
|
|
register: ssh_supported_kex
|
|
changed_when: false
|
|
|
|
- name: Render sshd_config from template
|
|
ansible.windows.win_template:
|
|
src: windows_sshd_config.j2
|
|
dest: 'C:\ProgramData\ssh\sshd_config'
|
|
register: sshd_config_changed
|
|
become: true
|
|
become_method: ansible.builtin.runas
|
|
become_user: "NT AUTHORITY\\SYSTEM"
|
|
|
|
- name: Restart sshd if config changed # noqa no-handler
|
|
ansible.windows.win_service:
|
|
name: sshd
|
|
state: restarted
|
|
when: sshd_config_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: ansible.builtin.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: ansible.builtin.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: ansible.builtin.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: ansible.builtin.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: ansible.builtin.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: ansible.builtin.runas
|
|
become_user: "NT AUTHORITY\\SYSTEM"
|
|
|
|
- name: Restrict administrators_authorized_keys permissions
|
|
ansible.windows.win_shell: |
|
|
$path = 'C:\ProgramData\ssh\administrators_authorized_keys'
|
|
if (Test-Path $path) {
|
|
$before = (Get-Acl -Path $path).Sddl
|
|
icacls.exe $path /inheritance:r | Out-Null
|
|
icacls.exe $path /grant:r "S-1-5-18:(F)" "S-1-5-32-544:(F)" | Out-Null
|
|
$after = (Get-Acl -Path $path).Sddl
|
|
if ($before -ne $after) {
|
|
Write-Output 'admin-auth-keys-acl-changed'
|
|
} else {
|
|
Write-Output 'admin-auth-keys-acl-unchanged'
|
|
}
|
|
} else {
|
|
Write-Output 'no-admin-auth-keys'
|
|
}
|
|
args:
|
|
executable: powershell.exe
|
|
register: restrict_admin_auth_perms
|
|
changed_when: "'admin-auth-keys-acl-changed' in restrict_admin_auth_perms.stdout"
|
|
when: (__ssh_users__ | default([]) | selectattr('admin','equalto',true) | list) | length > 0
|
|
become: true
|
|
become_method: ansible.builtin.runas
|
|
become_user: "NT AUTHORITY\\SYSTEM"
|