Secure Raspberry Pi SSH with Fail2ban

{"time":1776378046116,"version":"2.28.2","blocks":[{"type":"header","data":{"text":"1. Update the System","level":2}},{"type":"paragraph","data":{"text":"Before installing any new software, bring the system fully up to date. This ensures that Fail2Ban installs against the latest available versions of its dependencies and that any recently patched security vulnerabilities in the operating system are closed before the Pi is exposed to the internet."}},{"type":"code","data":{"code":"sudo apt update && sudo apt upgrade -y"}},{"type":"paragraph","data":{"text":"After a significant upgrade — particularly one that updates the kernel or system libraries — a reboot is recommended to ensure all changes take effect cleanly:"}},{"type":"code","data":{"code":"sudo reboot"}},{"type":"header","data":{"text":"2. Install Fail2Ban","level":2}},{"type":"paragraph","data":{"text":"Fail2Ban is available directly from the Raspberry Pi OS package repository. The installation is straightforward and requires no additional dependencies beyond what is already present on a standard Pi OS installation:"}},{"type":"code","data":{"code":"sudo apt install fail2ban -y"}},{"type":"paragraph","data":{"text":"Fail2Ban is exceptionally well-suited to Raspberry Pi hardware. At idle, it consumes approximately 20–30 MB of RAM — a negligible footprint even on a 1 GB Pi model. It performs no continuous polling by default when configured with the systemd backend (covered in Step 4), which also keeps CPU usage minimal between events."}},{"type":"header","data":{"text":"3. Verify Fail2Ban Is Running","level":2}},{"type":"paragraph","data":{"text":"Confirm that Fail2Ban started successfully after installation:"}},{"type":"code","data":{"code":"sudo systemctl status fail2ban"}},{"type":"paragraph","data":{"text":"The output should include the following line, confirming the service is active and running:"}},{"type":"code","data":{"code":"Active: active (running)"}},{"type":"paragraph","data":{"text":"If the service is not running — which can occasionally happen if the installation encountered a configuration issue — enable and start it manually:"}},{"type":"code","data":{"code":"sudo systemctl enable --now fail2ban"}},{"type":"paragraph","data":{"text":"The --now flag both enables the service to start at boot and starts it immediately in the same command."}},{"type":"header","data":{"text":"4. Create Your Local Configuration File","level":2}},{"type":"paragraph","data":{"text":"Fail2Ban ships with a default configuration file at /etc/fail2ban/jail.conf. This file must never be edited directly. When Fail2Ban is updated via apt, the package manager will overwrite jail.conf with a fresh copy, silently discarding any changes you made to it. Fail2Ban’s override system solves this: any settings placed in jail.local take precedence over jail.conf and are never touched by package updates."}},{"type":"paragraph","data":{"text":"Rather than copying the entire default file (which is hundreds of lines long and includes many settings you will never need), create a minimal jail.local that contains only the settings you intend to control. This approach is easier to read, easier to audit, and easier to maintain:"}},{"type":"code","data":{"code":"sudo nano /etc/fail2ban/jail.local"}},{"type":"paragraph","data":{"text":"Paste the following minimal, Pi-optimized configuration:"}},{"type":"code","data":{"code":"[DEFAULT]\nbantime = 1h\nfindtime = 10m\nmaxretry = 5\nbackend = systemd\nignoreip = 127.0.0.1/8 ::1\n\n[sshd]\nenabled = true\nport = ssh\nlogpath = %(sshd_log)s"}},{"type":"paragraph","data":{"text":"Here is what each setting in this configuration does:"}},{"type":"list","data":{"style":"unordered","items":["bantime = 1h — An IP that triggers a ban will be blocked for one hour. After that period, the ban is lifted automatically. You can increase this value (e.g., 24h or -1 for permanent) for higher-security environments, but one hour is a reasonable default that balances protection against accidental self-lockout.","findtime = 10m — Fail2Ban counts failures within a rolling 10-minute window. If the failure count exceeds maxretry within this window, the IP is banned. Setting this too short misses distributed slow attacks; setting it too long increases the risk of banning legitimate users who mistype their password a few times.","maxretry = 5 — An IP must fail authentication five times within the findtime window before it is banned. Five attempts is generous enough to accommodate mistyped passwords while still catching automated brute-force tools, which typically attempt hundreds of logins per minute.","backend = systemd — Instructs Fail2Ban to read logs from the systemd journal rather than from flat log files. On modern Raspberry Pi OS (based on Debian Bookworm or Bullseye), most services log to the journal by default. The systemd backend is faster, more reliable, and eliminates the need for Fail2Ban to poll log files on disk — an important consideration for SD card longevity.","ignoreip = 127.0.0.1/8 ::1 — IP addresses in this list are never banned, regardless of how many failures they generate. The loopback addresses are included by default. You should add your WireGuard VPN subnet (e.g., 10.8.0.0/24) and any trusted static IPs here to prevent accidentally locking yourself out during testing or maintenance.","[sshd] — Enables the SSH jail, which monitors authentication failures reported by the SSH daemon and bans IPs that exceed the configured thresholds."]}},{"type":"header","data":{"text":"5. Restart Fail2Ban to Apply the Configuration","level":2}},{"type":"paragraph","data":{"text":"After saving jail.local, restart Fail2Ban to load the new configuration:"}},{"type":"code","data":{"code":"sudo systemctl restart fail2ban"}},{"type":"paragraph","data":{"text":"If the service fails to restart, run sudo journalctl -u fail2ban --since \"1 minute ago\" to view the error output and identify any syntax issues in jail.local."}},{"type":"header","data":{"text":"6. Confirm the SSH Jail Is Active","level":2}},{"type":"paragraph","data":{"text":"Verify that Fail2Ban has loaded the SSH jail correctly:"}},{"type":"code","data":{"code":"sudo fail2ban-client status"}},{"type":"paragraph","data":{"text":"The output should list sshd as an active jail:"}},{"type":"code","data":{"code":"Jail list: sshd"}},{"type":"paragraph","data":{"text":"To view detailed status for the SSH jail — including the current number of monitored log lines, failed attempts detected, and any IPs currently banned — run:"}},{"type":"code","data":{"code":"sudo fail2ban-client status sshd"}},{"type":"paragraph","data":{"text":"If the jail list is empty or sshd does not appear, check jail.local for a typo in the [sshd] section header or a missing enabled = true line, then restart Fail2Ban again."}},{"type":"header","data":{"text":"7. Reduce Resource Usage (Important for 1 GB Pi Models)","level":2}},{"type":"paragraph","data":{"text":"By default, Fail2Ban logs at the WARNING level to the systemd journal. On a Raspberry Pi — especially a model with 1 GB of RAM — writing verbose logs to disk also accelerates SD card wear over time. The following configuration reduces log verbosity and redirects output to a dedicated log file that can be managed with log rotation (covered in Step 8)."}},{"type":"paragraph","data":{"text":"Open the main Fail2Ban configuration file:"}},{"type":"code","data":{"code":"sudo nano /etc/fail2ban/fail2ban.conf"}},{"type":"paragraph","data":{"text":"Locate and set the following two values:"}},{"type":"code","data":{"code":"loglevel = INFO\nlogtarget = /var/log/fail2ban.log"}},{"type":"paragraph","data":{"text":"loglevel = INFO captures bans, unbans, and service start/stop events without the verbose debug output that DEBUG or TRACE levels would generate. logtarget redirects output to a flat file, which log rotation can then compress and cycle on a schedule, keeping total disk usage predictable and bounded."}},{"type":"paragraph","data":{"text":"Because the configuration uses backend = systemd, Fail2Ban reads events from the journal rather than polling log files. This eliminates the file-polling overhead that the default auto or polling backend would introduce, further reducing CPU and I/O activity."}},{"type":"header","data":{"text":"8. Configure Log Rotation to Protect the SD Card","level":2}},{"type":"paragraph","data":{"text":"Raspberry Pi OS runs from an SD card, which has a significantly lower write endurance than a traditional hard drive or SSD. Log files that grow without bound are one of the most common causes of premature SD card failure in long-running Pi deployments. Log rotation prevents this by compressing and cycling the Fail2Ban log file on a regular schedule, capping its total disk footprint."}},{"type":"paragraph","data":{"text":"Open or create the Fail2Ban log rotation configuration:"}},{"type":"code","data":{"code":"sudo nano /etc/logrotate.d/fail2ban"}},{"type":"paragraph","data":{"text":"Paste the following configuration:"}},{"type":"code","data":{"code":"/var/log/fail2ban.log {\n weekly\n rotate 4\n compress\n missingok\n notifempty\n}"}},{"type":"paragraph","data":{"text":"Here is what each directive does:"}},{"type":"list","data":{"style":"unordered","items":["weekly — The log file is rotated once per week.","rotate 4 — Four weeks of compressed log archives are retained before the oldest is deleted. This gives you one month of ban history available for review without unbounded disk growth.","compress — Rotated log files are compressed with gzip, typically reducing their size by 80–90%.","missingok — Log rotation will not generate an error if the log file does not exist yet. This prevents spurious logrotate error emails during the initial setup period before Fail2Ban has written its first log entry.","notifempty — If the log file is empty, it will not be rotated. This avoids creating unnecessary compressed archive files during quiet periods."]}},{"type":"header","data":{"text":"9. Test Fail2Ban (Safe Verification Procedure)","level":2}},{"type":"paragraph","data":{"text":"Before relying on Fail2Ban for production protection, verify that it correctly detects failures and applies bans. Perform this test from a second machine — never from the Pi itself, as loopback connections are excluded by the ignoreip setting and will not trigger a ban."}},{"type":"paragraph","data":{"text":"Important: Before beginning, add your current machine’s IP address to the ignoreip list in jail.local, or ensure you have an alternative way to access the Pi (such as a serial console or another network path) in case you accidentally ban yourself during testing."}},{"type":"paragraph","data":{"text":"From a second machine, deliberately fail SSH authentication five times by using an incorrect username or password:"}},{"type":"code","data":{"code":"ssh wronguser@pi-ip"}},{"type":"paragraph","data":{"text":"After five failures within the 10-minute findtime window, your source IP will be banned. Subsequent SSH connection attempts from that IP will time out or be refused immediately rather than presenting an authentication prompt — this change in behavior confirms the ban is in effect."}},{"type":"paragraph","data":{"text":"To confirm the ban was applied, check the jail status from the Pi:"}},{"type":"code","data":{"code":"sudo fail2ban-client status sshd"}},{"type":"paragraph","data":{"text":"Your test IP should appear in the “Banned IP list” section of the output. Once confirmed, unban your test IP immediately:"}},{"type":"code","data":{"code":"sudo fail2ban-client set sshd unbanip YOUR_IP"}},{"type":"header","data":{"text":"10. Optional Hardening (Strongly Recommended)","level":2}},{"type":"paragraph","data":{"text":"Fail2Ban significantly reduces the risk of a successful brute-force attack, but it still allows an attacker to attempt up to five logins before being blocked. The most effective complement to Fail2Ban is disabling password-based SSH authentication entirely, so that only clients with a valid private key can authenticate — regardless of how many connection attempts they make."}},{"type":"header","data":{"text":"Disable SSH Password Authentication","level":3}},{"type":"paragraph","data":{"text":"Before making these changes, confirm that you have a working SSH key pair set up and can authenticate with your key successfully. Disabling password authentication while you are locked out of key-based access will prevent you from logging into the Pi remotely."}},{"type":"paragraph","data":{"text":"Open the SSH daemon configuration file:"}},{"type":"code","data":{"code":"sudo nano /etc/ssh/sshd_config"}},{"type":"paragraph","data":{"text":"Locate and set the following two directives, adding them if they do not already exist:"}},{"type":"code","data":{"code":"PasswordAuthentication no\nPermitRootLogin no"}},{"type":"paragraph","data":{"text":"PasswordAuthentication no disables all password-based login attempts. SSH will only accept public key authentication. Automated brute-force tools that attempt password guessing will fail immediately at the authentication handshake — before Fail2Ban even needs to intervene. PermitRootLogin no prevents direct root login over SSH, forcing all privileged operations to go through a non-root account and sudo."}},{"type":"paragraph","data":{"text":"Apply the changes by restarting the SSH service:"}},{"type":"code","data":{"code":"sudo systemctl restart ssh"}},{"type":"paragraph","data":{"text":"After restarting, open a new SSH session from your client machine to confirm that key-based authentication still works correctly before closing your existing session. Do not close your current session until you have verified that the new session connects successfully."}},{"type":"header","data":{"text":"Summary: What This Configuration Achieves","level":2}},{"type":"paragraph","data":{"text":"Following this guide produces a Fail2Ban deployment that is specifically optimized for Raspberry Pi hardware and long-term unattended operation:"}},{"type":"list","data":{"style":"unordered","items":["✅ Low RAM usage — approximately 20–30 MB at idle, leaving the majority of available memory for other services.","✅ SD card-friendly logging — log rotation limits total write volume and disk footprint, extending SD card lifespan.","✅ SSH protected against brute force — the SSH jail detects and bans repeated authentication failures automatically.","✅ systemd backend — event-driven log reading eliminates continuous file polling, reducing CPU and I/O overhead compared to the default polling backend.","✅ Upgrade-proof configuration — all custom settings are isolated in jail.local, which is never overwritten by package updates."]}}]}