Add reusable Debian and Ubuntu hardening playbooks

This commit is contained in:
2026-09-29 00:07:10 +04:00
commit 3d08c8f1ff
17 changed files with 663 additions and 0 deletions
+222
View File
@@ -0,0 +1,222 @@
---
- name: Validate platform and migration settings
ansible.builtin.assert:
that:
- ansible_os_family == "Debian"
- ansible_service_mgr == "systemd"
- ssh_port | int > 0
- ssh_port | int <= 65535
- ssh_legacy_port | int > 0
- ssh_legacy_port | int <= 65535
- ansible_port | default(22) | int == ssh_port | int or (ssh_keep_legacy_port | bool and ansible_port | default(22) | int == ssh_legacy_port | int)
fail_msg: >-
The inventory connection port must be either the new SSH port or the
legacy port while the legacy listener is intentionally retained.
- name: Install hardening packages
ansible.builtin.apt:
name:
- fail2ban
- ufw
- unattended-upgrades
state: present
update_cache: true
cache_valid_time: 3600
- name: Enable UFW IPv6 support
ansible.builtin.lineinfile:
path: /etc/default/ufw
regexp: '^IPV6='
line: 'IPV6=yes'
backup: true
- name: Set UFW default inbound policy
community.general.ufw:
direction: incoming
default: "{{ ufw_default_incoming }}"
- name: Set UFW default outbound policy
community.general.ufw:
direction: outgoing
default: "{{ ufw_default_outgoing }}"
- name: Set UFW default routed policy
community.general.ufw:
direction: routed
default: "{{ ufw_default_routed }}"
- name: Set UFW logging level
community.general.ufw:
logging: "{{ ufw_logging }}"
- name: Build the SSH ports that must remain allowed during this run
ansible.builtin.set_fact:
hardening_ssh_ports: >-
{{ ([ssh_port | string] + ([ssh_legacy_port | string] if ssh_keep_legacy_port | bool else [])) | unique }}
- name: Allow configured SSH listener ports before enabling UFW
community.general.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop: "{{ hardening_ssh_ports }}"
- name: Allow additional inventory-declared service ports
community.general.ufw:
rule: allow
port: "{{ item.port | string }}"
proto: "{{ item.proto | default('tcp') }}"
from_ip: "{{ item.from_ip | default('any') }}"
comment: "{{ item.comment | default(omit) }}"
loop: "{{ ufw_allowed_rules }}"
loop_control:
label: "{{ item.proto | default('tcp') }}/{{ item.port }}"
- name: Enable UFW after the SSH and service allow rules are installed
community.general.ufw:
state: enabled
when: ufw_enabled | bool
- name: Remove the legacy SSH firewall rule after port cutover
community.general.ufw:
rule: allow
port: "{{ ssh_legacy_port | string }}"
proto: tcp
delete: true
when:
- not (ssh_keep_legacy_port | bool)
- ssh_legacy_port | string != ssh_port | string
- name: Remove explicitly retired inventory-declared UFW rules
community.general.ufw:
rule: allow
port: "{{ item.port | string }}"
proto: "{{ item.proto | default('tcp') }}"
from_ip: "{{ item.from_ip | default('any') }}"
delete: true
loop: "{{ ufw_remove_rules }}"
loop_control:
label: "{{ item.proto | default('tcp') }}/{{ item.port }}"
- name: Install Fail2ban SSH jail configuration
ansible.builtin.copy:
dest: /etc/fail2ban/jail.d/10-ansible-sshd.local
owner: root
group: root
mode: "0644"
content: |
[DEFAULT]
banaction = ufw
ignoreip = 127.0.0.1/8 ::1 {{ fail2ban_ignore_ips | join(' ') }}
[sshd]
enabled = true
backend = systemd
port = {{ hardening_ssh_ports | join(',') }}
maxretry = {{ fail2ban_ssh_maxretry }}
findtime = {{ fail2ban_ssh_findtime }}
bantime = {{ fail2ban_ssh_bantime }}
notify: Restart fail2ban
- name: Enable and start Fail2ban
ansible.builtin.service:
name: fail2ban
enabled: true
state: started
- name: Detect active systemd SSH socket
ansible.builtin.command: systemctl is-active --quiet ssh.socket
register: hardening_ssh_socket
changed_when: false
failed_when: false
- name: Detect active systemd SSH service
ansible.builtin.command: systemctl is-active --quiet ssh.service
register: hardening_ssh_service
changed_when: false
failed_when: false
- name: Ensure the SSH socket override directory exists
ansible.builtin.file:
path: /etc/systemd/system/ssh.socket.d
state: directory
owner: root
group: root
mode: "0755"
when: hardening_ssh_socket.rc == 0
- name: Set SSH socket listener ports when socket activation is active
ansible.builtin.copy:
dest: /etc/systemd/system/ssh.socket.d/zz-ansible-ports.conf
owner: root
group: root
mode: "0644"
content: |
[Socket]
ListenStream=
{% for port in hardening_ssh_ports %}
ListenStream={{ port }}
{% endfor %}
when: hardening_ssh_socket.rc == 0
notify: Apply SSH changes
- name: Install SSH hardening drop-in
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/00-ansible-hardening.conf
owner: root
group: root
mode: "0644"
content: |
Port {{ ssh_port }}
{% if ssh_keep_legacy_port | bool and ssh_legacy_port | int != ssh_port | int %}Port {{ ssh_legacy_port }}
{% endif %}PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication {{ 'yes' if ssh_password_authentication | bool else 'no' }}
KbdInteractiveAuthentication {{ 'yes' if ssh_kbd_interactive_authentication | bool else 'no' }}
PermitEmptyPasswords no
MaxAuthTries {{ ssh_max_auth_tries }}
LoginGraceTime {{ ssh_login_grace_time }}
X11Forwarding no
validate: /usr/sbin/sshd -t -f %s
backup: true
notify: Apply SSH changes
- name: Apply pending service configuration changes
ansible.builtin.meta: flush_handlers
- name: Read effective SSH configuration
ansible.builtin.command: /usr/sbin/sshd -T
register: effective_sshd
changed_when: false
- name: Verify effective SSH hardening and listener ports
ansible.builtin.assert:
that:
- "'permitrootlogin no' in effective_sshd.stdout_lines"
- "'passwordauthentication ' + ('yes' if ssh_password_authentication | bool else 'no') in effective_sshd.stdout_lines"
- "'pubkeyauthentication yes' in effective_sshd.stdout_lines"
- "'port ' + (ssh_port | string) in effective_sshd.stdout_lines"
- ssh_port | int == ssh_legacy_port | int or (ssh_keep_legacy_port | bool) == (('port ' + (ssh_legacy_port | string)) in effective_sshd.stdout_lines)
- name: Wait for the new SSH listener locally on the host
ansible.builtin.wait_for:
host: 127.0.0.1
port: "{{ ssh_port }}"
state: started
timeout: 30
- name: Confirm the legacy SSH listener is closed after cutover
ansible.builtin.wait_for:
host: 127.0.0.1
port: "{{ ssh_legacy_port }}"
state: stopped
timeout: 10
when:
- not (ssh_keep_legacy_port | bool)
- ssh_legacy_port | int != ssh_port | int
- name: Confirm Fail2ban has the SSH jail active
ansible.builtin.command: fail2ban-client status sshd
register: fail2ban_sshd_status
changed_when: false
failed_when: fail2ban_sshd_status.rc != 0