How to configure hardened Raspberry Pi
Requirements
- Raspberry Pi (with 64-bit support)
- HDMI cable, keyboard, microSD card, microSD to SD adapter and power adapter
- macOS 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 11)
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
WARNING: DO NOT RUN THE FOLLOWING COMMANDS AS-IS.
Heads-up: run
diskutil list
to find disk ID of microSD card to overwrite with “Raspberry Pi OS Lite” (disk4
in the following example).
Heads-up: replace
diskn
andrdiskn
with disk ID of microSD card (disk4
andrdisk4
in the following example) and2022-01-28-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 328.4 MB disk3s2
4: APFS Volume Recovery 815.1 MB disk3s3
5: APFS Volume Data 458.6 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-01-28-raspios-bullseye-arm64-lite.img of=/dev/rdiskn
1904+0 records in
1904+0 records out
1996488704 bytes transferred in 141.557206 secs (14103759 bytes/sec)
$ sudo diskutil unmountDisk /dev/diskn
Unmount of all volumes on disk4 was successful
Step 5: log in as pi (using keyboard) and change password using passwd
Heads-up: current password is
raspberry
.
$ passwd
Changing password for pi.
Current password:
New password:
Retype new password:
passwd: password updated successfully
Step 6: configure Wi-Fi (if not using ethernet)
sudo raspi-config
Select “System Options”, then “Wireless LAN”, choose country, then select “OK”, enter “SSID”, enter passphrase.
Step 7: enable SSH
sudo raspi-config
Select “Interface Options”, then “SSH”, then “Yes”, then “OK” and finally “Finish”.
When asked if you wish to reboot, select “No”.
Step 8: find IP of Raspberry Pi (see eth0
if using ethernet or wlan0
if using Wi-Fi)
ip a
Step 9: log in to Raspberry Pi over SSH
Heads-up: replace
10.0.1.181
with IP of Raspberry Pi.
Heads-up: when asked for passphrase, enter passphrase from step 5.
ssh pi@10.0.1.181
Step 10: 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 11: 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 12: log out
exit
Step 13: log in
Heads-up: replace
10.0.1.181
with IP of Raspberry Pi.
Heads-up: when asked for passphrase, enter passphrase from step 1.
ssh -i ~/.ssh/pi pi@10.0.1.181
Step 14: switch to root
sudo su -
Step 15: disable root Bash history
echo "HISTFILESIZE=0" >> ~/.bashrc
history -c; history -w
source ~/.bashrc
Step 16: disable pi sudo nopassword
“feature”
rm /etc/sudoers.d/010_*
Step 17: 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 18: 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 19: 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 20: 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 21: 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 22: log out and log in to confirm firewall is not blocking SSH
Log out
$ exit
$ exit
Log in
Heads-up: replace
10.0.1.181
with IP of Raspberry Pi.
ssh -i ~/.ssh/pi pi@10.0.1.181
Step 23: switch to root
sudo su -
Step 24: make firewall rules persistent
cat << "EOF" > /etc/nftables.conf
#!/usr/sbin/nft -f
flush ruleset
EOF
nft list ruleset >> /etc/nftables.conf
Step 25: set timezone
See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
timedatectl set-timezone America/Montreal
Step 26: disable swap
systemctl disable dphys-swapfile
Step 27: update APT index and upgrade packages
$ apt update
$ apt upgrade -y
👍