How to configure hardened Raspberry Pi
Requirements
- Raspberry Pi 4
- microSD card or external solid state drive (with USB-A connector)
- microSD card reader or secure digital (SD) card reader with microSD to SD adapter (if using microSD card)
- USB-C power adapter (minimum 3A)
- Keyboard (with USB-A connector)
- Micro HDMI to HDMI cable
- macOS or Linux computer
Caveats
- When copy/pasting commands that start with
$
, strip out$
as this character is not part of the command - When copy/pasting commands that start with
cat << "EOF"
, select all lines at once (fromcat << "EOF"
toEOF
inclusively) as they are part of the same (single) command
Guide
Step 1: create SSH key pair (on macOS)
When asked for file in which to save key, enter pi
.
When asked for passphrase, use output from openssl rand -base64 24
(and store passphrase in password manager).
$ mkdir ~/.ssh
$ cd ~/.ssh
$ ssh-keygen -t ed25519 -C "pi"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/Users/sunknudsen/.ssh/id_ed25519): pi
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in pi.
Your public key has been saved in pi.pub.
The key fingerprint is:
SHA256:U3hEUQC0GAyCOPaks1Xv04ouoN9ezwtfK4CnUxKqAms pi
The key's randomart image is:
+--[ED25519 256]--+
|... .o..oo=+. |
|+. o ..o + |
|..+ . o o o |
| o o. . o |
| +. o. S |
|.o. o +o o |
|oo. =+.o . |
|=E ooo *.. . |
|o...=o =o. |
+----[SHA256]-----+
$ cat pi.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHLwQ2fk5VvoKJ6PNdJfmtum6fTAIn7xG5vbFm0YjEGY pi
Step 2: generate heredoc (the output of following command will be used at step 13)
cat << EOF
cat << "_EOF" > ~/.ssh/authorized_keys
$(cat ~/.ssh/pi.pub)
_EOF
EOF
Step 3: download latest version of 64-bit Raspberry Pi OS Lite
Step 4: copy “Raspberry Pi OS Lite” to microSD card or external solid state drive (follow these steps instead of step 4 if on Linux)
WARNING: BE VERY CAREFUL WHEN RUNNING
DD
AS DATA CAN BE PERMANENTLY DESTROYED (BEGINNERS SHOULD CONSIDER USING BALENAETCHER INSTEAD).
Heads-up: run
diskutil list
to find disk ID of microSD card or external solid state drive to overwrite with “Raspberry Pi OS Lite” (disk4
in the following example).
Heads-up: replace
diskn
andrdiskn
with disk ID of microSD card or external solid state drive (disk4
andrdisk4
in the following example) and2022-04-04-raspios-bullseye-arm64-lite.img
with current image.
$ diskutil list
/dev/disk0 (internal):
#: TYPE NAME SIZE IDENTIFIER
0: GUID_partition_scheme 500.3 GB disk0
1: Apple_APFS_ISC 524.3 MB disk0s1
2: Apple_APFS Container disk3 494.4 GB disk0s2
3: Apple_APFS_Recovery 5.4 GB disk0s3
/dev/disk3 (synthesized):
#: TYPE NAME SIZE IDENTIFIER
0: APFS Container Scheme - +494.4 GB disk3
Physical Store disk0s2
1: APFS Volume Macintosh HD 15.3 GB disk3s1
2: APFS Snapshot com.apple.os.update-... 15.3 GB disk3s1s1
3: APFS Volume Preboot 412.4 MB disk3s2
4: APFS Volume Recovery 807.3 MB disk3s3
5: APFS Volume Data 384.5 GB disk3s5
6: APFS Volume VM 2.1 GB disk3s6
/dev/disk4 (external, physical):
#: TYPE NAME SIZE IDENTIFIER
0: FDisk_partition_scheme *15.9 GB disk4
1: Windows_NTFS Untitled 15.9 GB disk4s1
$ sudo diskutil unmount /dev/diskn
disk4 was already unmounted or it has a partitioning scheme so use "diskutil unmountDisk" instead
$ sudo diskutil unmountDisk /dev/diskn (if previous step fails)
Unmount of all volumes on disk4 was successful
$ sudo dd bs=1m if=$HOME/Downloads/2022-04-04-raspios-bullseye-arm64-lite.img of=/dev/rdiskn
1908+0 records in
1908+0 records out
2000683008 bytes transferred in 239.955976 secs (8337709 bytes/sec)
$ sudo diskutil unmountDisk /dev/diskn
Unmount of all volumes on disk4 was successful
Step 5: configure keyboard
Step 6: create user
When asked for user, use pi-admin
.
When asked for password, use output from openssl rand -base64 24
(and store password in password manager).
Step 7: configure Wi-Fi (if not using ethernet)
sudo raspi-config
Select “System Options”, then “Wireless LAN”, choose country, then select “OK”, enter “SSID” and, finally, enter passphrase.
Step 8: disable auto login
sudo raspi-config
Select “System Options”, then “Boot / Auto Login” and, finally, select “Console”.
Step 9: enable SSH
sudo raspi-config
Select “Interface Options”, then “SSH”, then “Yes”, then “OK” and, finally, select “Finish”.
When asked if you wish to reboot, select “No”.
Step 10: find IP of Raspberry Pi (see eth0
if using ethernet or wlan0
if using Wi-Fi)
ip a
Step 11: log in to Raspberry Pi over SSH
Heads-up: replace
10.0.1.94
with IP of Raspberry Pi.
Heads-up: when asked for passphrase, enter passphrase from step 5.
ssh pi-admin@10.0.1.94
Step 12: disable pi Bash history
sed -i -E 's/^HISTSIZE=/#HISTSIZE=/' ~/.bashrc
sed -i -E 's/^HISTFILESIZE=/#HISTFILESIZE=/' ~/.bashrc
echo "HISTFILESIZE=0" >> ~/.bashrc
history -c; history -w
source ~/.bashrc
Step 13: configure pi SSH authorized keys
Create .ssh
directory
mkdir ~/.ssh
Create ~/.ssh/authorized_keys
using heredoc generated at step 2
cat << "_EOF" > ~/.ssh/authorized_keys
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHLwQ2fk5VvoKJ6PNdJfmtum6fTAIn7xG5vbFm0YjEGY pi
_EOF
Step 14: log out
exit
Step 15: log in
Heads-up: replace
10.0.1.94
with IP of Raspberry Pi.
Heads-up: when asked for passphrase, enter passphrase from step 1.
ssh -i ~/.ssh/pi pi-admin@10.0.1.94
Step 16: switch to root
sudo su -
Step 17: disable root Bash history
echo "HISTFILESIZE=0" >> ~/.bashrc
history -c; history -w
source ~/.bashrc
Step 18: disable pi sudo nopassword
“feature”
rm /etc/sudoers.d/010_*
Step 19: set root password
When asked for password, use output from openssl rand -base64 24
(and store password in password manager).
$ passwd
New password:
Retype new password:
passwd: password updated successfully
Step 20: disable root login and password authentication
sed -i -E 's/^(#)?PermitRootLogin (prohibit-password|yes)/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i -E 's/^(#)?PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart ssh
Step 21: disable Bluetooth and Wi-Fi
Heads-up: step will take effect after reboot.
Disable Bluetooth
echo "dtoverlay=disable-bt" >> /boot/config.txt
Disable Wi-Fi (if using ethernet)
echo "dtoverlay=disable-wifi" >> /boot/config.txt
Step 22: configure sysctl (if network is IPv4-only)
Heads-up: only run following if network is IPv4-only.
cp /etc/sysctl.conf /etc/sysctl.conf.backup
cat << "EOF" >> /etc/sysctl.conf
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
EOF
sysctl -p
Step 23: enable nftables and configure firewall rules
Enable nftables
systemctl enable nftables
systemctl start nftables
Configure firewall rules
nft flush ruleset
nft add table ip firewall
nft add chain ip firewall input { type filter hook input priority 0 \; policy drop \; }
nft add rule ip firewall input iif lo accept
nft add rule ip firewall input iif != lo ip daddr 127.0.0.0/8 drop
nft add rule ip firewall input tcp dport ssh accept
nft add rule ip firewall input ct state established,related accept
nft add chain ip firewall forward { type filter hook forward priority 0 \; policy drop \; }
nft add chain ip firewall output { type filter hook output priority 0 \; policy drop \; }
nft add rule ip firewall output oif lo accept
nft add rule ip firewall output tcp dport { http, https } accept
nft add rule ip firewall output udp dport { domain, ntp } accept
nft add rule ip firewall output ct state established,related accept
If network is IPv4-only, run:
nft add table ip6 firewall
nft add chain ip6 firewall input { type filter hook input priority 0 \; policy drop \; }
nft add chain ip6 firewall forward { type filter hook forward priority 0 \; policy drop \; }
nft add chain ip6 firewall output { type filter hook output priority 0 \; policy drop \; }
If network is dual stack (IPv4 + IPv6) run:
nft add table ip6 firewall
nft add chain ip6 firewall input { type filter hook input priority 0\; policy drop\; }
nft add rule ip6 firewall input iif lo accept
nft add rule ip6 firewall input iif != lo ip6 daddr ::1 drop
nft add rule ip6 firewall input meta l4proto ipv6-icmp icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem } accept
nft add rule ip6 firewall input meta l4proto ipv6-icmp icmpv6 type { nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, nd-redirect } ip6 hoplimit 255 accept
nft add rule ip6 firewall input tcp dport ssh accept
nft add rule ip6 firewall input ct state established,related accept
nft add chain ip6 firewall forward { type filter hook forward priority 0\; policy drop\; }
nft add chain ip6 firewall output { type filter hook output priority 0\; policy drop\; }
nft add rule ip6 firewall output oif lo accept
nft add rule ip6 firewall output meta l4proto ipv6-icmp icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem } accept
nft add rule ip6 firewall output meta l4proto ipv6-icmp icmpv6 type { nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } ip6 hoplimit 255 accept
nft add rule ip6 firewall output tcp dport { http, https } accept
nft add rule ip6 firewall output udp dport { domain, ntp } accept
nft add rule ip6 firewall output ct state related,established accept
Step 24: log out and log in to confirm firewall is not blocking SSH
Log out
$ exit
$ exit
Log in
Heads-up: replace
10.0.1.94
with IP of Raspberry Pi.
ssh -i ~/.ssh/pi pi-admin@10.0.1.94
Step 25: switch to root
sudo su -
Step 26: make firewall rules persistent
cat << "EOF" > /etc/nftables.conf
#!/usr/sbin/nft -f
flush ruleset
EOF
nft list ruleset >> /etc/nftables.conf
Step 27: set timezone
See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
timedatectl set-timezone America/Montreal
Step 28: disable swap
systemctl disable dphys-swapfile
Step 29: update APT index and upgrade packages
$ apt update
$ apt upgrade -y
👍