From deec75de0fa850d7c2248cd1fe8157eb0cc70466 Mon Sep 17 00:00:00 2001 From: sjat Date: Sun, 14 Jun 2026 16:42:56 +0200 Subject: [PATCH] feat(base): ssh hardening + fail2ban (hardening concern, ADR-002) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../production/group_vars/all/vars.yml | 3 ++- roles/base/defaults/main.yml | 8 ++++++ roles/base/handlers/main.yml | 11 ++++++++ roles/base/tasks/fail2ban.yml | 21 +++++++++++++++ roles/base/tasks/main.yml | 8 ++++++ roles/base/tasks/ssh.yml | 26 +++++++++++++++++++ roles/base/templates/fail2ban_sshd.local.j2 | 6 +++++ roles/base/templates/sshd_hardening.conf.j2 | 5 ++++ 8 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 roles/base/tasks/fail2ban.yml create mode 100644 roles/base/tasks/ssh.yml create mode 100644 roles/base/templates/fail2ban_sshd.local.j2 create mode 100644 roles/base/templates/sshd_hardening.conf.j2 diff --git a/inventories/production/group_vars/all/vars.yml b/inventories/production/group_vars/all/vars.yml index 50b4f86..f4efa1d 100644 --- a/inventories/production/group_vars/all/vars.yml +++ b/inventories/production/group_vars/all/vars.yml @@ -8,7 +8,8 @@ ansible_python_interpreter: /usr/bin/python3 # SSH authorised keys — add one entry per person # Format: "ssh-ed25519 AAAA... user@host" -base__ssh_authorised_keys: [] +base__ssh_authorised_keys: + - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKSx1TFLJ9H8vCe5ZJSu7MYmAiH0/OC8evloQjGR0Bqw claude@ubongo" # Timezone base__timezone: Europe/Copenhagen diff --git a/roles/base/defaults/main.yml b/roles/base/defaults/main.yml index cea3010..19832e3 100644 --- a/roles/base/defaults/main.yml +++ b/roles/base/defaults/main.yml @@ -11,3 +11,11 @@ base__firewall_rollback_timeout: 45 # seconds before the auto-revert fires on a base__firewall_confirm_timeout: 20 # seconds to re-establish a fresh connection post-apply base__firewall_dropin_dir: /etc/nftables.d base__firewall_apply: true # set false to render+validate without applying (CI/Molecule) + +# SSH hardening + fail2ban (ADR-002) — `hardening` concern. +base__ssh_password_authentication: "no" +base__ssh_permit_root_login: "no" +base__fail2ban_maxretry: 5 +base__fail2ban_bantime: 1h +base__fail2ban_findtime: 10m +# base__ssh_authorised_keys lives in group_vars/all/vars.yml (per-person control keys). diff --git a/roles/base/handlers/main.yml b/roles/base/handlers/main.yml index ed97d53..0b94520 100644 --- a/roles/base/handlers/main.yml +++ b/roles/base/handlers/main.yml @@ -1 +1,12 @@ --- +- name: Reload sshd + listen: reload sshd + ansible.builtin.service: + name: ssh + state: reloaded + +- name: Restart fail2ban + listen: restart fail2ban + ansible.builtin.service: + name: fail2ban + state: restarted diff --git a/roles/base/tasks/fail2ban.yml b/roles/base/tasks/fail2ban.yml new file mode 100644 index 0000000..8a39f23 --- /dev/null +++ b/roles/base/tasks/fail2ban.yml @@ -0,0 +1,21 @@ +--- +- name: Install fail2ban + ansible.builtin.apt: + name: fail2ban + state: present + update_cache: true + +- name: Configure the sshd jail + ansible.builtin.template: + src: fail2ban_sshd.local.j2 + dest: /etc/fail2ban/jail.d/sshd.local + owner: root + group: root + mode: "0644" + notify: restart fail2ban + +- name: Enable and start fail2ban + ansible.builtin.service: + name: fail2ban + enabled: true + state: started diff --git a/roles/base/tasks/main.yml b/roles/base/tasks/main.yml index 1d77af9..15b8e12 100644 --- a/roles/base/tasks/main.yml +++ b/roles/base/tasks/main.yml @@ -2,3 +2,11 @@ - name: Configure host firewall (nftables) ansible.builtin.include_tasks: firewall.yml tags: [firewall] + +- name: SSH hardening + ansible.builtin.include_tasks: ssh.yml + tags: [hardening] + +- name: Fail2ban intrusion deterrence + ansible.builtin.include_tasks: fail2ban.yml + tags: [hardening] diff --git a/roles/base/tasks/ssh.yml b/roles/base/tasks/ssh.yml new file mode 100644 index 0000000..8ef7eba --- /dev/null +++ b/roles/base/tasks/ssh.yml @@ -0,0 +1,26 @@ +--- +- name: Ensure openssh-server is installed + ansible.builtin.apt: + name: openssh-server + state: present + update_cache: true + +- name: Render hardened sshd drop-in + ansible.builtin.template: + src: sshd_hardening.conf.j2 + dest: /etc/ssh/sshd_config.d/10-boma.conf + owner: root + group: root + mode: "0644" + notify: reload sshd + +- name: Validate the full sshd config (drop-in included) + ansible.builtin.command: sshd -t + changed_when: false + +- name: Authorise control SSH keys for the ansible user + ansible.posix.authorized_key: + user: "{{ ansible_user | default('ansible') }}" + key: "{{ base__ssh_authorised_keys | join('\n') }}" + exclusive: true + when: base__ssh_authorised_keys | length > 0 diff --git a/roles/base/templates/fail2ban_sshd.local.j2 b/roles/base/templates/fail2ban_sshd.local.j2 new file mode 100644 index 0000000..fd297f4 --- /dev/null +++ b/roles/base/templates/fail2ban_sshd.local.j2 @@ -0,0 +1,6 @@ +# Managed by Ansible (base role, ADR-002). +[sshd] +enabled = true +maxretry = {{ base__fail2ban_maxretry }} +bantime = {{ base__fail2ban_bantime }} +findtime = {{ base__fail2ban_findtime }} diff --git a/roles/base/templates/sshd_hardening.conf.j2 b/roles/base/templates/sshd_hardening.conf.j2 new file mode 100644 index 0000000..d0a845e --- /dev/null +++ b/roles/base/templates/sshd_hardening.conf.j2 @@ -0,0 +1,5 @@ +# Managed by Ansible (base role, ADR-002). Do not edit on the host. +PasswordAuthentication {{ base__ssh_password_authentication }} +PermitRootLogin {{ base__ssh_permit_root_login }} +PubkeyAuthentication yes +KbdInteractiveAuthentication no