006103760f
Review fixes: - Availability plays: "ping_result is succeeded" is true for unreachable hosts, so offline hosts were added to reachable_hosts and failed the next play. Test reachability explicitly (7 playbooks). - bootstrap_lxc: vim-enhanced and mtr-tiny package names, enable CRB next to EPEL, add the root key without replacing authorized_keys, validate sshd_config edits and restart sshd only on change. - backup_docker: the command module does not expand $(...); fix rsync filter order and add --exclude=*; restart only the containers that were stopped, even when rsync fails; read the storage address from the inventory; keep semaphore and semaphore-db running. - new_lxc: community.general.proxmox no longer exists, use community.proxmox; node is mini-pc; valid hostname; password or token auth; let the module pick the VMID; longer timeout. - proxmox-backup: surface the vzdump exit code, let vzdump prune with keep-last, drop the rename/prune steps that globbed *.zstd. - time-region: chrony regex no longer comments out its own server line; /etc/localtime as a symlink; reuse facts across plays. - update_pihole_blocklists: FQCN, become: true, changed_when. - ansible.cfg gathering = smart; update_system only pings ubuntu/rocky. Telegram alerts: - health_check: filesystems above disk_warn_pct (default 80), excluding efivarfs, and offline hosts. - service_check: services not running. - One message per run from localhost, credentials from Semaphore (TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID), no_log on the send task, prints the alert text when Telegram is not configured. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEizHawueRXUcxQELRZ8kq
210 lines
6.5 KiB
YAML
210 lines
6.5 KiB
YAML
---
|
|
# Hardens sshd on every reachable Ubuntu/Rocky guest and turns on a fail2ban
|
|
# sshd jail. Settings go into /etc/ssh/sshd_config.d/00-ansible.conf, which
|
|
# sorts first and therefore wins over anything in sshd_config or the
|
|
# cloud-init drop-in (sshd keeps the first value it sees for each option).
|
|
#
|
|
# Lockout guard: password authentication is only disabled on hosts where
|
|
# root already has at least one authorized key.
|
|
#
|
|
# Extra vars (optional):
|
|
# ssh_disable_password_auth - default true
|
|
# ssh_max_auth_tries - default 4
|
|
|
|
- name: Check Host Availability
|
|
hosts: ubuntu:rocky
|
|
gather_facts: false
|
|
ignore_unreachable: true
|
|
|
|
tasks:
|
|
- name: Ping hosts to check availability
|
|
ansible.builtin.ping:
|
|
register: ping_result
|
|
ignore_errors: true
|
|
ignore_unreachable: true
|
|
|
|
- name: Add reachable hosts to group
|
|
ansible.builtin.group_by:
|
|
key: reachable_hosts
|
|
when: ping_result is reachable and ping_result is succeeded
|
|
|
|
- name: Report offline hosts
|
|
ansible.builtin.debug:
|
|
msg: "{{ inventory_hostname }} - OFFLINE"
|
|
when: ping_result is unreachable or ping_result is failed
|
|
delegate_to: localhost
|
|
|
|
- name: SSH Hardening - Install fail2ban on Ubuntu
|
|
hosts: ubuntu:&reachable_hosts
|
|
become: true
|
|
gather_facts: false
|
|
|
|
tasks:
|
|
- name: Install fail2ban and nftables via apt
|
|
ansible.builtin.apt:
|
|
name:
|
|
- fail2ban
|
|
- nftables
|
|
state: present
|
|
update_cache: true
|
|
cache_valid_time: 3600
|
|
|
|
- name: SSH Hardening - Install fail2ban on Rocky Linux
|
|
hosts: rocky:&reachable_hosts
|
|
become: true
|
|
gather_facts: false
|
|
|
|
tasks:
|
|
- name: Ensure EPEL repo is enabled
|
|
ansible.builtin.dnf:
|
|
name: epel-release
|
|
state: present
|
|
|
|
- name: Install fail2ban and nftables via dnf
|
|
ansible.builtin.dnf:
|
|
name:
|
|
- fail2ban
|
|
- nftables
|
|
state: present
|
|
|
|
- name: SSH Hardening - Configure sshd and fail2ban
|
|
hosts: reachable_hosts
|
|
become: true
|
|
gather_facts: false
|
|
|
|
vars:
|
|
ssh_disable_password_auth: true
|
|
ssh_max_auth_tries: 4
|
|
sshd_dropin_path: /etc/ssh/sshd_config.d/00-ansible.conf
|
|
|
|
tasks:
|
|
- name: Count public keys in root's authorized_keys
|
|
ansible.builtin.command: grep -cE '^(ssh-|ecdsa-|sk-)' /root/.ssh/authorized_keys
|
|
register: key_count
|
|
failed_when: false
|
|
changed_when: false
|
|
|
|
- name: Decide whether password authentication can be disabled
|
|
ansible.builtin.set_fact:
|
|
has_keys: "{{ key_count.rc == 0 and (key_count.stdout | int) > 0 }}"
|
|
|
|
- name: Warn when root has no authorized keys
|
|
ansible.builtin.debug:
|
|
msg: "WARNING: no authorized keys for root on {{ inventory_hostname }} - password authentication stays enabled"
|
|
when: not has_keys
|
|
|
|
- name: Ensure sshd_config.d directory exists
|
|
ansible.builtin.file:
|
|
path: /etc/ssh/sshd_config.d
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: '0755'
|
|
|
|
- name: Ensure sshd_config includes the drop-in directory
|
|
ansible.builtin.lineinfile:
|
|
path: /etc/ssh/sshd_config
|
|
regexp: '^Include\s+/etc/ssh/sshd_config\.d/\*\.conf'
|
|
line: 'Include /etc/ssh/sshd_config.d/*.conf'
|
|
insertbefore: BOF
|
|
backup: true
|
|
register: sshd_include
|
|
|
|
- name: Write sshd hardening drop-in
|
|
ansible.builtin.copy:
|
|
dest: "{{ sshd_dropin_path }}"
|
|
content: |
|
|
# Managed by Ansible (playbooks/ssh_harden.yml) - do not edit by hand
|
|
PermitRootLogin prohibit-password
|
|
PubkeyAuthentication yes
|
|
PasswordAuthentication {{ 'no' if (has_keys and ssh_disable_password_auth | bool) else 'yes' }}
|
|
KbdInteractiveAuthentication no
|
|
MaxAuthTries {{ ssh_max_auth_tries }}
|
|
X11Forwarding no
|
|
owner: root
|
|
group: root
|
|
mode: '0600'
|
|
validate: /usr/sbin/sshd -t -f %s
|
|
register: sshd_dropin
|
|
|
|
- name: Record whether sshd needs a restart
|
|
ansible.builtin.set_fact:
|
|
sshd_changed: "{{ sshd_include is changed or sshd_dropin is changed }}"
|
|
|
|
- name: Configure fail2ban sshd jail
|
|
ansible.builtin.copy:
|
|
dest: /etc/fail2ban/jail.d/sshd.local
|
|
content: |
|
|
# Managed by Ansible (playbooks/ssh_harden.yml)
|
|
[sshd]
|
|
enabled = true
|
|
backend = systemd
|
|
banaction = nftables-multiport
|
|
maxretry = 5
|
|
findtime = 10m
|
|
bantime = 1h
|
|
owner: root
|
|
group: root
|
|
mode: '0644'
|
|
register: f2b_jail
|
|
|
|
- name: Ensure fail2ban is enabled and running
|
|
ansible.builtin.systemd:
|
|
name: fail2ban
|
|
enabled: true
|
|
state: "{{ 'restarted' if f2b_jail is changed else 'started' }}"
|
|
|
|
- name: SSH Hardening - Restart SSH on Ubuntu
|
|
hosts: ubuntu:&reachable_hosts
|
|
become: true
|
|
gather_facts: false
|
|
|
|
tasks:
|
|
- name: Restart ssh service
|
|
ansible.builtin.service:
|
|
name: ssh
|
|
state: restarted
|
|
when: sshd_changed | bool
|
|
|
|
- name: SSH Hardening - Restart SSH on Rocky Linux
|
|
hosts: rocky:&reachable_hosts
|
|
become: true
|
|
gather_facts: false
|
|
|
|
tasks:
|
|
- name: Restart sshd service
|
|
ansible.builtin.service:
|
|
name: sshd
|
|
state: restarted
|
|
when: sshd_changed | bool
|
|
|
|
- name: SSH Hardening - Verify access
|
|
hosts: reachable_hosts
|
|
become: true
|
|
gather_facts: false
|
|
|
|
tasks:
|
|
- name: Drop the cached SSH connection so the next task performs a fresh login
|
|
ansible.builtin.meta: reset_connection
|
|
|
|
- name: Confirm key-based login still works
|
|
ansible.builtin.ping:
|
|
|
|
- name: Read fail2ban sshd jail status
|
|
ansible.builtin.command: fail2ban-client status sshd
|
|
register: f2b_status
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Summary
|
|
ansible.builtin.debug:
|
|
msg: |
|
|
======================================================
|
|
HOST: {{ inventory_hostname }} ({{ ansible_host }})
|
|
======================================================
|
|
PASSWORD AUTH: {{ 'disabled' if (has_keys and ssh_disable_password_auth | default(true) | bool) else 'ENABLED' }}
|
|
ROOT KEYS: {{ key_count.stdout if key_count.rc == 0 else 0 }}
|
|
SSHD: {{ 'restarted' if sshd_changed | bool else 'unchanged' }}
|
|
FAIL2BAN: {{ 'sshd jail active' if f2b_status.rc == 0 else 'WARNING: jail not reporting (' ~ (f2b_status.stderr | default('') | trim) ~ ')' }}
|
|
======================================================
|