Outcome: apply your SSHD/PAM/fail2ban defaults across hosts safely, with check/diff runs and a skeleton for tests.
1) Layout
baseline/
├─ inventory.yaml
├─ site.yaml
└─ roles/
└─ hardening/
├─ tasks/main.yaml
├─ templates/sshd_config.j2
└─ files/sudoers_logging
2) Inventory
# inventory.yaml
all:
hosts:
web1.example.com:
db1.example.com:
vars:
ansible_user: admin
3) Playbook
# site.yaml
- hosts: all
become: true
roles:
- hardening
4) Role tasks (extract)
# roles/hardening/tasks/main.yaml
- name: Install packages
package:
name: [fail2ban, auditd]
state: present
- name: SSHD config
template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: '0644'
notify: restart ssh
- name: Sudo logging
copy:
src: sudoers_logging
dest: /etc/sudoers.d/logging
owner: root
group: root
mode: '0440'
notify: validate sudoers
- name: Ensure journald persistent
file:
path: /var/log/journal
state: directory
owner: root
group: systemd-journal
mode: '2755'
handlers:
- name: restart ssh
service: { name: ssh, state: reloaded }
- name: validate sudoers
command: visudo -c
5) Run modes
# Dry run
ansible-playbook -i inventory.yaml site.yaml --check
# See changes
ansible-playbook -i inventory.yaml site.yaml --diff
# Apply for real
ansible-playbook -i inventory.yaml site.yaml
6) Minimal test (Molecule optional)
At least validate SSH is reachable and password auth is disabled post-run. Add Molecule later if you want full CI.