From 55991dcb4e19b4be9be8ff2c7cb38aebd7691e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BBbikowski?= Date: Sun, 12 Jul 2026 08:44:40 +0200 Subject: [PATCH] v1.0.0 --- .gitignore | 2 + ansible.cfg | 12 ++ cron/ansible-autopilot.logrotate | 11 ++ cron/crontab.example | 16 +++ cron/run-autopilot.sh | 32 ++++++ group_vars/all.yml | 15 +++ group_vars/mgmt.yml | 3 + group_vars/root.yml | 3 + patchmon_inventory.yml.example | 6 + playbooks/playbook_autopilot.yml | 22 ++++ playbooks/playbook_cleanup.yml | 46 ++++++++ playbooks/playbook_docker_cleanup.yml | 56 +++++++++ playbooks/playbook_fleet_report.yml | 66 +++++++++++ playbooks/playbook_security_baseline.yml | 64 +++++++++++ playbooks/playbook_ssh.yml | 23 ++++ playbooks/playbook_ssh_keys.yml | 32 ++++++ playbooks/playbook_upgrade.yml | 16 +++ readme.md | 139 +++++++++++++++++++++++ reports/.gitkeep | 0 templates/50unattended-upgrades.j2 | 13 +++ templates/fleet_report.j2 | 33 ++++++ 21 files changed, 610 insertions(+) create mode 100644 .gitignore create mode 100644 ansible.cfg create mode 100644 cron/ansible-autopilot.logrotate create mode 100644 cron/crontab.example create mode 100755 cron/run-autopilot.sh create mode 100644 group_vars/all.yml create mode 100644 group_vars/mgmt.yml create mode 100644 group_vars/root.yml create mode 100644 patchmon_inventory.yml.example create mode 100644 playbooks/playbook_autopilot.yml create mode 100644 playbooks/playbook_cleanup.yml create mode 100644 playbooks/playbook_docker_cleanup.yml create mode 100644 playbooks/playbook_fleet_report.yml create mode 100644 playbooks/playbook_security_baseline.yml create mode 100644 playbooks/playbook_ssh.yml create mode 100644 playbooks/playbook_ssh_keys.yml create mode 100644 playbooks/playbook_upgrade.yml create mode 100644 readme.md create mode 100644 reports/.gitkeep create mode 100644 templates/50unattended-upgrades.j2 create mode 100644 templates/fleet_report.j2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2a4484c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +ssh_keys/ +patchmon_inventory.yml diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..83a86f8 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,12 @@ +[defaults] +inventory = patchmon_inventory.yml +localhost_warning = False +interpreter_python = auto_silent +timeout = 30 + +[ssh_connection] +ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ConnectTimeout=30 -o ConnectionAttempts=5 + +[inventory] +enable_plugins = patchmon.dynamic_inventory.dynamic_inventory + diff --git a/cron/ansible-autopilot.logrotate b/cron/ansible-autopilot.logrotate new file mode 100644 index 0000000..e5461d6 --- /dev/null +++ b/cron/ansible-autopilot.logrotate @@ -0,0 +1,11 @@ +# Przykładowa konfiguracja logrotate dla logu bezobsługowych przebiegów autopilot. +# Instalacja: sudo cp cron/ansible-autopilot.logrotate /etc/logrotate.d/ansible-autopilot +/var/log/ansible-autopilot.log { + weekly + rotate 8 + compress + delaycompress + missingok + notifempty + copytruncate +} diff --git a/cron/crontab.example b/cron/crontab.example new file mode 100644 index 0000000..d68e66f --- /dev/null +++ b/cron/crontab.example @@ -0,0 +1,16 @@ +# Przykładowy wpis crontab dla bezobsługowego zarządzania flotą przez PatchMon + Ansible. +# +# Instalacja: +# crontab -e +# (wklej poniższy wiersz, dostosuj godzinę) +# +# albo jako plik w /etc/cron.d/ (wtedy trzeba dodać pole z użytkownikiem, np. "root"): +# sudo cp crontab.example /etc/cron.d/ansible-autopilot +# +# Cotygodniowo, w niedzielę o 03:30: aktualizacja pakietów + sprzątanie + raport floty. +# Log trafia do /var/log/ansible-autopilot.log (patrz cron/ansible-autopilot.logrotate). +30 3 * * 0 /root/ansible/cron/run-autopilot.sh + +# Opcjonalnie: sam raport stanu floty (bez aktualizacji) codziennie o 07:00, +# żeby mieć świeże dane o dysku/reboot-required do porannego przeglądu. +# 0 7 * * * cd /root/ansible && /usr/bin/ansible-playbook -i patchmon_inventory.yml playbooks/playbook_fleet_report.yml --limit "Update-now,localhost" >> /var/log/ansible-autopilot.log 2>&1 diff --git a/cron/run-autopilot.sh b/cron/run-autopilot.sh new file mode 100755 index 0000000..8196e10 --- /dev/null +++ b/cron/run-autopilot.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Wrapper do bezobsługowego uruchamiania playbook_autopilot.yml z crona. +# - flock zapobiega nakładającym się uruchomieniom (np. jeśli poprzedni przebieg +# jeszcze trwa, gdy odpala się kolejny). +# - Wynik (stdout+stderr) trafia z timestampem do LOG_FILE. +# - Reboot NIE jest wykonywany automatycznie — playbook_fleet_report.yml tylko +# zgłasza, które hosty go wymagają (patrz reports/). +set -uo pipefail + +ANSIBLE_DIR="/root/ansible" +LOCK_FILE="/var/run/ansible-autopilot.lock" +LOG_FILE="/var/log/ansible-autopilot.log" +TARGET_GROUP="${TARGET_GROUP:-Update-now}" + +exec 200>"${LOCK_FILE}" +if ! flock -n 200; then + echo "$(date '+%Y-%m-%d %H:%M:%S') [run-autopilot] Poprzedni przebieg wciąż trwa, pomijam." >>"${LOG_FILE}" + exit 0 +fi + +cd "${ANSIBLE_DIR}" + +{ + echo "===== $(date '+%Y-%m-%d %H:%M:%S') [run-autopilot] START (target: ${TARGET_GROUP}) =====" + ansible-playbook -i patchmon_inventory.yml \ + playbooks/playbook_autopilot.yml \ + --limit "${TARGET_GROUP},localhost" + rc=$? + echo "===== $(date '+%Y-%m-%d %H:%M:%S') [run-autopilot] KONIEC (exit code: ${rc}) =====" +} >>"${LOG_FILE}" 2>&1 + +exit "${rc}" diff --git a/group_vars/all.yml b/group_vars/all.yml new file mode 100644 index 0000000..19fd79a --- /dev/null +++ b/group_vars/all.yml @@ -0,0 +1,15 @@ +--- +# Wspólne ustawienia połączenia dla wszystkich hostów z inwentarza PatchMon. +# playbook_dir sprawia, że ścieżka do klucza działa niezależnie od katalogu, +# z którego wywołujemy ansible-playbook. +ansible_ssh_private_key_file: "{{ playbook_dir }}/../ssh_keys/id_rsa" +ansible_ssh_common_args: '-o StrictHostKeyChecking=no' +ansible_become: true +ansible_become_method: sudo + +# Klucze publiczne wdrażane przez playbooks/playbook_ssh_keys.yml. +# Uzupełnij o właściwe klucze przed użyciem tego playbooka. +admin_ssh_keys: [] + +# Próg (%) zajętości dysku, po którym playbooks/playbook_cleanup.yml zgłasza ostrzeżenie. +disk_usage_warn_percent: 85 diff --git a/group_vars/mgmt.yml b/group_vars/mgmt.yml new file mode 100644 index 0000000..a4dba93 --- /dev/null +++ b/group_vars/mgmt.yml @@ -0,0 +1,3 @@ +--- +# Hosty w grupie PatchMon "mgmt" logują się kontem mgmt i podnoszą uprawnienia przez sudo. +ansible_user: mgmt diff --git a/group_vars/root.yml b/group_vars/root.yml new file mode 100644 index 0000000..8814e4e --- /dev/null +++ b/group_vars/root.yml @@ -0,0 +1,3 @@ +--- +# Hosty w grupie PatchMon "root" logują się bezpośrednio jako root. +ansible_user: root diff --git a/patchmon_inventory.yml.example b/patchmon_inventory.yml.example new file mode 100644 index 0000000..598bcb1 --- /dev/null +++ b/patchmon_inventory.yml.example @@ -0,0 +1,6 @@ +--- +plugin: patchmon.dynamic_inventory.dynamic_inventory +api_url: http://patchmon.example.local/api/v1/api/hosts +api_key: CHANGE_ME_API_KEY +api_secret: CHANGE_ME_API_SECRET +verify_ssl: false diff --git a/playbooks/playbook_autopilot.yml b/playbooks/playbook_autopilot.yml new file mode 100644 index 0000000..5db52d4 --- /dev/null +++ b/playbooks/playbook_autopilot.yml @@ -0,0 +1,22 @@ +--- +# Playbook do bezobsługowego uruchamiania z crona (patrz cron/run-autopilot.sh). +# Kolejność: aktualizacja pakietów -> sprzątanie/kondycja -> raport floty. +# +# Świadomie NIE zawiera: +# - playbook_security_baseline.yml (zmiany w sshd/unattended-upgrades — uruchamiać ręcznie) +# - restartu hostów (reboot jest tylko raportowany w fleet_report, decyzję +# o restarcie podejmuje operator ręcznie) +# +# Użycie: ansible-playbook -i patchmon_inventory.yml playbooks/playbook_autopilot.yml --limit "Update-now,localhost" +# +# WAŻNE: ",localhost" w --limit jest wymagane — playbook_fleet_report.yml ma +# drugą fazę na hosts: localhost (renderowanie raportu), a --limit bez "localhost" +# w wzorcu powoduje jej pominięcie ("skipping: no hosts matched"). +- name: Aktualizacja pakietów + import_playbook: playbook_upgrade.yml + +- name: Sprzątanie i kondycja systemu + import_playbook: playbook_cleanup.yml + +- name: Raport stanu floty + import_playbook: playbook_fleet_report.yml diff --git a/playbooks/playbook_cleanup.yml b/playbooks/playbook_cleanup.yml new file mode 100644 index 0000000..2dda7ad --- /dev/null +++ b/playbooks/playbook_cleanup.yml @@ -0,0 +1,46 @@ +--- +# Rutynowe sprzątanie/kondycja systemu: apt autoremove/clean, czyszczenie journala, +# sprawdzenie zajętości dysku z progiem ostrzegawczym (domyślnie 85%, group_vars/all.yml). +- name: Sprzątanie i kondycja systemu + hosts: all + gather_facts: true + + tasks: + - name: Usuń nieużywane pakiety (apt autoremove) + ansible.builtin.apt: + autoremove: yes + purge: yes + when: ansible_os_family in ["Debian", "Ubuntu"] + + - name: Wyczyść lokalny cache pobranych pakietów (apt clean) + ansible.builtin.apt: + autoclean: yes + when: ansible_os_family in ["Debian", "Ubuntu"] + + - name: Wyczyść dziennik systemowy starszy niż 7 dni (journalctl --vacuum-time) + ansible.builtin.command: journalctl --vacuum-time=7d + register: journal_vacuum + changed_when: "'Vacuuming done' in journal_vacuum.stdout and 'freed' in journal_vacuum.stdout" + when: ansible_service_mgr == "systemd" + + - name: Odśwież fakty o zajętości dysku (po sprzątaniu) + ansible.builtin.setup: + filter: ansible_mounts + gather_timeout: 30 + + - name: Sprawdź zajętość dysku na partycji root + ansible.builtin.set_fact: + root_disk_percent_used: >- + {{ (( (mount.size_total - mount.size_available) / mount.size_total * 100) | round(0, 'ceil')) | int }} + vars: + mount: "{{ ansible_mounts | selectattr('mount', 'equalto', '/') | first }}" + when: ansible_mounts | selectattr('mount', 'equalto', '/') | list | length > 0 + + - name: OSTRZEŻENIE — zajętość dysku powyżej progu + ansible.builtin.debug: + msg: >- + WARN [{{ inventory_hostname }}]: dysk / zajęty w {{ root_disk_percent_used }}% + (próg: {{ disk_usage_warn_percent }}%) + when: + - root_disk_percent_used is defined + - root_disk_percent_used | int >= disk_usage_warn_percent | int diff --git a/playbooks/playbook_docker_cleanup.yml b/playbooks/playbook_docker_cleanup.yml new file mode 100644 index 0000000..b65c515 --- /dev/null +++ b/playbooks/playbook_docker_cleanup.yml @@ -0,0 +1,56 @@ +--- +# Czyszczenie nieużywanych obrazów Docker — tylko na hostach, na których Docker +# jest faktycznie zainstalowany (pozostałe hosty są bezpiecznie pomijane). +# +# Domyślnie usuwa wyłącznie obrazy "dangling" (bez tagu) — bezpieczne, nie rusza +# tagowanych obrazów, nawet jeśli akurat nie są używane przez żaden kontener. +# +# Aby usunąć WSZYSTKIE nieużywane obrazy (także tagowane, nieużywane przez żaden +# kontener) — więcej odzyskanego miejsca, ale bardziej agresywne — uruchom z: +# -e "docker_prune_all_images=true" +- name: Czyszczenie nieużywanych obrazów Docker + hosts: all + gather_facts: false + vars: + docker_prune_all_images: false + + tasks: + - name: Sprawdź, czy docker jest zainstalowany + ansible.builtin.command: which docker + register: docker_check + changed_when: false + failed_when: false + + - name: Pomiń — Docker nie jest zainstalowany na tym hoście + ansible.builtin.debug: + msg: "Docker nie jest zainstalowany na {{ inventory_hostname }} — pomijam." + when: docker_check.rc != 0 + + - name: Zajętość miejsca przez Dockera przed czyszczeniem + ansible.builtin.command: docker system df + register: docker_df_before + changed_when: false + when: docker_check.rc == 0 + + - name: Usuń nieużywane obrazy Docker (dangling) + ansible.builtin.command: docker image prune -f + register: prune_result + changed_when: "'Total reclaimed space: 0B' not in prune_result.stdout" + when: + - docker_check.rc == 0 + - not docker_prune_all_images + + - name: Usuń WSZYSTKIE nieużywane obrazy Docker (także tagowane) + ansible.builtin.command: docker image prune -a -f + register: prune_result_all + changed_when: "'Total reclaimed space: 0B' not in prune_result_all.stdout" + when: + - docker_check.rc == 0 + - docker_prune_all_images + + - name: Wynik czyszczenia obrazów + ansible.builtin.debug: + msg: >- + {{ (prune_result_all if docker_prune_all_images else prune_result).stdout_lines + | default(['brak danych']) }} + when: docker_check.rc == 0 diff --git a/playbooks/playbook_fleet_report.yml b/playbooks/playbook_fleet_report.yml new file mode 100644 index 0000000..fce2ee5 --- /dev/null +++ b/playbooks/playbook_fleet_report.yml @@ -0,0 +1,66 @@ +--- +# Raport stanu floty (tylko odczyt, bez żadnych zmian na hostach): +# OS/wersja, uptime, zajętość dysku, liczba oczekujących aktualizacji +# oraz flaga reboot-required (decyzję o restarcie podejmuje operator ręcznie). +# +# WAŻNE: druga faza (renderowanie raportu) działa na hosts: localhost — jeśli +# używasz --limit, dopisz do niego ",localhost", inaczej ta faza zostanie pominięta, +# np.: --limit "Update-now,localhost" +- name: Zbierz stan floty + hosts: all + gather_facts: true + + tasks: + - name: Sprawdź czy wymagany jest restart (reboot-required) + ansible.builtin.stat: + path: /var/run/reboot-required + register: reboot_required_stat + + - name: Policz pakiety oczekujące na aktualizację + ansible.builtin.command: apt list --upgradable + register: upgradable_raw + changed_when: false + failed_when: false + when: ansible_os_family in ["Debian", "Ubuntu"] + + - name: Sprawdź zajętość dysku na partycji root + ansible.builtin.set_fact: + root_disk_percent_used: >- + {{ (( (mount.size_total - mount.size_available) / mount.size_total * 100) | round(0, 'ceil')) | int }} + vars: + mount: "{{ ansible_mounts | selectattr('mount', 'equalto', '/') | first }}" + when: ansible_mounts | selectattr('mount', 'equalto', '/') | list | length > 0 + + - name: Zbuduj podsumowanie hosta + ansible.builtin.set_fact: + host_report: + os: "{{ ansible_distribution }} {{ ansible_distribution_version }}" + uptime_days: "{{ (ansible_uptime_seconds / 86400) | round(1) }}" + disk_root_percent: "{{ root_disk_percent_used | default('n/a') }}" + upgradable_packages: >- + {{ (upgradable_raw.stdout_lines | default([]) | length - 1) + if (upgradable_raw is defined and upgradable_raw.stdout_lines is defined and upgradable_raw.stdout_lines | length > 0) + else 0 }} + reboot_required: "{{ reboot_required_stat.stat.exists }}" + +- name: Wygeneruj raport floty + hosts: localhost + connection: local + gather_facts: true + + tasks: + - name: Upewnij się, że katalog reports/ istnieje + ansible.builtin.file: + path: "{{ playbook_dir }}/../reports" + state: directory + mode: "0755" + + - name: Renderuj raport floty + ansible.builtin.template: + src: "{{ playbook_dir }}/../templates/fleet_report.j2" + dest: "{{ playbook_dir }}/../reports/fleet_report_{{ ansible_date_time.iso8601_basic_short }}.txt" + mode: "0644" + + - name: Ścieżka wygenerowanego raportu + ansible.builtin.debug: + msg: "Raport zapisany w reports/fleet_report_{{ ansible_date_time.iso8601_basic_short }}.txt" diff --git a/playbooks/playbook_security_baseline.yml b/playbooks/playbook_security_baseline.yml new file mode 100644 index 0000000..cf8c0c2 --- /dev/null +++ b/playbooks/playbook_security_baseline.yml @@ -0,0 +1,64 @@ +--- +# Baza bezpieczeństwa: unattended-upgrades (tylko aktualizacje bezpieczeństwa, +# bez automatycznego restartu — patrz playbook_fleet_report.yml dla flagi reboot-required) +# oraz wyłączenie logowania hasłem po SSH. +- name: Baza bezpieczeństwa (unattended-upgrades + wyłączenie logowania hasłem) + hosts: all + gather_facts: true + + tasks: + - name: Zainstaluj unattended-upgrades (Debian/Ubuntu) + ansible.builtin.apt: + name: + - unattended-upgrades + - apt-listchanges + state: present + update_cache: yes + cache_valid_time: 3600 + when: ansible_os_family in ["Debian", "Ubuntu"] + + - name: Skonfiguruj unattended-upgrades (tylko aktualizacje bezpieczeństwa, bez auto-reboot) + ansible.builtin.template: + src: ../templates/50unattended-upgrades.j2 + dest: /etc/apt/apt.conf.d/50unattended-upgrades + owner: root + group: root + mode: "0644" + when: ansible_os_family in ["Debian", "Ubuntu"] + + - name: Włącz okresowe aktualizacje APT (20auto-upgrades) + ansible.builtin.copy: + dest: /etc/apt/apt.conf.d/20auto-upgrades + owner: root + group: root + mode: "0644" + content: | + APT::Periodic::Update-Package-Lists "1"; + APT::Periodic::Unattended-Upgrade "1"; + when: ansible_os_family in ["Debian", "Ubuntu"] + + - name: Wyłącz logowanie hasłem (PasswordAuthentication no) + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '(?i)^\s*#?\s*PasswordAuthentication' + line: 'PasswordAuthentication no' + state: present + backup: yes + validate: '/usr/sbin/sshd -t -f %s' + notify: Restart SSH + + - name: Ogranicz logowanie root do klucza (PermitRootLogin prohibit-password) + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '(?i)^\s*#?\s*PermitRootLogin' + line: 'PermitRootLogin prohibit-password' + state: present + backup: yes + validate: '/usr/sbin/sshd -t -f %s' + notify: Restart SSH + + handlers: + - name: Restart SSH + ansible.builtin.service: + name: ssh + state: restarted diff --git a/playbooks/playbook_ssh.yml b/playbooks/playbook_ssh.yml new file mode 100644 index 0000000..8f77125 --- /dev/null +++ b/playbooks/playbook_ssh.yml @@ -0,0 +1,23 @@ +--- +- name: Konfiguracja SSH (blokada pustych haseł) + hosts: all + gather_facts: true + + tasks: + - name: Zablokuj logowanie na puste hasła (PermitEmptyPasswords no) + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + # Szuka linii zaczynającej się od PermitEmptyPasswords (również zakomentowanej) + regexp: '(?i)^\s*#?\s*PermitEmptyPasswords' + line: 'PermitEmptyPasswords no' + state: present + backup: yes + # Sprawdza poprawność pliku przed zapisem, aby nie zepsuć SSH + validate: '/usr/sbin/sshd -t -f %s' + notify: Restart SSH + + handlers: + - name: Restart SSH + ansible.builtin.service: + name: ssh + state: restarted diff --git a/playbooks/playbook_ssh_keys.yml b/playbooks/playbook_ssh_keys.yml new file mode 100644 index 0000000..4690ee6 --- /dev/null +++ b/playbooks/playbook_ssh_keys.yml @@ -0,0 +1,32 @@ +--- +# Zarządzanie kluczami publicznymi SSH konta logowania (root lub mgmt, w zależności +# od grupy PatchMon hosta) na całej flocie jednym uruchomieniem. +# +# Lista kluczy: admin_ssh_keys w group_vars/all.yml (uzupełnij przed użyciem). +# +# Domyślnie tylko DODAJE brakujące klucze (bezpieczne). Aby wymusić, żeby +# authorized_keys zawierał WYŁĄCZNIE klucze z admin_ssh_keys (usuwając inne), +# uruchom z: -e "ssh_keys_exclusive=true" — rób to świadomie, może odciąć dostęp. +- name: Zarządzanie kluczami SSH konta logowania + hosts: all + gather_facts: false + vars: + ssh_keys_exclusive: false + + tasks: + - name: Sprawdź, czy admin_ssh_keys nie jest puste + ansible.builtin.assert: + that: + - admin_ssh_keys | length > 0 + fail_msg: >- + admin_ssh_keys jest puste — uzupełnij listę kluczy w group_vars/all.yml + przed uruchomieniem tego playbooka. + run_once: true + delegate_to: localhost + + - name: Zapewnij obecność kluczy publicznych w authorized_keys + ansible.posix.authorized_key: + user: "{{ ansible_user }}" + key: "{{ admin_ssh_keys | join('\n') }}" + state: present + exclusive: "{{ ssh_keys_exclusive }}" diff --git a/playbooks/playbook_upgrade.yml b/playbooks/playbook_upgrade.yml new file mode 100644 index 0000000..3876373 --- /dev/null +++ b/playbooks/playbook_upgrade.yml @@ -0,0 +1,16 @@ +--- +- name: Aktualizacja pakietów (Debian/Ubuntu) + hosts: all + gather_facts: true + tasks: + - name: Aktualizacja cache pakietów (Debian/Ubuntu) + ansible.builtin.apt: + update_cache: yes + cache_valid_time: 3600 + when: ansible_os_family in ["Debian", "Ubuntu"] + + - name: Aktualizacja wszystkich pakietów (Debian/Ubuntu) + ansible.builtin.apt: + upgrade: dist + when: ansible_os_family in ["Debian", "Ubuntu"] + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..25fc4cc --- /dev/null +++ b/readme.md @@ -0,0 +1,139 @@ +# Ansible + PatchMon — zarządzanie flotą serwerów + +Inwentarz jest pobierany dynamicznie z PatchMon (`patchmon_inventory.yml`, plugin +`patchmon.dynamic_inventory`). Grupy hostów pochodzą wprost z grup przypisanych +w PatchMon — filtrowanie na potrzeby playbooków (`--limit`) odbywa się przez te grupy, +nie przez ręczne listy hostów. + +## Pierwsze uruchomienie (świeży klon repozytorium) + +Sekrety nie są trzymane w gicie — trzeba je uzupełnić lokalnie: + +```bash +cp patchmon_inventory.yml.example patchmon_inventory.yml +# uzupełnij api_url / api_key / api_secret w patchmon_inventory.yml + +mkdir -p ssh_keys && chmod 700 ssh_keys +cp /ścieżka/do/twojego/klucza ssh_keys/id_rsa && chmod 600 ssh_keys/id_rsa +``` + +## Grupy PatchMon używane w tym projekcie + +| Grupa | Znaczenie | +|---|---| +| `root` | Hosty logowane bezpośrednio na konto `root`. | +| `mgmt` | Hosty logowane na konto `mgmt` (z `sudo`). | +| `Update-now` | Wszystkie hosty dopuszczone do automatycznej aktualizacji (`root` ∪ `mgmt`). Domyślny cel dla crona/autopilota. | +| `Type-HOST` | Fizyczne hosty Proxmox — traktuj ostrożniej (restart hosta restartuje też jego VM/CT). | +| `Type-VM` / `Type-CT` | Maszyny wirtualne / kontenery LXC. | +| `Site-nni` / `Site-on` | Lokalizacje. | + +Konto logowania (`ansible_user`) **nie** jest już podawane ręcznie przez `-e` — jest +ustawiane automatycznie przez `group_vars/root.yml` i `group_vars/mgmt.yml` na +podstawie grupy PatchMon danego hosta. Dzięki temu jedno uruchomienie playbooka na +grupie mieszanej (np. `Update-now`) obsłuży poprawnie oba typy kont. + +## Struktura projektu + +``` +ssh_keys/ + id_rsa # klucz prywatny SSH (poza gitem, patrz sekcja "Sekrety") +group_vars/ + all.yml # klucz SSH, become, próg dysku, lista kluczy admina + root.yml / mgmt.yml # ansible_user per grupa PatchMon +playbooks/ + playbook_upgrade.yml # apt update + dist-upgrade + playbook_cleanup.yml # autoremove/autoclean, czyszczenie journala, alarm dysku + playbook_docker_cleanup.yml # czyszczenie nieużywanych obrazów Docker (tylko hosty z Dockerem) + playbook_fleet_report.yml # raport: OS, uptime, dysk, liczba aktualizacji, reboot-required + playbook_security_baseline.yml # unattended-upgrades + wyłączenie logowania hasłem SSH + playbook_ssh.yml # blokada pustych haseł SSH + playbook_ssh_keys.yml # zarządzanie kluczami publicznymi na koncie logowania + playbook_autopilot.yml # orkiestracja pod cron: upgrade -> cleanup -> fleet_report +templates/ + 50unattended-upgrades.j2 + fleet_report.j2 +cron/ + run-autopilot.sh # wrapper do crona (flock + log) + crontab.example + ansible-autopilot.logrotate +reports/ # tu trafiają wygenerowane raporty floty +``` + +## Użycie + +```bash +# Aktualizacja pakietów na całej flocie dopuszczonej do auto-update +ansible-playbook -i patchmon_inventory.yml playbooks/playbook_upgrade.yml --limit "Update-now" + +# Sprzątanie/kondycja systemu (autoremove, journal, alarm zajętości dysku) +ansible-playbook -i patchmon_inventory.yml playbooks/playbook_cleanup.yml --limit "Update-now" + +# Czyszczenie nieużywanych obrazów Docker (tylko hosty, na których jest Docker) +ansible-playbook -i patchmon_inventory.yml playbooks/playbook_docker_cleanup.yml --limit "Update-now" + +# Raport stanu floty -> plik w reports/ +# WAŻNE: dopisz ",localhost" do --limit, inaczej faza renderowania raportu +# zostanie pominięta ("skipping: no hosts matched"). +ansible-playbook -i patchmon_inventory.yml playbooks/playbook_fleet_report.yml --limit "Update-now,localhost" + +# Baza bezpieczeństwa: unattended-upgrades + wyłączenie logowania hasłem SSH +# (dotyka sshd_config — uruchamiaj świadomie, ręcznie, nie z crona) +ansible-playbook -i patchmon_inventory.yml playbooks/playbook_security_baseline.yml --limit "Update-now" + +# Blokada logowania na puste hasła +ansible-playbook -i patchmon_inventory.yml playbooks/playbook_ssh.yml --limit "Update-now" + +# Zarządzanie kluczami SSH konta logowania (uzupełnij admin_ssh_keys w group_vars/all.yml) +ansible-playbook -i patchmon_inventory.yml playbooks/playbook_ssh_keys.yml --limit "Update-now" + +# Pełny przebieg autopilot ręcznie (to samo co robi cron) +ansible-playbook -i patchmon_inventory.yml playbooks/playbook_autopilot.yml --limit "Update-now,localhost" +``` + +Można też celować w węższe grupy, np. `--limit "Site-on"`, `--limit "Type-CT"`, +albo pojedynczy host: `--limit "pve"`. + +## Reboot — tylko raportowanie, nigdy automatycznie + +`playbook_fleet_report.yml` sprawdza `/var/run/reboot-required` na każdym hoście +i wypisuje listę hostów wymagających restartu w raporcie (`reports/fleet_report_*.txt`). +**Żaden playbook w tym projekcie nie restartuje hostów automatycznie** — decyzję +i moment restartu (szczególnie dla `Type-HOST`, czyli fizycznego Proxmoksa) podejmuje +operator ręcznie, po przejrzeniu raportu. + +## Bezobsługowe uruchamianie (cron) + +`cron/run-autopilot.sh` to wrapper na `playbook_autopilot.yml`: +- blokuje się przez `flock`, żeby nie nakładały się dwa równoległe przebiegi, +- loguje przebieg (start/koniec/kod wyjścia) do `/var/log/ansible-autopilot.log`, +- domyślnie celuje w grupę `Update-now` (nadpisywalne zmienną `TARGET_GROUP`). + +Instalacja: + +```bash +chmod +x cron/run-autopilot.sh +crontab -e # wklej zawartość cron/crontab.example +sudo cp cron/ansible-autopilot.logrotate /etc/logrotate.d/ansible-autopilot +``` + +Każdy przebieg `playbook_fleet_report.yml` (w tym z crona) dokłada nowy plik do +`reports/` — warto od czasu do czasu posprzątać stare raporty, np. +`find reports/ -name 'fleet_report_*.txt' -mtime +30 -delete`. + +## Sekrety + +- **Klucz prywatny SSH** leży w `ssh_keys/id_rsa` (katalog `chmod 700`, plik `chmod 600`). + Katalog `ssh_keys/` jest w `.gitignore` — klucz nigdy nie trafia do repozytorium. + Ścieżka do niego jest ustawiona centralnie w `group_vars/all.yml` + (`ansible_ssh_private_key_file: "{{ playbook_dir }}/../ssh_keys/id_rsa"`), więc + wszystkie playbooki i `cron/run-autopilot.sh` korzystają z niej automatycznie — + nie trzeba nic dodatkowo konfigurować. +- **`api_key`/`api_secret`/`api_url` PatchMon** leżą w `patchmon_inventory.yml`, który + jest w `.gitignore` — nigdy nie trafia do repozytorium. Wersja do repo to + `patchmon_inventory.yml.example` (same placeholdery, bez sekretów). Przy zakładaniu + projektu od zera: `cp patchmon_inventory.yml.example patchmon_inventory.yml` i + uzupełnij `api_url`/`api_key`/`api_secret`. + (Plugin `patchmon.dynamic_inventory` wczytuje ten plik bezpośrednio, bez + templatowania Jinja2/Vault, dlatego to rozdzielenie na plik realny + `.example`, + a nie np. `{{ lookup('env', ...) }}`, jest tu jedynym niezawodnym sposobem.) diff --git a/reports/.gitkeep b/reports/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/templates/50unattended-upgrades.j2 b/templates/50unattended-upgrades.j2 new file mode 100644 index 0000000..68add00 --- /dev/null +++ b/templates/50unattended-upgrades.j2 @@ -0,0 +1,13 @@ +// Zarządzane przez Ansible (playbook_security_baseline.yml) — nie edytuj ręcznie. +// Instaluje wyłącznie aktualizacje bezpieczeństwa, bez automatycznego restartu: +// decyzję o reboot podejmuje operator na podstawie playbook_fleet_report.yml. + +Unattended-Upgrade::Origins-Pattern { + "origin=Debian,codename=${distro_codename},label=Debian-Security"; + "origin=Debian,codename=${distro_codename}-security,label=Debian-Security"; + "origin=Ubuntu,archive=${distro_codename}-security"; +}; + +Unattended-Upgrade::Remove-Unused-Dependencies "true"; +Unattended-Upgrade::Automatic-Reboot "false"; +Unattended-Upgrade::Automatic-Reboot-WithUsers "false"; diff --git a/templates/fleet_report.j2 b/templates/fleet_report.j2 new file mode 100644 index 0000000..7ef1bba --- /dev/null +++ b/templates/fleet_report.j2 @@ -0,0 +1,33 @@ +Raport stanu floty — {{ ansible_date_time.iso8601 }} +{{ '=' * 60 }} + +{% set reported_hosts = hostvars | dict2items | selectattr('value.host_report', 'defined') | map(attribute='key') | sort %} +{% set reboot_hosts = [] %} +{% for h in reported_hosts %} +{% if hostvars[h].host_report.reboot_required %} +{% set _ = reboot_hosts.append(h) %} +{% endif %} +{% endfor %} + +Hosty ({{ reported_hosts | length }}): +{% for h in reported_hosts %} +{% set r = hostvars[h].host_report %} +- {{ h }} + OS: {{ r.os }} + Uptime: {{ r.uptime_days }} dni + Dysk / zajęty: {{ r.disk_root_percent }}% + Aktualizacje: {{ r.upgradable_packages }} oczekujących + Reboot wymagany: {{ 'TAK' if r.reboot_required else 'nie' }} +{% endfor %} + +{{ '=' * 60 }} +{% if reboot_hosts | length > 0 %} +UWAGA — hosty wymagające ręcznego restartu ({{ reboot_hosts | length }}): +{% for h in reboot_hosts %} + - {{ h }} +{% endfor %} +Reboot NIE jest wykonywany automatycznie — zrestartuj te hosty ręcznie, +gdy będzie to bezpieczne (patrz grupy Type-HOST / Type-VM / Type-CT). +{% else %} +Żaden host nie wymaga restartu. +{% endif %}