diff --git a/ansible/host_vars/game-01.lab.alexpires.me.yml b/ansible/host_vars/game-01.lab.alexpires.me.yml index f6bda8a..bb4924c 100644 --- a/ansible/host_vars/game-01.lab.alexpires.me.yml +++ b/ansible/host_vars/game-01.lab.alexpires.me.yml @@ -10,6 +10,16 @@ game_password: "{{ lookup('env', 'WIN_GAME_PASSWORD') }}" game_default_groups: - Administradores # This is mandatory to install games +ssh_users: + - name: "{{ ansible_user }}" + admin: true + keys: + - "{{ lookup('file', 'keys/ansible_user.pub') }}" + - name: calex + admin: true + keys: + - "{{ lookup('file', 'keys/operator.pub') }}" + grafana_prometheus_sources: - 10.19.4.136 windows_exporter_collectors: diff --git a/ansible/host_vars/hpz440.lab.alexpires.me.yml b/ansible/host_vars/hpz440.lab.alexpires.me.yml new file mode 100644 index 0000000..afe8be5 --- /dev/null +++ b/ansible/host_vars/hpz440.lab.alexpires.me.yml @@ -0,0 +1,35 @@ +hostname: hpz440.lab.alexpires.me +ansible_user: "{{ lookup('env', 'ANSIBLE_USER') }}" +ansible_password: "{{ lookup('env', 'WIN_HPZ440_PASSWORD') }}" +ansible_connection: winrm +ansible_winrm_transport: basic +ansible_winrm_server_cert_validation: ignore + +ssh_users: + - name: "{{ ansible_user }}" + admin: true + keys: + - "{{ lookup('file', 'keys/ansible_user.pub') }}" + - name: HP + admin: true + keys: + - "{{ lookup('file', 'keys/operator.pub') }}" + +grafana_prometheus_sources: + - 10.19.4.136 + +windows_exporter_collectors: + - cache + - cpu + - cpu_info + - gpu + - diskdrive + - logical_disk + - memory + - net + - os + - process + - service + - tcp + - time + - update diff --git a/ansible/host_vars/prod-01.alexpires.me.yml b/ansible/host_vars/prod-01.alexpires.me.yml index 693e734..001d01d 100644 --- a/ansible/host_vars/prod-01.alexpires.me.yml +++ b/ansible/host_vars/prod-01.alexpires.me.yml @@ -89,7 +89,7 @@ fail2ban_jails: logpath: "/var/log/auth.log" maxretry: 5 findtime: 600 - bantime: 3600 + bantime: -1 postfix: enabled: true port: "smtp,ssmtp" diff --git a/ansible/playbook_windows_ssh.yml b/ansible/playbook_windows_ssh.yml new file mode 100644 index 0000000..f6df998 --- /dev/null +++ b/ansible/playbook_windows_ssh.yml @@ -0,0 +1,7 @@ +- name: Setup ssh on Windows + hosts: windows + + tasks: + - name: Setup ssh + ansible.builtin.include_tasks: + file: tasks/windows_ssh.yml diff --git a/ansible/sectool.env b/ansible/sectool.env index 53048f0..7b22aec 100644 --- a/ansible/sectool.env +++ b/ansible/sectool.env @@ -15,3 +15,4 @@ CLOUDNS_AUTH_ID=$CLOUDNS_AUTH_ID CLOUDNS_PASSWORD=$CLOUDNS_PASSWORD WIN_PROVISION_PASSWORD=$WIN_PROVISION_PASSWORD WIN_GAME_PASSWORD=$WIN_GAME_PASSWORD +WIN_HPZ440_PASSWORD=$WIN_HPZ440_PASSWORD \ No newline at end of file diff --git a/ansible/tasks/windows_ssh.yml b/ansible/tasks/windows_ssh.yml new file mode 100644 index 0000000..8f2409a --- /dev/null +++ b/ansible/tasks/windows_ssh.yml @@ -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 diff --git a/inventory/hosts b/inventory/hosts index 1464d49..21648ba 100644 --- a/inventory/hosts +++ b/inventory/hosts @@ -98,13 +98,15 @@ gpu-01.lab.alexpires.me [windows] game-01.lab.alexpires.me +hpz440.lab.alexpires.me [windows_game] game-01.lab.alexpires.me -[prometheus] +[prometheus_linux] prod-01.alexpires.me dev-01.lab.alexpires.me [windows_prometheus] -game-01.lab.alexpires.me \ No newline at end of file +game-01.lab.alexpires.me +hpz440.lab.alexpires.me diff --git a/scripts/ConfigureRemotingForAnsible.ps1 b/scripts/ConfigureRemotingForAnsible.ps1 index e096144..92c5496 100644 --- a/scripts/ConfigureRemotingForAnsible.ps1 +++ b/scripts/ConfigureRemotingForAnsible.ps1 @@ -165,11 +165,20 @@ Function New-LegacySelfSignedCert { $certdata = $enrollment.CreateRequest(0) $enrollment.InstallResponse(2, $certdata, 0, "") - # extract/return the thumbprint from the generated cert - $parsed_cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 - $parsed_cert.Import([System.Text.Encoding]::UTF8.GetBytes($certdata)) - - return $parsed_cert.Thumbprint + try { + # Try to decode as base64 (most likely) then construct the cert from raw bytes. + $raw = $null + try { + $raw = [Convert]::FromBase64String($certdata) + } catch { + # Fallback to UTF8 bytes if it's not base64 + $raw = [System.Text.Encoding]::UTF8.GetBytes($certdata) + } + $parsed_cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList (, $raw) + return $parsed_cert.Thumbprint + } catch { + Throw "Failed to construct X509Certificate2 from generated data: $($_.Exception.Message)" + } } Function Enable-GlobalHttpFirewallAccess { @@ -327,7 +336,9 @@ If (!($listeners | Where-Object { $_.Keys -like "TRANSPORT=HTTPS" })) { } Write-Verbose "Enabling SSL listener." - New-WSManInstance -ResourceURI 'winrm/config/Listener' -SelectorSet $selectorset -ValueSet $valueset + # Fallback to winrm.exe create which is more compatible across WinRM versions + $body = "@{Hostname=`"$SubjectName`";CertificateThumbprint=`"$thumbprint`"}" + & winrm create 'winrm/config/Listener?Address=*+Transport=HTTPS' $body Write-ProgressLog "Enabled SSL listener." } Else { @@ -353,7 +364,8 @@ Else { Remove-WSManInstance -ResourceURI 'winrm/config/Listener' -SelectorSet $selectorset # Add new Listener with new SSL cert - New-WSManInstance -ResourceURI 'winrm/config/Listener' -SelectorSet $selectorset -ValueSet $valueset + $body = "@{Hostname=`"$SubjectName`";CertificateThumbprint=`"$thumbprint`"}" + & winrm create 'winrm/config/Listener?Address=*+Transport=HTTPS' $body } } diff --git a/terraform/apps/dev-01/config.tf b/terraform/apps/dev-01/config.tf index 7233011..44df377 100644 --- a/terraform/apps/dev-01/config.tf +++ b/terraform/apps/dev-01/config.tf @@ -92,6 +92,11 @@ locals { type = "A" content = "10.5.5.2" }, + { + name = "hpz440" + type = "A" + content = "10.19.4.121" + }, ] } ] diff --git a/terraform/apps/dev-01/resources/prometheus.yml b/terraform/apps/dev-01/resources/prometheus.yml index 4c34948..77b2516 100644 --- a/terraform/apps/dev-01/resources/prometheus.yml +++ b/terraform/apps/dev-01/resources/prometheus.yml @@ -3,10 +3,10 @@ scrape_configs: scrape_interval: 10s static_configs: - targets: - - "node-exporter.kube-system.svc.cluster.local:9100" + - "node-exporter.prometheus.svc.cluster.local:9100" - "10.19.4.1:9100" - job_name: auth-exporter scrape_interval: 10s static_configs: - targets: - - "auth-exporter.kube-system.svc.cluster.local:9100" + - "auth-exporter.prometheus.svc.cluster.local:9100" diff --git a/terraform/apps/prod-01/resources/prometheus.yml b/terraform/apps/prod-01/resources/prometheus.yml index 942fa94..9e5d501 100644 --- a/terraform/apps/prod-01/resources/prometheus.yml +++ b/terraform/apps/prod-01/resources/prometheus.yml @@ -2,13 +2,15 @@ scrape_configs: - job_name: node-exporter scrape_interval: 10s static_configs: - - targets: ["node-exporter.kube-system.svc.cluster.local:9100"] + - targets: + - "node-exporter.prometheus.svc.cluster.local:9100" - job_name: fail2ban scrape_interval: 60s static_configs: - - targets: ["fail2ban-exporter.kube-system.svc.cluster.local:9100"] + - targets: + - "fail2ban-exporter.prometheus.svc.cluster.local:9100" - job_name: auth-exporter scrape_interval: 10s static_configs: - targets: - - "auth-exporter.kube-system.svc.cluster.local:9100" + - "auth-exporter.prometheus.svc.cluster.local:9100" diff --git a/terraform/modules/apps/grafana/resources/prometheus.bak.yml b/terraform/modules/apps/grafana/resources/prometheus.bak.yml deleted file mode 100644 index ac7400d..0000000 --- a/terraform/modules/apps/grafana/resources/prometheus.bak.yml +++ /dev/null @@ -1,126 +0,0 @@ -global: - scrape_interval: 5s - evaluation_interval: 5s -rule_files: - - /etc/prometheus/prometheus.rules -alerting: - alertmanagers: - - scheme: http - static_configs: - - targets: - - "alertmanager.monitoring.svc:9093" -scrape_configs: - - job_name: "node-exporter" - kubernetes_sd_configs: - - role: endpoints - relabel_configs: - - source_labels: [__meta_kubernetes_endpoints_name] - regex: "node-exporter" - action: keep - - job_name: "kubernetes-apiservers" - kubernetes_sd_configs: - - role: endpoints - scheme: https - tls_config: - ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token - relabel_configs: - - source_labels: - [ - __meta_kubernetes_namespace, - __meta_kubernetes_service_name, - __meta_kubernetes_endpoint_port_name, - ] - action: keep - regex: default;kubernetes;https - - job_name: "kubernetes-nodes" - scheme: https - tls_config: - ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token - kubernetes_sd_configs: - - role: node - relabel_configs: - - action: labelmap - regex: __meta_kubernetes_node_label_(.+) - - target_label: __address__ - replacement: kubernetes.default.svc:443 - - source_labels: [__meta_kubernetes_node_name] - regex: (.+) - target_label: __metrics_path__ - replacement: /api/v1/nodes/$${1}/proxy/metrics - - job_name: "kubernetes-pods" - kubernetes_sd_configs: - - role: pod - relabel_configs: - - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] - action: keep - regex: true - - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] - action: replace - target_label: __metrics_path__ - regex: (.+) - - source_labels: - [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] - action: replace - regex: ([^:]+)(?::\d+)?;(\d+) - replacement: $1:$2 - target_label: __address__ - - action: labelmap - regex: __meta_kubernetes_pod_label_(.+) - - source_labels: [__meta_kubernetes_namespace] - action: replace - target_label: kubernetes_namespace - - source_labels: [__meta_kubernetes_pod_name] - action: replace - target_label: kubernetes_pod_name - - job_name: "kube-state-metrics" - static_configs: - - targets: ["kube-state-metrics.kube-system.svc.cluster.local:8080"] - - job_name: "kubernetes-cadvisor" - scheme: https - tls_config: - ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token - kubernetes_sd_configs: - - role: node - relabel_configs: - - action: labelmap - regex: __meta_kubernetes_node_label_(.+) - - target_label: __address__ - replacement: kubernetes.default.svc:443 - - source_labels: [__meta_kubernetes_node_name] - regex: (.+) - target_label: __metrics_path__ - replacement: /api/v1/nodes/$${1}/proxy/metrics/cadvisor - - job_name: "kubernetes-service-endpoints" - kubernetes_sd_configs: - - role: endpoints - relabel_configs: - - source_labels: - [__meta_kubernetes_service_annotation_prometheus_io_scrape] - action: keep - regex: true - - source_labels: - [__meta_kubernetes_service_annotation_prometheus_io_scheme] - action: replace - target_label: __scheme__ - regex: (https?) - - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path] - action: replace - target_label: __metrics_path__ - regex: (.+) - - source_labels: - [__address__, __meta_kubernetes_service_annotation_prometheus_io_port] - action: replace - target_label: __address__ - regex: ([^:]+)(?::\d+)?;(\d+) - replacement: $1:$2 - - action: labelmap - regex: __meta_kubernetes_service_label_(.+) - - source_labels: [__meta_kubernetes_namespace] - action: replace - target_label: kubernetes_namespace - - source_labels: [__meta_kubernetes_service_name] - action: replace - target_label: kubernetes_name diff --git a/terraform/modules/utils/auth-exporter/variables.tf b/terraform/modules/utils/auth-exporter/variables.tf index f099930..226b1fb 100644 --- a/terraform/modules/utils/auth-exporter/variables.tf +++ b/terraform/modules/utils/auth-exporter/variables.tf @@ -7,7 +7,7 @@ variable "tag" { variable "namespace" { description = "Kubernetes namespace to deploy the prometheus exporter" type = string - default = "kube-system" + default = "prometheus" } variable "persistent_folder" { diff --git a/terraform/modules/utils/fail2ban-exporter/variables.tf b/terraform/modules/utils/fail2ban-exporter/variables.tf index b57ea7b..feeddd8 100644 --- a/terraform/modules/utils/fail2ban-exporter/variables.tf +++ b/terraform/modules/utils/fail2ban-exporter/variables.tf @@ -7,5 +7,5 @@ variable "tag" { variable "namespace" { description = "Kubernetes namespace to deploy the Fail2Ban exporter" type = string - default = "kube-system" + default = "prometheus" } diff --git a/terraform/modules/utils/node-exporter/variables.tf b/terraform/modules/utils/node-exporter/variables.tf index b57ea7b..feeddd8 100644 --- a/terraform/modules/utils/node-exporter/variables.tf +++ b/terraform/modules/utils/node-exporter/variables.tf @@ -7,5 +7,5 @@ variable "tag" { variable "namespace" { description = "Kubernetes namespace to deploy the Fail2Ban exporter" type = string - default = "kube-system" + default = "prometheus" }