All sourced from 2026-04-17 work sessions: - fail2ban-nginx-bad-request-jail: enable stock jail (just needs wiring) - fail2ban-apache-bad-request-jail: custom filter from scratch, no stock equivalent - ssh-hardening-ansible-fleet: drop-in approach with Fedora/Ubuntu edge cases - watchtower-smtp-localhost-relay: credential-free localhost postfix relay pattern Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.7 KiB
title, domain, category, tags, status, created, updated
| title | domain | category | tags | status | created | updated | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| SSH Hardening Fleet-Wide with Ansible | selfhosting | security |
|
published | 2026-04-17 | 2026-04-17 |
SSH Hardening Fleet-Wide with Ansible
Overview
Default SSH daemon settings on both Ubuntu and Fedora/RHEL are permissive. A drop-in configuration file (/etc/ssh/sshd_config.d/99-hardening.conf) lets you tighten settings without touching the distro-managed base config — and Ansible can deploy it atomically across every fleet host with a single playbook run.
Settings to Change
| Setting | Default | Hardened | Reason |
|---|---|---|---|
PermitRootLogin |
yes |
without-password |
Prevent password-based root login; key auth still works for Ansible |
X11Forwarding |
yes |
no |
Nothing in a typical homelab fleet uses X11 tunneling |
AllowTcpForwarding |
yes |
no |
Eliminates a tunneling vector if a service account is compromised |
MaxAuthTries |
6 |
3 |
Cuts per-connection brute-force attempts in half |
LoginGraceTime |
120 |
30 |
Reduces the window for slow-connect attacks |
The Drop-in Approach
Rather than editing /etc/ssh/sshd_config directly (which may be managed by the distro or overwritten on upgrades), place overrides in /etc/ssh/sshd_config.d/99-hardening.conf. The Include /etc/ssh/sshd_config.d/*.conf directive in the base config loads these in alphabetical order, and first match wins — so 99- ensures your overrides come last and take precedence.
Fedora/RHEL gotcha: Fedora ships
/etc/ssh/sshd_config.d/50-redhat.confwhich setsX11Forwarding yes. Because first-match-wins applies,50-redhat.confloads before99-hardening.confand wins. You must patch50-redhat.confin-place before deploying your drop-in, or the X11Forwarding setting will be silently ignored.
Ansible Playbook
- name: Harden SSH daemon fleet-wide
hosts: all:!raspbian
become: true
gather_facts: true
tasks:
- 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 Include directive is present in sshd_config
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
line: "Include /etc/ssh/sshd_config.d/*.conf"
insertbefore: BOF
state: present
# Fedora only: neutralize 50-redhat.conf's X11Forwarding yes
# (first-match-wins means it would override our 99- drop-in)
- name: Comment out X11Forwarding in 50-redhat.conf (Fedora)
ansible.builtin.replace:
path: /etc/ssh/sshd_config.d/50-redhat.conf
regexp: '^(X11Forwarding yes)'
replace: '# \1 # disabled by ansible hardening'
when: ansible_os_family == "RedHat"
ignore_errors: true
- name: Deploy SSH hardening drop-in
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/99-hardening.conf
content: |
# Managed by Ansible — do not edit manually
PermitRootLogin without-password
X11Forwarding no
AllowTcpForwarding no
MaxAuthTries 3
LoginGraceTime 30
owner: root
group: root
mode: '0644'
notify: Reload sshd
- name: Verify effective SSH settings
ansible.builtin.command:
cmd: sshd -T
register: sshd_effective
changed_when: false
- name: Assert hardened settings are active
ansible.builtin.assert:
that:
- "'permitrootlogin without-password' in sshd_effective.stdout"
- "'x11forwarding no' in sshd_effective.stdout"
- "'allowtcpforwarding no' in sshd_effective.stdout"
- "'maxauthtries 3' in sshd_effective.stdout"
- "'logingracetime 30' in sshd_effective.stdout"
fail_msg: "One or more SSH hardening settings not effective — check for conflicting config"
when: not ansible_check_mode
handlers:
- name: Reload sshd
ansible.builtin.service:
# Ubuntu/Debian: 'ssh' | Fedora/RHEL: 'sshd'
name: "{{ 'ssh' if ansible_os_family == 'Debian' else 'sshd' }}"
state: reloaded
Edge Cases
Ubuntu vs Fedora service name: The SSH daemon is ssh on Debian/Ubuntu and sshd on Fedora/RHEL. The handler uses ansible_os_family to pick the right name automatically.
Missing Include directive: Some minimal installs don't have Include /etc/ssh/sshd_config.d/*.conf in their base config. The lineinfile task adds it if absent. Without this, the drop-in directory exists but is never loaded.
Fedora's 50-redhat.conf: Sets X11Forwarding yes with first-match priority. The playbook patches it before deploying the drop-in.
sshd -T in check mode: sshd -T reads the current running config, not the pending changes. The assert task is guarded with when: not ansible_check_mode to prevent false failures during dry runs.
PermitRootLogin on hosts that already had it set: Some hosts (e.g., those managed by another tool) may already have PermitRootLogin without-password set elsewhere. The drop-in still applies cleanly — it just becomes a no-op for that setting.
Verify Manually
# Check effective settings on any host
ssh root@<host> "sshd -T | grep -E 'permitrootlogin|x11forwarding|allowtcpforwarding|maxauthtries|logingracetime'"
# Expected:
# permitrootlogin without-password
# x11forwarding no
# allowtcpforwarding no
# maxauthtries 3
# logingracetime 30