From c15bf4d708932b8f2310e890a5ab05d61d73327d Mon Sep 17 00:00:00 2001 From: Nikholas Pcenicni <82239765+nikpcenicni@users.noreply.github.com> Date: Wed, 1 Apr 2026 01:19:50 -0400 Subject: [PATCH] Enhance Ansible playbooks and documentation for Debian and Proxmox management. Add new playbooks for Debian hardening, maintenance, SSH key rotation, and Proxmox cluster setup. Update README.md with quick start instructions for Debian and Proxmox operations. Modify group_vars to include Argo CD application settings, improving deployment flexibility and clarity. --- ansible/README.md | 67 +++++++++++- ansible/group_vars/all.yml | 3 + ansible/group_vars/debian_servers.yml | 12 +++ ansible/group_vars/proxmox_hosts.yml | 37 +++++++ ansible/inventory/debian.example.yml | 11 ++ ansible/inventory/proxmox.example.yml | 24 +++++ ansible/inventory/proxmox.yml | 24 +++++ ansible/playbooks/debian_harden.yml | 8 ++ ansible/playbooks/debian_maintenance.yml | 8 ++ ansible/playbooks/debian_ops.yml | 3 + ansible/playbooks/debian_rotate_ssh_keys.yml | 8 ++ ansible/playbooks/noble.yml | 4 +- ansible/playbooks/proxmox_cluster.yml | 9 ++ ansible/playbooks/proxmox_ops.yml | 4 + ansible/playbooks/proxmox_prepare.yml | 8 ++ ansible/playbooks/proxmox_upgrade.yml | 9 ++ .../defaults/main.yml | 39 +++++++ .../handlers/main.yml | 12 +++ .../debian_baseline_hardening/tasks/main.yml | 52 +++++++++ .../debian_maintenance/defaults/main.yml | 7 ++ .../roles/debian_maintenance/tasks/main.yml | 30 ++++++ .../debian_ssh_key_rotation/defaults/main.yml | 10 ++ .../debian_ssh_key_rotation/tasks/main.yml | 50 +++++++++ ansible/roles/noble_argocd/defaults/main.yml | 4 + ansible/roles/noble_argocd/tasks/main.yml | 14 +++ .../roles/noble_post_deploy/tasks/main.yml | 5 +- .../roles/proxmox_baseline/defaults/main.yml | 14 +++ .../roles/proxmox_baseline/handlers/main.yml | 5 + ansible/roles/proxmox_baseline/tasks/main.yml | 100 ++++++++++++++++++ .../roles/proxmox_cluster/defaults/main.yml | 7 ++ ansible/roles/proxmox_cluster/tasks/main.yml | 63 +++++++++++ .../proxmox_maintenance/defaults/main.yml | 6 ++ .../roles/proxmox_maintenance/tasks/main.yml | 30 ++++++ 33 files changed, 682 insertions(+), 5 deletions(-) create mode 100644 ansible/group_vars/debian_servers.yml create mode 100644 ansible/group_vars/proxmox_hosts.yml create mode 100644 ansible/inventory/debian.example.yml create mode 100644 ansible/inventory/proxmox.example.yml create mode 100644 ansible/inventory/proxmox.yml create mode 100644 ansible/playbooks/debian_harden.yml create mode 100644 ansible/playbooks/debian_maintenance.yml create mode 100644 ansible/playbooks/debian_ops.yml create mode 100644 ansible/playbooks/debian_rotate_ssh_keys.yml create mode 100644 ansible/playbooks/proxmox_cluster.yml create mode 100644 ansible/playbooks/proxmox_ops.yml create mode 100644 ansible/playbooks/proxmox_prepare.yml create mode 100644 ansible/playbooks/proxmox_upgrade.yml create mode 100644 ansible/roles/debian_baseline_hardening/defaults/main.yml create mode 100644 ansible/roles/debian_baseline_hardening/handlers/main.yml create mode 100644 ansible/roles/debian_baseline_hardening/tasks/main.yml create mode 100644 ansible/roles/debian_maintenance/defaults/main.yml create mode 100644 ansible/roles/debian_maintenance/tasks/main.yml create mode 100644 ansible/roles/debian_ssh_key_rotation/defaults/main.yml create mode 100644 ansible/roles/debian_ssh_key_rotation/tasks/main.yml create mode 100644 ansible/roles/noble_argocd/defaults/main.yml create mode 100644 ansible/roles/proxmox_baseline/defaults/main.yml create mode 100644 ansible/roles/proxmox_baseline/handlers/main.yml create mode 100644 ansible/roles/proxmox_baseline/tasks/main.yml create mode 100644 ansible/roles/proxmox_cluster/defaults/main.yml create mode 100644 ansible/roles/proxmox_cluster/tasks/main.yml create mode 100644 ansible/roles/proxmox_maintenance/defaults/main.yml create mode 100644 ansible/roles/proxmox_maintenance/tasks/main.yml diff --git a/ansible/README.md b/ansible/README.md index 8766a74..bbef493 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -37,6 +37,14 @@ Copy **`.env.sample`** to **`.env`** at the repository root (`.env` is gitignore | [`playbooks/noble.yml`](playbooks/noble.yml) | Helm + `kubectl` platform (after Phase A). | | [`playbooks/post_deploy.yml`](playbooks/post_deploy.yml) | SOPS reminders and optional Argo root Application note. | | [`playbooks/talos_bootstrap.yml`](playbooks/talos_bootstrap.yml) | **`talhelper genconfig` only** (legacy shortcut; prefer **`talos_phase_a.yml`**). | +| [`playbooks/debian_harden.yml`](playbooks/debian_harden.yml) | Baseline hardening for Debian servers (SSH/sysctl/fail2ban/unattended-upgrades). | +| [`playbooks/debian_maintenance.yml`](playbooks/debian_maintenance.yml) | Debian maintenance run (apt upgrades, autoremove/autoclean, reboot when required). | +| [`playbooks/debian_rotate_ssh_keys.yml`](playbooks/debian_rotate_ssh_keys.yml) | Rotate managed users' `authorized_keys`. | +| [`playbooks/debian_ops.yml`](playbooks/debian_ops.yml) | Convenience pipeline: harden then maintenance for Debian servers. | +| [`playbooks/proxmox_prepare.yml`](playbooks/proxmox_prepare.yml) | Configure Proxmox community repos and disable no-subscription UI warning. | +| [`playbooks/proxmox_upgrade.yml`](playbooks/proxmox_upgrade.yml) | Proxmox maintenance run (apt dist-upgrade, cleanup, reboot when required). | +| [`playbooks/proxmox_cluster.yml`](playbooks/proxmox_cluster.yml) | Create a Proxmox cluster on the master and join additional hosts. | +| [`playbooks/proxmox_ops.yml`](playbooks/proxmox_ops.yml) | Convenience pipeline: prepare, upgrade, then cluster Proxmox hosts. | ```bash cd ansible @@ -71,7 +79,7 @@ ansible-playbook playbooks/noble.yml --tags velero -e noble_velero_install=true ### Variables — `group_vars/all.yml` and role defaults -- **`group_vars/all.yml`:** **`noble_newt_install`**, **`noble_velero_install`**, **`noble_cert_manager_require_cloudflare_secret`**, **`noble_k8s_api_server_override`**, **`noble_k8s_api_server_auto_fallback`**, **`noble_k8s_api_server_fallback`**, **`noble_skip_k8s_health_check`** +- **`group_vars/all.yml`:** **`noble_newt_install`**, **`noble_velero_install`**, **`noble_cert_manager_require_cloudflare_secret`**, **`noble_argocd_apply_root_application`**, **`noble_k8s_api_server_override`**, **`noble_k8s_api_server_auto_fallback`**, **`noble_k8s_api_server_fallback`**, **`noble_skip_k8s_health_check`** - **`roles/noble_platform/defaults/main.yml`:** **`noble_apply_sops_secrets`**, **`noble_sops_age_key_file`** (SOPS secrets under **`clusters/noble/secrets/`**) ## Roles @@ -84,6 +92,63 @@ ansible-playbook playbooks/noble.yml --tags velero -e noble_velero_install=true | `noble_landing_urls` | Writes **`ansible/output/noble-lab-ui-urls.md`** — URLs, service names, and (optional) Argo/Grafana passwords from Secrets | | `noble_post_deploy` | Post-install reminders | | `talos_bootstrap` | Genconfig-only (used by older playbook) | +| `debian_baseline_hardening` | Baseline Debian hardening (SSH policy, sysctl profile, fail2ban, unattended upgrades) | +| `debian_maintenance` | Routine Debian maintenance tasks (updates, cleanup, reboot-on-required) | +| `debian_ssh_key_rotation` | Declarative `authorized_keys` rotation for server users | +| `proxmox_baseline` | Proxmox repo prep (community repos) and no-subscription warning suppression | +| `proxmox_maintenance` | Proxmox package maintenance (dist-upgrade, cleanup, reboot-on-required) | +| `proxmox_cluster` | Proxmox cluster bootstrap/join automation using `pvecm` | + +## Debian server ops quick start + +These playbooks are separate from the Talos/noble flow and target hosts in `debian_servers`. + +1. Copy `inventory/debian.example.yml` to `inventory/debian.yml` and update hosts/users. +2. Update `group_vars/debian_servers.yml` with your allowed SSH users and real public keys. +3. Run with the Debian inventory: + +```bash +cd ansible +ansible-playbook -i inventory/debian.yml playbooks/debian_harden.yml +ansible-playbook -i inventory/debian.yml playbooks/debian_rotate_ssh_keys.yml +ansible-playbook -i inventory/debian.yml playbooks/debian_maintenance.yml +``` + +Or run the combined maintenance pipeline: + +```bash +cd ansible +ansible-playbook -i inventory/debian.yml playbooks/debian_ops.yml +``` + +## Proxmox host + cluster quick start + +These playbooks are separate from the Talos/noble flow and target hosts in `proxmox_hosts`. + +1. Copy `inventory/proxmox.example.yml` to `inventory/proxmox.yml` and update hosts/users. +2. Update `group_vars/proxmox_hosts.yml` with your cluster name (`proxmox_cluster_name`), chosen cluster master, and root public key file paths to install. +3. First run (no SSH keys yet): use `--ask-pass` **or** set `ansible_password` (prefer Ansible Vault). Keep `ansible_ssh_common_args: "-o StrictHostKeyChecking=accept-new"` in inventory for first-contact hosts. +4. Run prepare first to install your public keys on each host, then continue: + +```bash +cd ansible +ansible-playbook -i inventory/proxmox.yml playbooks/proxmox_prepare.yml --ask-pass +ansible-playbook -i inventory/proxmox.yml playbooks/proxmox_upgrade.yml +ansible-playbook -i inventory/proxmox.yml playbooks/proxmox_cluster.yml +``` + +After `proxmox_prepare.yml` finishes, SSH key auth should work for root (keys from `proxmox_root_authorized_key_files`), so `--ask-pass` is usually no longer needed. + +If `pvecm add` still prompts for the master root password during join, set `proxmox_cluster_master_root_password` (prefer Vault) to run join non-interactively. + +Changing `proxmox_cluster_name` only affects new cluster creation; it does not rename an already-created cluster. + +Or run the full Proxmox pipeline: + +```bash +cd ansible +ansible-playbook -i inventory/proxmox.yml playbooks/proxmox_ops.yml +``` ## Migrating from Argo-managed `noble-platform` diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index bf33f25..4177088 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -21,3 +21,6 @@ noble_cert_manager_require_cloudflare_secret: true # Velero — set **noble_velero_install: true** plus S3 bucket/URL (and credentials — see clusters/noble/bootstrap/velero/README.md) noble_velero_install: false + +# Argo CD — apply app-of-apps root Application (clusters/noble/bootstrap/argocd/root-application.yaml). Set false to skip. +noble_argocd_apply_root_application: true diff --git a/ansible/group_vars/debian_servers.yml b/ansible/group_vars/debian_servers.yml new file mode 100644 index 0000000..7c5d088 --- /dev/null +++ b/ansible/group_vars/debian_servers.yml @@ -0,0 +1,12 @@ +--- +# Hardened SSH settings +debian_baseline_ssh_allow_users: + - admin + +# Example key rotation entries. Replace with your real users and keys. +debian_ssh_rotation_users: + - name: admin + home: /home/admin + state: present + keys: + - "ssh-ed25519 AAAAEXAMPLE_REPLACE_ME admin@workstation" diff --git a/ansible/group_vars/proxmox_hosts.yml b/ansible/group_vars/proxmox_hosts.yml new file mode 100644 index 0000000..8d8035a --- /dev/null +++ b/ansible/group_vars/proxmox_hosts.yml @@ -0,0 +1,37 @@ +--- +# Proxmox repositories +proxmox_repo_debian_codename: trixie +proxmox_repo_disable_enterprise: true +proxmox_repo_disable_ceph_enterprise: true +proxmox_repo_enable_pve_no_subscription: true +proxmox_repo_enable_ceph_no_subscription: true + +# Suppress "No valid subscription" warning in UI +proxmox_no_subscription_notice_disable: true + +# Public keys to install for root on each Proxmox host. +proxmox_root_authorized_key_files: + - "{{ lookup('env', 'HOME') }}/.ssh/id_ed25519.pub" + - "{{ lookup('env', 'HOME') }}/.ssh/ansible.pub" + +# Package upgrade/reboot policy +proxmox_upgrade_apt_cache_valid_time: 3600 +proxmox_upgrade_autoremove: true +proxmox_upgrade_autoclean: true +proxmox_upgrade_reboot_if_required: true +proxmox_upgrade_reboot_timeout: 1800 + +# Cluster settings +proxmox_cluster_enabled: true +proxmox_cluster_name: atomic-hub + +# Bootstrap host name from inventory (first host by default if empty) +proxmox_cluster_master: "" + +# Optional explicit IP/FQDN for joining; leave empty to use ansible_host of master +proxmox_cluster_master_ip: "" +proxmox_cluster_force: false + +# Optional: use only for first cluster joins when inter-node SSH trust is not established. +# Prefer storing with Ansible Vault if you set this. +proxmox_cluster_master_root_password: "Hemroid8" diff --git a/ansible/inventory/debian.example.yml b/ansible/inventory/debian.example.yml new file mode 100644 index 0000000..96199c5 --- /dev/null +++ b/ansible/inventory/debian.example.yml @@ -0,0 +1,11 @@ +--- +all: + children: + debian_servers: + hosts: + debian-01: + ansible_host: 192.168.50.101 + ansible_user: admin + debian-02: + ansible_host: 192.168.50.102 + ansible_user: admin diff --git a/ansible/inventory/proxmox.example.yml b/ansible/inventory/proxmox.example.yml new file mode 100644 index 0000000..3e775b6 --- /dev/null +++ b/ansible/inventory/proxmox.example.yml @@ -0,0 +1,24 @@ +--- +all: + children: + proxmox_hosts: + vars: + ansible_ssh_common_args: "-o StrictHostKeyChecking=accept-new" + hosts: + helium: + ansible_host: 192.168.1.100 + ansible_user: root + # First run without SSH keys: + # ansible_password: "{{ vault_proxmox_root_password }}" + neon: + ansible_host: 192.168.1.90 + ansible_user: root + # ansible_password: "{{ vault_proxmox_root_password }}" + argon: + ansible_host: 192.168.1.80 + ansible_user: root + # ansible_password: "{{ vault_proxmox_root_password }}" + krypton: + ansible_host: 192.168.1.70 + ansible_user: root + # ansible_password: "{{ vault_proxmox_root_password }}" \ No newline at end of file diff --git a/ansible/inventory/proxmox.yml b/ansible/inventory/proxmox.yml new file mode 100644 index 0000000..3e775b6 --- /dev/null +++ b/ansible/inventory/proxmox.yml @@ -0,0 +1,24 @@ +--- +all: + children: + proxmox_hosts: + vars: + ansible_ssh_common_args: "-o StrictHostKeyChecking=accept-new" + hosts: + helium: + ansible_host: 192.168.1.100 + ansible_user: root + # First run without SSH keys: + # ansible_password: "{{ vault_proxmox_root_password }}" + neon: + ansible_host: 192.168.1.90 + ansible_user: root + # ansible_password: "{{ vault_proxmox_root_password }}" + argon: + ansible_host: 192.168.1.80 + ansible_user: root + # ansible_password: "{{ vault_proxmox_root_password }}" + krypton: + ansible_host: 192.168.1.70 + ansible_user: root + # ansible_password: "{{ vault_proxmox_root_password }}" \ No newline at end of file diff --git a/ansible/playbooks/debian_harden.yml b/ansible/playbooks/debian_harden.yml new file mode 100644 index 0000000..14b9f08 --- /dev/null +++ b/ansible/playbooks/debian_harden.yml @@ -0,0 +1,8 @@ +--- +- name: Debian server baseline hardening + hosts: debian_servers + become: true + gather_facts: true + roles: + - role: debian_baseline_hardening + tags: [hardening, baseline] diff --git a/ansible/playbooks/debian_maintenance.yml b/ansible/playbooks/debian_maintenance.yml new file mode 100644 index 0000000..718b859 --- /dev/null +++ b/ansible/playbooks/debian_maintenance.yml @@ -0,0 +1,8 @@ +--- +- name: Debian maintenance (updates + reboot handling) + hosts: debian_servers + become: true + gather_facts: true + roles: + - role: debian_maintenance + tags: [maintenance, updates] diff --git a/ansible/playbooks/debian_ops.yml b/ansible/playbooks/debian_ops.yml new file mode 100644 index 0000000..bd6b247 --- /dev/null +++ b/ansible/playbooks/debian_ops.yml @@ -0,0 +1,3 @@ +--- +- import_playbook: debian_harden.yml +- import_playbook: debian_maintenance.yml diff --git a/ansible/playbooks/debian_rotate_ssh_keys.yml b/ansible/playbooks/debian_rotate_ssh_keys.yml new file mode 100644 index 0000000..d52e959 --- /dev/null +++ b/ansible/playbooks/debian_rotate_ssh_keys.yml @@ -0,0 +1,8 @@ +--- +- name: Debian SSH key rotation + hosts: debian_servers + become: true + gather_facts: false + roles: + - role: debian_ssh_key_rotation + tags: [ssh, ssh_keys, rotation] diff --git a/ansible/playbooks/noble.yml b/ansible/playbooks/noble.yml index 096f2a2..25a7a41 100644 --- a/ansible/playbooks/noble.yml +++ b/ansible/playbooks/noble.yml @@ -113,6 +113,7 @@ tags: [always] # talosctl kubeconfig often sets server to the VIP; off-LAN you can reach a control-plane IP but not 192.168.50.230. + # kubectl stderr is often "The connection to the server ... was refused" (no substring "connection refused"). - name: Auto-fallback API server when VIP is unreachable (temp kubeconfig) tags: [always] when: @@ -120,8 +121,7 @@ - noble_k8s_api_server_override | default('') | length == 0 - not (noble_skip_k8s_health_check | default(false) | bool) - (noble_k8s_health_first.rc | default(1)) != 0 or (noble_k8s_health_first.stdout | default('') | trim) != 'ok' - - ('network is unreachable' in (noble_k8s_health_first.stderr | default('') | lower)) or - ('no route to host' in (noble_k8s_health_first.stderr | default('') | lower)) + - (((noble_k8s_health_first.stderr | default('')) ~ (noble_k8s_health_first.stdout | default(''))) | lower) is search('network is unreachable|no route to host|connection refused|was refused', multiline=False) block: - name: Ensure temp dir for kubeconfig auto-fallback ansible.builtin.file: diff --git a/ansible/playbooks/proxmox_cluster.yml b/ansible/playbooks/proxmox_cluster.yml new file mode 100644 index 0000000..8f3d5d6 --- /dev/null +++ b/ansible/playbooks/proxmox_cluster.yml @@ -0,0 +1,9 @@ +--- +- name: Proxmox cluster bootstrap/join + hosts: proxmox_hosts + become: true + gather_facts: false + serial: 1 + roles: + - role: proxmox_cluster + tags: [proxmox, cluster] diff --git a/ansible/playbooks/proxmox_ops.yml b/ansible/playbooks/proxmox_ops.yml new file mode 100644 index 0000000..1477c7c --- /dev/null +++ b/ansible/playbooks/proxmox_ops.yml @@ -0,0 +1,4 @@ +--- +- import_playbook: proxmox_prepare.yml +- import_playbook: proxmox_upgrade.yml +- import_playbook: proxmox_cluster.yml diff --git a/ansible/playbooks/proxmox_prepare.yml b/ansible/playbooks/proxmox_prepare.yml new file mode 100644 index 0000000..dac73ed --- /dev/null +++ b/ansible/playbooks/proxmox_prepare.yml @@ -0,0 +1,8 @@ +--- +- name: Proxmox host preparation (community repos + no-subscription notice) + hosts: proxmox_hosts + become: true + gather_facts: true + roles: + - role: proxmox_baseline + tags: [proxmox, prepare, repos, ui] diff --git a/ansible/playbooks/proxmox_upgrade.yml b/ansible/playbooks/proxmox_upgrade.yml new file mode 100644 index 0000000..72469a1 --- /dev/null +++ b/ansible/playbooks/proxmox_upgrade.yml @@ -0,0 +1,9 @@ +--- +- name: Proxmox host maintenance (upgrade to latest) + hosts: proxmox_hosts + become: true + gather_facts: true + serial: 1 + roles: + - role: proxmox_maintenance + tags: [proxmox, maintenance, updates] diff --git a/ansible/roles/debian_baseline_hardening/defaults/main.yml b/ansible/roles/debian_baseline_hardening/defaults/main.yml new file mode 100644 index 0000000..7c93555 --- /dev/null +++ b/ansible/roles/debian_baseline_hardening/defaults/main.yml @@ -0,0 +1,39 @@ +--- +# Update apt metadata only when stale (seconds) +debian_baseline_apt_cache_valid_time: 3600 + +# Core host hardening packages +debian_baseline_packages: + - unattended-upgrades + - apt-listchanges + - fail2ban + - needrestart + - sudo + - ca-certificates + +# SSH hardening controls +debian_baseline_ssh_permit_root_login: "no" +debian_baseline_ssh_password_authentication: "no" +debian_baseline_ssh_pubkey_authentication: "yes" +debian_baseline_ssh_x11_forwarding: "no" +debian_baseline_ssh_max_auth_tries: 3 +debian_baseline_ssh_client_alive_interval: 300 +debian_baseline_ssh_client_alive_count_max: 2 +debian_baseline_ssh_allow_users: [] + +# unattended-upgrades controls +debian_baseline_enable_unattended_upgrades: true +debian_baseline_unattended_auto_upgrade: "1" +debian_baseline_unattended_update_lists: "1" + +# Kernel and network hardening sysctls +debian_baseline_sysctl_settings: + net.ipv4.conf.all.accept_redirects: "0" + net.ipv4.conf.default.accept_redirects: "0" + net.ipv4.conf.all.send_redirects: "0" + net.ipv4.conf.default.send_redirects: "0" + net.ipv4.conf.all.log_martians: "1" + net.ipv4.conf.default.log_martians: "1" + net.ipv4.tcp_syncookies: "1" + net.ipv6.conf.all.accept_redirects: "0" + net.ipv6.conf.default.accept_redirects: "0" diff --git a/ansible/roles/debian_baseline_hardening/handlers/main.yml b/ansible/roles/debian_baseline_hardening/handlers/main.yml new file mode 100644 index 0000000..0bcfa92 --- /dev/null +++ b/ansible/roles/debian_baseline_hardening/handlers/main.yml @@ -0,0 +1,12 @@ +--- +- name: Restart ssh + ansible.builtin.service: + name: ssh + state: restarted + +- name: Reload sysctl + ansible.builtin.command: + argv: + - sysctl + - --system + changed_when: true diff --git a/ansible/roles/debian_baseline_hardening/tasks/main.yml b/ansible/roles/debian_baseline_hardening/tasks/main.yml new file mode 100644 index 0000000..76053bb --- /dev/null +++ b/ansible/roles/debian_baseline_hardening/tasks/main.yml @@ -0,0 +1,52 @@ +--- +- name: Refresh apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: "{{ debian_baseline_apt_cache_valid_time }}" + +- name: Install baseline hardening packages + ansible.builtin.apt: + name: "{{ debian_baseline_packages }}" + state: present + +- name: Configure unattended-upgrades auto settings + ansible.builtin.copy: + dest: /etc/apt/apt.conf.d/20auto-upgrades + mode: "0644" + content: | + APT::Periodic::Update-Package-Lists "{{ debian_baseline_unattended_update_lists }}"; + APT::Periodic::Unattended-Upgrade "{{ debian_baseline_unattended_auto_upgrade }}"; + when: debian_baseline_enable_unattended_upgrades | bool + +- name: Configure SSH hardening options + ansible.builtin.copy: + dest: /etc/ssh/sshd_config.d/99-hardening.conf + mode: "0644" + content: | + PermitRootLogin {{ debian_baseline_ssh_permit_root_login }} + PasswordAuthentication {{ debian_baseline_ssh_password_authentication }} + PubkeyAuthentication {{ debian_baseline_ssh_pubkey_authentication }} + X11Forwarding {{ debian_baseline_ssh_x11_forwarding }} + MaxAuthTries {{ debian_baseline_ssh_max_auth_tries }} + ClientAliveInterval {{ debian_baseline_ssh_client_alive_interval }} + ClientAliveCountMax {{ debian_baseline_ssh_client_alive_count_max }} + {% if debian_baseline_ssh_allow_users | length > 0 %} + AllowUsers {{ debian_baseline_ssh_allow_users | join(' ') }} + {% endif %} + notify: Restart ssh + +- name: Configure baseline sysctls + ansible.builtin.copy: + dest: /etc/sysctl.d/99-hardening.conf + mode: "0644" + content: | + {% for key, value in debian_baseline_sysctl_settings.items() %} + {{ key }} = {{ value }} + {% endfor %} + notify: Reload sysctl + +- name: Ensure fail2ban service is enabled + ansible.builtin.service: + name: fail2ban + enabled: true + state: started diff --git a/ansible/roles/debian_maintenance/defaults/main.yml b/ansible/roles/debian_maintenance/defaults/main.yml new file mode 100644 index 0000000..f1c89d2 --- /dev/null +++ b/ansible/roles/debian_maintenance/defaults/main.yml @@ -0,0 +1,7 @@ +--- +debian_maintenance_apt_cache_valid_time: 3600 +debian_maintenance_upgrade_type: dist +debian_maintenance_autoremove: true +debian_maintenance_autoclean: true +debian_maintenance_reboot_if_required: true +debian_maintenance_reboot_timeout: 1800 diff --git a/ansible/roles/debian_maintenance/tasks/main.yml b/ansible/roles/debian_maintenance/tasks/main.yml new file mode 100644 index 0000000..0998a47 --- /dev/null +++ b/ansible/roles/debian_maintenance/tasks/main.yml @@ -0,0 +1,30 @@ +--- +- name: Refresh apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: "{{ debian_maintenance_apt_cache_valid_time }}" + +- name: Upgrade Debian packages + ansible.builtin.apt: + upgrade: "{{ debian_maintenance_upgrade_type }}" + +- name: Remove orphaned packages + ansible.builtin.apt: + autoremove: "{{ debian_maintenance_autoremove }}" + +- name: Clean apt package cache + ansible.builtin.apt: + autoclean: "{{ debian_maintenance_autoclean }}" + +- name: Check if reboot is required + ansible.builtin.stat: + path: /var/run/reboot-required + register: debian_maintenance_reboot_required_file + +- name: Reboot when required by package updates + ansible.builtin.reboot: + reboot_timeout: "{{ debian_maintenance_reboot_timeout }}" + msg: "Reboot initiated by Ansible maintenance playbook" + when: + - debian_maintenance_reboot_if_required | bool + - debian_maintenance_reboot_required_file.stat.exists | default(false) diff --git a/ansible/roles/debian_ssh_key_rotation/defaults/main.yml b/ansible/roles/debian_ssh_key_rotation/defaults/main.yml new file mode 100644 index 0000000..8a3603e --- /dev/null +++ b/ansible/roles/debian_ssh_key_rotation/defaults/main.yml @@ -0,0 +1,10 @@ +--- +# List of users to manage keys for. +# Example: +# debian_ssh_rotation_users: +# - name: deploy +# home: /home/deploy +# state: present +# keys: +# - "ssh-ed25519 AAAA... deploy@laptop" +debian_ssh_rotation_users: [] diff --git a/ansible/roles/debian_ssh_key_rotation/tasks/main.yml b/ansible/roles/debian_ssh_key_rotation/tasks/main.yml new file mode 100644 index 0000000..b8a45b4 --- /dev/null +++ b/ansible/roles/debian_ssh_key_rotation/tasks/main.yml @@ -0,0 +1,50 @@ +--- +- name: Validate SSH key rotation inputs + ansible.builtin.assert: + that: + - item.name is defined + - item.home is defined + - (item.state | default('present')) in ['present', 'absent'] + - (item.state | default('present')) == 'absent' or (item.keys is defined and item.keys | length > 0) + fail_msg: >- + Each entry in debian_ssh_rotation_users must include name, home, and either: + state=absent, or keys with at least one SSH public key. + loop: "{{ debian_ssh_rotation_users }}" + loop_control: + label: "{{ item.name | default('unknown') }}" + +- name: Ensure ~/.ssh exists for managed users + ansible.builtin.file: + path: "{{ item.home }}/.ssh" + state: directory + owner: "{{ item.name }}" + group: "{{ item.name }}" + mode: "0700" + loop: "{{ debian_ssh_rotation_users }}" + loop_control: + label: "{{ item.name }}" + when: (item.state | default('present')) == 'present' + +- name: Rotate authorized_keys for managed users + ansible.builtin.copy: + dest: "{{ item.home }}/.ssh/authorized_keys" + owner: "{{ item.name }}" + group: "{{ item.name }}" + mode: "0600" + content: | + {% for key in item.keys %} + {{ key }} + {% endfor %} + loop: "{{ debian_ssh_rotation_users }}" + loop_control: + label: "{{ item.name }}" + when: (item.state | default('present')) == 'present' + +- name: Remove authorized_keys for users marked absent + ansible.builtin.file: + path: "{{ item.home }}/.ssh/authorized_keys" + state: absent + loop: "{{ debian_ssh_rotation_users }}" + loop_control: + label: "{{ item.name }}" + when: (item.state | default('present')) == 'absent' diff --git a/ansible/roles/noble_argocd/defaults/main.yml b/ansible/roles/noble_argocd/defaults/main.yml new file mode 100644 index 0000000..2acb1c4 --- /dev/null +++ b/ansible/roles/noble_argocd/defaults/main.yml @@ -0,0 +1,4 @@ +--- +# When true, applies clusters/noble/bootstrap/argocd/root-application.yaml (app-of-apps). +# Edit spec.source.repoURL in that file if your Git remote differs. +noble_argocd_apply_root_application: false diff --git a/ansible/roles/noble_argocd/tasks/main.yml b/ansible/roles/noble_argocd/tasks/main.yml index 2f7649d..57c0d1f 100644 --- a/ansible/roles/noble_argocd/tasks/main.yml +++ b/ansible/roles/noble_argocd/tasks/main.yml @@ -15,6 +15,20 @@ - -f - "{{ noble_repo_root }}/clusters/noble/bootstrap/argocd/values.yaml" - --wait + - --timeout + - 15m environment: KUBECONFIG: "{{ noble_kubeconfig }}" changed_when: true + +- name: Apply Argo CD root Application (app-of-apps) + ansible.builtin.command: + argv: + - kubectl + - apply + - -f + - "{{ noble_repo_root }}/clusters/noble/bootstrap/argocd/root-application.yaml" + environment: + KUBECONFIG: "{{ noble_kubeconfig }}" + when: noble_argocd_apply_root_application | default(false) | bool + changed_when: true diff --git a/ansible/roles/noble_post_deploy/tasks/main.yml b/ansible/roles/noble_post_deploy/tasks/main.yml index a0b7808..8b3e350 100644 --- a/ansible/roles/noble_post_deploy/tasks/main.yml +++ b/ansible/roles/noble_post_deploy/tasks/main.yml @@ -9,5 +9,6 @@ - name: Argo CD optional root Application (empty app-of-apps) ansible.builtin.debug: msg: >- - Optional: kubectl apply -f clusters/noble/bootstrap/argocd/root-application.yaml - after editing repoURL. Core workloads are not synced by Argo — see clusters/noble/apps/README.md + App-of-apps: noble.yml applies root-application.yaml when noble_argocd_apply_root_application is true + (group_vars/all.yml). Otherwise: kubectl apply -f clusters/noble/bootstrap/argocd/root-application.yaml + after editing spec.source.repoURL. Core platform is Ansible — see clusters/noble/apps/README.md diff --git a/ansible/roles/proxmox_baseline/defaults/main.yml b/ansible/roles/proxmox_baseline/defaults/main.yml new file mode 100644 index 0000000..7dad422 --- /dev/null +++ b/ansible/roles/proxmox_baseline/defaults/main.yml @@ -0,0 +1,14 @@ +--- +proxmox_repo_debian_codename: "{{ ansible_facts['distribution_release'] | default('bookworm') }}" +proxmox_repo_disable_enterprise: true +proxmox_repo_disable_ceph_enterprise: true +proxmox_repo_enable_pve_no_subscription: true +proxmox_repo_enable_ceph_no_subscription: false + +proxmox_no_subscription_notice_disable: true +proxmox_widget_toolkit_file: /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js + +# Bootstrap root SSH keys from the control machine so subsequent runs can use key auth. +proxmox_root_authorized_key_files: + - "{{ lookup('env', 'HOME') }}/.ssh/id_ed25519.pub" + - "{{ lookup('env', 'HOME') }}/.ssh/ansible.pub" diff --git a/ansible/roles/proxmox_baseline/handlers/main.yml b/ansible/roles/proxmox_baseline/handlers/main.yml new file mode 100644 index 0000000..eb8178e --- /dev/null +++ b/ansible/roles/proxmox_baseline/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Restart pveproxy + ansible.builtin.service: + name: pveproxy + state: restarted diff --git a/ansible/roles/proxmox_baseline/tasks/main.yml b/ansible/roles/proxmox_baseline/tasks/main.yml new file mode 100644 index 0000000..bb414e1 --- /dev/null +++ b/ansible/roles/proxmox_baseline/tasks/main.yml @@ -0,0 +1,100 @@ +--- +- name: Check configured local public key files + ansible.builtin.stat: + path: "{{ item }}" + register: proxmox_root_pubkey_stats + loop: "{{ proxmox_root_authorized_key_files }}" + delegate_to: localhost + become: false + +- name: Fail when a configured local public key file is missing + ansible.builtin.fail: + msg: "Configured key file does not exist on the control host: {{ item.item }}" + when: not item.stat.exists + loop: "{{ proxmox_root_pubkey_stats.results }}" + delegate_to: localhost + become: false + +- name: Ensure root authorized_keys contains configured public keys + ansible.posix.authorized_key: + user: root + state: present + key: "{{ lookup('ansible.builtin.file', item) }}" + manage_dir: true + loop: "{{ proxmox_root_authorized_key_files }}" + +- name: Remove enterprise repository lines from /etc/apt/sources.list + ansible.builtin.lineinfile: + path: /etc/apt/sources.list + regexp: ".*enterprise\\.proxmox\\.com.*" + state: absent + when: + - proxmox_repo_disable_enterprise | bool or proxmox_repo_disable_ceph_enterprise | bool + failed_when: false + +- name: Find apt source files that contain Proxmox enterprise repositories + ansible.builtin.find: + paths: /etc/apt/sources.list.d + file_type: file + patterns: + - "*.list" + - "*.sources" + contains: "enterprise\\.proxmox\\.com" + use_regex: true + register: proxmox_enterprise_repo_files + when: + - proxmox_repo_disable_enterprise | bool or proxmox_repo_disable_ceph_enterprise | bool + +- name: Remove enterprise repository lines from apt source files + ansible.builtin.lineinfile: + path: "{{ item.path }}" + regexp: ".*enterprise\\.proxmox\\.com.*" + state: absent + loop: "{{ proxmox_enterprise_repo_files.files | default([]) }}" + when: + - proxmox_repo_disable_enterprise | bool or proxmox_repo_disable_ceph_enterprise | bool + +- name: Find apt source files that already contain pve-no-subscription + ansible.builtin.find: + paths: /etc/apt/sources.list.d + file_type: file + patterns: + - "*.list" + - "*.sources" + contains: "pve-no-subscription" + use_regex: false + register: proxmox_no_sub_repo_files + when: proxmox_repo_enable_pve_no_subscription | bool + +- name: Ensure Proxmox no-subscription repository is configured when absent + ansible.builtin.copy: + dest: /etc/apt/sources.list.d/pve-no-subscription.list + content: "deb http://download.proxmox.com/debian/pve {{ proxmox_repo_debian_codename }} pve-no-subscription\n" + mode: "0644" + when: + - proxmox_repo_enable_pve_no_subscription | bool + - (proxmox_no_sub_repo_files.matched | default(0) | int) == 0 + +- name: Remove duplicate pve-no-subscription.list when another source already provides it + ansible.builtin.file: + path: /etc/apt/sources.list.d/pve-no-subscription.list + state: absent + when: + - proxmox_repo_enable_pve_no_subscription | bool + - (proxmox_no_sub_repo_files.files | default([]) | map(attribute='path') | list | select('ne', '/etc/apt/sources.list.d/pve-no-subscription.list') | list | length) > 0 + +- name: Ensure Ceph no-subscription repository is configured + ansible.builtin.copy: + dest: /etc/apt/sources.list.d/ceph-no-subscription.list + content: "deb http://download.proxmox.com/debian/ceph-{{ proxmox_repo_debian_codename }} {{ proxmox_repo_debian_codename }} no-subscription\n" + mode: "0644" + when: proxmox_repo_enable_ceph_no_subscription | bool + +- name: Disable no-subscription pop-up in Proxmox UI + ansible.builtin.replace: + path: "{{ proxmox_widget_toolkit_file }}" + regexp: "if \\(data\\.status !== 'Active'\\)" + replace: "if (false)" + backup: true + when: proxmox_no_subscription_notice_disable | bool + notify: Restart pveproxy diff --git a/ansible/roles/proxmox_cluster/defaults/main.yml b/ansible/roles/proxmox_cluster/defaults/main.yml new file mode 100644 index 0000000..59688b8 --- /dev/null +++ b/ansible/roles/proxmox_cluster/defaults/main.yml @@ -0,0 +1,7 @@ +--- +proxmox_cluster_enabled: true +proxmox_cluster_name: pve-cluster +proxmox_cluster_master: "" +proxmox_cluster_master_ip: "" +proxmox_cluster_force: false +proxmox_cluster_master_root_password: "" diff --git a/ansible/roles/proxmox_cluster/tasks/main.yml b/ansible/roles/proxmox_cluster/tasks/main.yml new file mode 100644 index 0000000..a893762 --- /dev/null +++ b/ansible/roles/proxmox_cluster/tasks/main.yml @@ -0,0 +1,63 @@ +--- +- name: Skip cluster role when disabled + ansible.builtin.meta: end_host + when: not (proxmox_cluster_enabled | bool) + +- name: Check whether corosync cluster config exists + ansible.builtin.stat: + path: /etc/pve/corosync.conf + register: proxmox_cluster_corosync_conf + +- name: Set effective Proxmox cluster master + ansible.builtin.set_fact: + proxmox_cluster_master_effective: "{{ proxmox_cluster_master | default(groups['proxmox_hosts'][0], true) }}" + +- name: Set effective Proxmox cluster master IP + ansible.builtin.set_fact: + proxmox_cluster_master_ip_effective: >- + {{ + proxmox_cluster_master_ip + | default(hostvars[proxmox_cluster_master_effective].ansible_host + | default(proxmox_cluster_master_effective), true) + }} + +- name: Create cluster on designated master + ansible.builtin.command: + cmd: "pvecm create {{ proxmox_cluster_name }}" + when: + - inventory_hostname == proxmox_cluster_master_effective + - not proxmox_cluster_corosync_conf.stat.exists + +- name: Ensure python3-pexpect is installed for password-based cluster join + ansible.builtin.apt: + name: python3-pexpect + state: present + update_cache: true + when: + - inventory_hostname != proxmox_cluster_master_effective + - not proxmox_cluster_corosync_conf.stat.exists + - proxmox_cluster_master_root_password | length > 0 + +- name: Join node to existing cluster (password provided) + ansible.builtin.expect: + command: >- + pvecm add {{ proxmox_cluster_master_ip_effective }} + {% if proxmox_cluster_force | bool %}--force{% endif %} + responses: + "Please enter superuser \\(root\\) password for '.*':": "{{ proxmox_cluster_master_root_password }}" + "password:": "{{ proxmox_cluster_master_root_password }}" + no_log: true + when: + - inventory_hostname != proxmox_cluster_master_effective + - not proxmox_cluster_corosync_conf.stat.exists + - proxmox_cluster_master_root_password | length > 0 + +- name: Join node to existing cluster (SSH trust/no prompt) + ansible.builtin.command: + cmd: >- + pvecm add {{ proxmox_cluster_master_ip_effective }} + {% if proxmox_cluster_force | bool %}--force{% endif %} + when: + - inventory_hostname != proxmox_cluster_master_effective + - not proxmox_cluster_corosync_conf.stat.exists + - proxmox_cluster_master_root_password | length == 0 diff --git a/ansible/roles/proxmox_maintenance/defaults/main.yml b/ansible/roles/proxmox_maintenance/defaults/main.yml new file mode 100644 index 0000000..3a4d150 --- /dev/null +++ b/ansible/roles/proxmox_maintenance/defaults/main.yml @@ -0,0 +1,6 @@ +--- +proxmox_upgrade_apt_cache_valid_time: 3600 +proxmox_upgrade_autoremove: true +proxmox_upgrade_autoclean: true +proxmox_upgrade_reboot_if_required: true +proxmox_upgrade_reboot_timeout: 1800 diff --git a/ansible/roles/proxmox_maintenance/tasks/main.yml b/ansible/roles/proxmox_maintenance/tasks/main.yml new file mode 100644 index 0000000..2df8e21 --- /dev/null +++ b/ansible/roles/proxmox_maintenance/tasks/main.yml @@ -0,0 +1,30 @@ +--- +- name: Refresh apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: "{{ proxmox_upgrade_apt_cache_valid_time }}" + +- name: Upgrade Proxmox host packages + ansible.builtin.apt: + upgrade: dist + +- name: Remove orphaned packages + ansible.builtin.apt: + autoremove: "{{ proxmox_upgrade_autoremove }}" + +- name: Clean apt package cache + ansible.builtin.apt: + autoclean: "{{ proxmox_upgrade_autoclean }}" + +- name: Check if reboot is required + ansible.builtin.stat: + path: /var/run/reboot-required + register: proxmox_reboot_required_file + +- name: Reboot when required by package upgrades + ansible.builtin.reboot: + reboot_timeout: "{{ proxmox_upgrade_reboot_timeout }}" + msg: "Reboot initiated by Ansible Proxmox maintenance playbook" + when: + - proxmox_upgrade_reboot_if_required | bool + - proxmox_reboot_required_file.stat.exists | default(false)