Raspberry Pi OS is now based on Debian 13 (Trixie). It’s time to revisit the article on how to harden a Raspberry Pi. Download the latest « Lite » image from the official site and flash it to your storage media.
On first boot you will be prompted to create and account and select a password. No more default « pi » account, this is a good news.
Login for the first time and run « sudo raspi-config ». You will be able to configure the keyboard layout, the locales, the hostname, boot order and more. Enable SSH but don’t forward any internet traffic yet. From now one, everything can be done remotely.
Package clean-up
Even the « lite » version of Raspberry Pi OS comes with tons of packages and services. On first boot we have 1.9 GB of disk used (plus 2 GB swap) and 15 running services.
# If you don't need Bluetooth
sudo apt remove --purge bluez
# If you don't need wifi
sudo apt remove --purge wireless-tools wpasupplicant
# Removing unwanted software
sudo apt remove --purge avahi-daemon build-essential busybox cloud-init cpp-14 fbset gcc-14 gdb gpiod lua5.1 luajit make manpages-dev mkvtoolnix modemmanager ntfs-3g ppp polkitd python3-gpiozero python3-libgpiod python3-rpi-lgpio python3-smbus2 python3-spidev rpicam-apps-lite v4l-utils libx11-6 libx11-data xauth xdg-user-dirs
# Removing dependencies of unwanted software
sudo apt autoremove --purge
# Removing unwanted locales
sudo dpkg-reconfigure locales
sudo apt install localepurge
# I generaly only keep en_US.UTF8 and fr_FR.UTF8
sudo localepurge
After clean up, we have 1.4 GB used and 10 services. Time to update the remaining packages and configure automatic application of security updates.
sudo apt update
sudo apt upgrade
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
Installing vim and configure it as default editor.
# Install your favorite editor
sudo apt install vim
# Configure it as default
sudo update-alternatives --config editor
# Optional, remove other editors
sudo apt remove --purge nano ed
Time to update firmware config
sudo vim /boot/firmware/config.txt
# review the content
# if needed add the following lines at the end
dtoverlay=disable-bt
dtoverlay=disable-wifi
Account security
Let’s create a second used to be used in case we break something with the main user.
# Create new user
sudo adduser <new user login>
# Setup the proper groups
sudo usermod -aG adm,dialout,cdrom,sudo,audio,video,plugdev,games,users,netdev,gpio,i2c,spi,render,input <new user login>
Let’s review all users with a password.
sudo awk -F":" '($2 != "!" && $2 != "*" && $2 != "!*") {print $1 " has a password"}' /etc/shadow
# If you want to lock a given user account
sudo passwd -l <user name>
Let’s review all users with a shell.
awk -F":" '($7 != "/usr/sbin/nologin") {print $1 " has a login shell: " $7}' /etc/passwd
# If you want to remove shell for a given user
# I recommend to remove the shell of root
sudo chsh -s /usr/sbin/nologin <user name>
Let’s review who are in groups « adm » and « sudo ».
awk -F":" '($1 == "adm") {print $4 }' /etc/group
awk -F":" '($1 == "sudo") {print $4 }' /etc/group
# If needed remove users from these groups
Let’s review sudoers.
sudo grep -e "^[^#].*NOPASSWD.*$" /etc/sudoers /etc/sudoers.d/*
# Nobody should skip password check
sudo grep -e "^[^#].*ALL.*$" /etc/sudoers /etc/sudoers.d/*
# Only root and %sudo should have rights
# If you need to adjust
sudo visudo
[Optional] Configure NTP
I found Cloudflare NTP server very reliable. I also use Nice University NTP and Observatoire de Paris NTP. I edit /etc/systemd/timesyncd.conf with the following values:
[Time]
NTP=time.cloudflare.com ntp.unice.fr ntp.obspm.fr
FallbackNTP=0.fr.pool.ntp.org 1.fr.pool.ntp.org 2.fr.pool.ntp.org 3.fr.pool.ntp.org
Hardening OpenSSH
Let’s create a group for SSH users. Login will be allowed only to the members of this group.
sudo groupadd -r ssh-users
sudo usermod -aG ssh-users $USER
awk -F":" '($1 == "ssh-users") {print $4 }' /etc/group
You can remove your old server keys and regenerate only the most secure formats.
systemctl status systemd-random-seed
sudo rm -f /etc/ssh/ssh_host_*
sudo ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""
sudo ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N ""
Remove small Deffie-Hellman moduli.
awk '$5 >= 3071' /etc/ssh/moduli > moduli.safe
sudo cp moduli.safe /etc/ssh/moduli
rm moduli.safe
Next step will be to edit /etc/ssh/sshd_config, if needed use the man page of sshd.
# Change the default port
Port <custom port numer>
# Use only the keys you regenerated
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
# Configure ciphers and keying
KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
Ciphers aes256-gcm@openssh.com,chacha20-poly1305@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
# Allow only members of group ssh-users
AllowGroups ssh-users
# We don't want root to connect
PermitRootLogin no
# You probably want to rely only on pubkey
PubkeyAuthentication yes
# Disable password auth only after testing your pubkey
PasswordAuthentication no
PermitEmptyPasswords no
TODO changing banner and motd ?
When you are happy with your config, restart your SSH server and check everything is ok before closing the actual connection.
sudo systemctl restart ssh
I took several ideas from this hardening guide.
I use 2 different services to test my SSH configuration: ssh-audit.com and cryptcheck.fr
Mosh, ufw and fail2ban
Mosh is great when you use SSH from a mobile. Ufw is the uncomplicated firewall. Fail2ban is an intrusion prevention system.
sudo apt update
sudo apt install mosh ufw fail2ban
Lets configure UFW to allow SSH, HTTP, HTTPS and Mosh
# Allow SSH - Replace with your custom port
ufw limit 22/tcp
# Allow Mosh
ufw allow 60000:61000/udp
ufw default deny incoming
ufw default allow outgoing
ufw logging on
ufw enable
Check the service is running ok
systemctl status ufw
sudo ufw status verbose
Fail2ban needs some tuning on Debian 12. Edit /etc/fail2ban/jail.conf and replace some values:
# Update ignoreip based on your need
ignoreip = 127.0.0.1/8 ::1
# Increase bantime = 10m
bantime = 12h
# Increase findtime = 10m
findtime = 12h
# Change backend = auto
backend = systemd
# Change banaction = iptables-multiport
banaction = ufw
# Change banaction_allports = iptables-multiport
banaction_allports = ufw
Restart the service and check it’s running ok
sudo systemctl restart fail2ban
systemctl status fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
To go further on fail2ban and UFW, I would recommend you to read this blog post.
If you are interested in port knocking, the ultimate security option, I recommend article 1 and article 2.

