Add reusable Debian and Ubuntu hardening playbooks
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user