Part
5
  |  
Production Engineering
  |  
Chapter
20

Security Hardening

Most engineers assume their Pi is safe because it's behind a NAT router — then discover that 'raspberry' is the most-scanned default password on the internet.
Reading Time
11
mins
BACK TO RASPBERRY PI MASTERCLASS

The most dangerous moment in a Pi's life is the moment you connect it to your network and walk away. I'm not talking about sophisticated nation-state attacks. I'm talking about automated bots that scan every IP range on the internet, find SSH services on port 22, and try default credentials. The default username on Raspberry Pi OS used to be pi. The default password used to be raspberry. That combination is in every credential-stuffing dictionary on the planet. Shodan — the search engine for internet-connected devices — indexes thousands of Raspberry Pis with open SSH ports every week. Most of them are one successful login attempt away from becoming cryptocurrency miners or botnet nodes.

"But my Pi is behind a NAT router," you say. That's true — and it's insufficient. NAT is not a firewall. It's an address-translation mechanism that happens to block inbound connections as a side effect. The moment you forward a port for remote access, set up a VPN incorrectly, or connect your Pi to a public network, that accidental protection evaporates. And even behind NAT, every other device on your local network can reach your Pi. If your laptop gets compromised, your Pi is one lateral hop away.

NAT is not a firewall. It's an address-translation mechanism that happens to block inbound connections as a side effect.

Security hardening for a Pi isn't paranoia. It's the minimum viable configuration for any device that has a network interface, runs an operating system, and stays powered on 24/7. The good news: it takes about fifteen minutes.

The Hardening Stack

Framework · The Hardening Stack · HS

SSH keys, fail2ban, ufw, unattended-upgrades — apply them in that order, every time, on every Pi. Each layer defends against a different class of attack, and together they bring a Pi from "trivially compromised" to "not worth the attacker's time."

The order matters. SSH keys eliminate password-based attacks — the largest category of automated intrusion attempts. Fail2ban catches anything that slips through by rate-limiting repeated failures. UFW closes every port you're not actively using. Unattended-upgrades patch known vulnerabilities automatically so you don't have to remember to run apt upgrade every week. Skip any layer and you leave a gap that automated tools will find.

Layer 1: SSH Key Authentication

Password authentication over SSH is the single largest attack surface on a default Pi installation. Replace it with key-based authentication and disable password login entirely.

On your workstation (not the Pi), generate a key pair if you don't have one:

ssh-keygen -t ed25519 -C "your-email@example.com"

Ed25519 is the modern choice — faster and more secure than RSA. Accept the default file location. Set a passphrase for the private key.

Copy the public key to your Pi:

ssh-copy-id pi@raspberrypi.local

Verify you can log in without a password:

ssh pi@raspberrypi.local
# Should log in without prompting for a password

Now disable password authentication on the Pi. Edit the SSH daemon config:

sudo nano /etc/ssh/sshd_config

Find and set these directives:

PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM no
PermitRootLogin no

Restart SSH:

sudo systemctl restart sshd
Don't lock yourself out

Test key-based login in a new terminal window BEFORE closing your current SSH session. If the key isn't set up correctly and you've already disabled password auth, you'll need physical access to the Pi (keyboard and monitor) to fix it.

From this point forward, anyone who doesn't possess your private key cannot log in via SSH. The vast majority of automated attacks — the bots spraying pi:raspberry across every IP range — are now irrelevant.

Key takeaway

Disabling password authentication and switching to SSH keys eliminates the single largest attack vector against a networked Pi. It takes five minutes and blocks 99% of automated intrusion attempts.

Layer 2: Fail2ban

SSH keys stop password guessing. Fail2ban stops everything else — repeated failed login attempts, port scanning probes, and brute-force attacks against any service. It works by monitoring log files and temporarily banning IPs that exhibit attack patterns.

sudo apt install -y fail2ban

Create a local configuration file (never edit the main config — it gets overwritten on updates):

sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 3
banaction = ufw

[sshd]
enabled = true
port    = ssh
filter  = sshd
logpath = /var/log/auth.log
maxretry = 3

This configuration bans any IP that fails three SSH login attempts within ten minutes, for one hour. The banaction = ufw directive tells fail2ban to use your firewall (which you'll set up next) for the actual blocking.

Start and enable fail2ban:

sudo systemctl enable fail2ban
sudo systemctl start fail2ban

Check the current ban status:

sudo fail2ban-client status sshd

I've seen this pattern where a Pi exposed to the internet accumulates hundreds of banned IPs within the first day. That's not a sign that your Pi is being specifically targeted. It's a sign of how much automated scanning happens on the internet at all times. Those bots aren't going away. Fail2ban makes sure they can't keep trying.

Fail2ban isn't limited to SSH. If you run a web-facing service, you can add jails for HTTP authentication failures, too:

[nginx-http-auth]
enabled = true
port    = http,https
filter  = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 3

The principle is the same for every jail: watch a log file for a pattern, count failures within a time window, ban the source IP when the threshold is crossed. Any service that logs authentication failures can be protected this way.

Key takeaway

Fail2ban turns your log files into an automated defense system. It bans IPs that exhibit attack patterns — brute-force logins, repeated authentication failures, port probes — so you don't have to monitor logs yourself.

Layer 3: UFW Firewall

UFW (Uncomplicated Firewall) is a frontend for iptables that does exactly what the name promises — makes firewall configuration simple enough that there's no excuse not to do it.

sudo apt install -y ufw

The strategy: deny everything inbound, then explicitly allow only the ports you need.

# Set default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH (do this BEFORE enabling, or you'll lock yourself out)
sudo ufw allow 22/tcp comment 'SSH'

# Allow other services as needed
sudo ufw allow 5000/tcp comment 'Flask API'
sudo ufw allow 1883/tcp comment 'MQTT'
sudo ufw allow 3000/tcp comment 'Grafana'

# Enable the firewall
sudo ufw enable

# Verify
sudo ufw status verbose
The port 22 rule

Always add the SSH allow rule BEFORE enabling UFW. If you enable UFW with a default deny policy and no SSH exception, your current session stays alive but you won't be able to reconnect. You'll need physical access to the Pi to fix it.

The output of ufw status verbose should look something like:

Status: active
Default: deny (incoming), allow (outgoing), disabled (routed)

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere         # SSH
5000/tcp                   ALLOW IN    Anywhere         # Flask API
1883/tcp                   ALLOW IN    Anywhere         # MQTT
3000/tcp                   ALLOW IN    Anywhere         # Grafana

A firewall that denies everything by default and allows only what you explicitly need is the difference between a Pi that's accessible and a Pi that's exposed.

For Pi boards that only need to be accessed from your local network, tighten the rules further:

# Only allow SSH from the local subnet
sudo ufw delete allow 22/tcp
sudo ufw allow from 192.168.1.0/24 to any port 22 proto tcp comment 'SSH local only'

Layer 4: Unattended Upgrades

Security patches ship regularly for Debian packages. If you don't apply them, known vulnerabilities accumulate on your Pi like unlocked doors. Unattended-upgrades automates the patching process so you don't need to SSH in every week and run apt upgrade manually.

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Select "Yes" when prompted. This enables automatic daily checks for security updates and installs them without intervention.

Verify the configuration:

cat /etc/apt/apt.conf.d/20auto-upgrades

You should see:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

For finer control, edit the unattended-upgrades config:

sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

Key settings to consider:

// Auto-reboot if a kernel update requires it (3 AM to minimize disruption)
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";

// Email notification (if you have a local MTA configured)
Unattended-Upgrade::Mail "you@example.com";

// Remove unused dependencies after upgrades
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Key takeaway

Unattended-upgrades closes the gap between "a vulnerability was patched upstream" and "my Pi has the patch." Without it, every day that passes without an update is a day your Pi runs with known, publicly documented vulnerabilities.

Disabling Unnecessary Services

A default Raspberry Pi OS installation runs services you probably don't need. Every running service is an attack surface. Audit and disable:

# List all enabled services
systemctl list-unit-files --state=enabled

# Common services to disable if unused
sudo systemctl disable bluetooth.service    # If you don't use Bluetooth
sudo systemctl disable avahi-daemon.service  # mDNS — disable if using static IPs
sudo systemctl disable triggerhappy.service  # Hotkey daemon for media buttons
sudo systemctl disable hciuart.service       # Bluetooth UART

Check which network ports are open:

sudo ss -tlnp

Any port that shows a listening service is a port an attacker can probe. If you don't recognize a service or don't need it, stop it and disable it.

On a typical fresh Raspberry Pi OS install, you'll find between eight and fifteen enabled services. After disabling what you don't need, you should be down to five or six: SSH, your firewall, your application, and the core system services. A smaller attack surface means fewer things to defend, fewer things to update, and fewer things that can go wrong.

VPN Access with WireGuard

If you need remote access to your Pi — and at some point you will — the answer is a VPN, not port forwarding. Port forwarding exposes your SSH port to the entire internet. A VPN exposes nothing; the Pi is reachable only from inside the encrypted tunnel.

WireGuard is the modern choice: faster than OpenVPN, simpler to configure, and built into the Linux kernel since 5.6.

sudo apt install -y wireguard

Generate keys on the Pi:

wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key
sudo chmod 600 /etc/wireguard/private.key

Create the WireGuard interface config:

sudo nano /etc/wireguard/wg0.conf
[Interface]
PrivateKey = <contents of /etc/wireguard/private.key>
Address = 10.0.0.1/24
ListenPort = 51820

[Peer]
PublicKey = <public key from your workstation>
AllowedIPs = 10.0.0.2/32

On your workstation, create a matching config that points to your router's public IP (with port 51820 forwarded to the Pi). Enable the tunnel:

sudo systemctl enable wg-quick@wg0
sudo systemctl start wg-quick@wg0

Add the WireGuard port to UFW:

sudo ufw allow 51820/udp comment 'WireGuard'

Now you can SSH into your Pi from anywhere in the world through the VPN tunnel — and the only port exposed to the internet is WireGuard's UDP port, which silently drops packets from any IP that doesn't have the correct cryptographic key. No handshake, no response, no confirmation the port is even open.

✕ Port forwarding SSH
  • Port 22 visible to the entire internet
  • Bots find it within hours
  • Brute-force attempts 24/7
  • Every service needs a separate forwarded port
✓ WireGuard VPN
  • Only UDP 51820 exposed
  • Silent drop for unauthorized packets
  • Cryptographic authentication only
  • All services accessible through one tunnel

The Attack Surface You're Defending Against

This isn't hypothetical. Shodan regularly indexes Raspberry Pi devices with open ports. The attack pattern is predictable:

  1. Automated scanners probe entire IP ranges for port 22 (SSH)
  2. When they find a response, they try default credentials: pi:raspberry, root:root, admin:admin
  3. If they get in, they install cryptocurrency miners, add the Pi to a botnet, or use it as a pivot point for attacking other devices on the same network
  4. The Pi's owner notices nothing until their internet slows down, their electricity bill spikes, or — worst case — their Pi is used to attack someone else

The Hardening Stack makes this attack chain fail at step 2 (SSH keys reject passwords), step 1 (UFW closes unused ports), and step 3 (fail2ban rate-limits the probing). Unattended-upgrades patches the vulnerabilities that could bypass all three.

Physical security

If an attacker has physical access to your Pi, they can pull the SD card and read it on another machine. Full-disk encryption with LUKS is possible on the Pi but adds complexity and requires entering a passphrase on every boot (which defeats the purpose of a headless deployment). For most use cases, physical security means putting the Pi in a locked enclosure. For truly sensitive deployments, use a Compute Module with eMMC storage (harder to remove) and consider Secure Boot.

What to Do Monday Morning

Change the default password right now

If your Pi still uses the default password, run passwd immediately. Choose something unique. This takes thirty seconds and eliminates the most common attack vector.

Set up SSH key authentication and disable passwords

Generate an ed25519 key pair on your workstation, copy it to the Pi with ssh-copy-id, verify key login works, then disable PasswordAuthentication in /etc/ssh/sshd_config. Test before closing your session.

Install and configure fail2ban

sudo apt install fail2ban, create /etc/fail2ban/jail.local with the SSH jail configuration from this chapter, enable and start the service. Check fail2ban-client status sshd after an hour — the ban count will convince you this was necessary.

Enable UFW with a deny-all default

Install UFW, set default deny incoming, explicitly allow only SSH and the ports your services actually use, enable. Run sudo ufw status verbose and read it. Every open port should be one you intentionally opened.

Enable unattended-upgrades

Install and enable with dpkg-reconfigure. Set automatic reboot at 3 AM for kernel updates. This is the layer most people skip — and it's the one that protects you six months from now when a vulnerability is disclosed and you've forgotten this Pi exists.

Audit with ss and systemctl

Run sudo ss -tlnp to see every listening port. Run systemctl list-unit-files --state=enabled to see every service that starts on boot. Disable anything you don't recognize or don't need. A smaller surface area is a safer surface area.

The trap was thinking that NAT provides security. NAT provides address translation. Security requires deliberate configuration — SSH keys to block password attacks, fail2ban to rate-limit probes, a firewall to close unused ports, and automatic patching to fix vulnerabilities before attackers exploit them. Fifteen minutes of hardening today prevents an incident you might not even detect for months.

Fifteen minutes of hardening today prevents an incident you might not even detect for months.