<!--
Title: How to configure hardened Raspberry Pi
Description: Learn how to configure hardened Raspberry Pi.
Author: Sun Knudsen <https://github.com/sunknudsen>
Contributors: Sun Knudsen <https://github.com/sunknudsen>
Reviewers:
Publication date: 2020-11-27T10:00:26.807Z
Listed: true
-->

# How to configure hardened Raspberry Pi

[![How to configure hardened Raspberry Pi](how-to-configure-hardened-raspberry-pi.png)](https://www.youtube.com/watch?v=6R8uKdstnts "How to configure hardened Raspberry Pi")

## Requirements

- [Raspberry Pi](https://www.raspberrypi.org/) (with [64-bit support](https://www.raspberrypi.com/news/raspberry-pi-os-64-bit/))
- 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 (from `cat << "EOF"` to `EOF` 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).

```console
$ 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](#step-11-configure-pi-ssh-authorized-keys))

```shell
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](https://www.raspberrypi.org/software/operating-systems/)

### 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` and `rdiskn` with disk ID of microSD card (`disk4` and `rdisk4` in the following example) and `2022-01-28-raspios-bullseye-arm64-lite.img` with current image.

```console
$ 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`.

```console
$ 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)

```shell
sudo raspi-config
```

Select “System Options”, then “Wireless LAN”, choose country, then select “OK”, enter “SSID”, enter passphrase.

### Step 7: enable SSH

```shell
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 8: find IP of Raspberry Pi (see `eth0` if using ethernet or `wlan0` if using Wi-Fi)

```shell
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](#step-5-log-in-as-pi-using-keyboard-and-change-password-using-passwd).

```shell
ssh pi@10.0.1.181
```

### Step 10: disable pi Bash history

```shell
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

```shell
mkdir ~/.ssh
```

#### Create `~/.ssh/authorized_keys` using heredoc generated at [step 2](#step-2-generate-heredoc-the-output-of-following-command-will-be-used-at-step-11)

```shell
cat << "_EOF" > ~/.ssh/authorized_keys
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHLwQ2fk5VvoKJ6PNdJfmtum6fTAIn7xG5vbFm0YjEGY pi
_EOF
```

### Step 12: log out

```shell
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](#step-1-create-ssh-key-pair-on-macos).

```shell
ssh -i ~/.ssh/pi pi@10.0.1.181
```

### Step 14: switch to root

```shell
sudo su -
```

### Step 15: disable root Bash history

```shell
echo "HISTFILESIZE=0" >> ~/.bashrc
history -c; history -w
source ~/.bashrc
```

### Step 16: disable pi sudo `nopassword` “feature”

```shell
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).

```console
$ passwd
New password:
Retype new password:
passwd: password updated successfully
```

### Step 18: disable root login and password authentication

```shell
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

```shell
echo "dtoverlay=disable-bt" >> /boot/config.txt
```

#### Disable Wi-Fi (if using ethernet)

```shell
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.

```shell
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

```shell
systemctl enable nftables
systemctl start nftables
```

#### Configure firewall rules

```shell
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:

```shell
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:

```shell
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

```console
$ exit

$ exit
```

#### Log in

> Heads-up: replace `10.0.1.181` with IP of Raspberry Pi.

```shell
ssh -i ~/.ssh/pi pi@10.0.1.181
```

### Step 23: switch to root

```shell
sudo su -
```

### Step 24: make firewall rules persistent

```shell
cat << "EOF" > /etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

EOF
```

```shell
nft list ruleset >> /etc/nftables.conf
```

### Step 25: set timezone

See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

```shell
timedatectl set-timezone America/Montreal
```

### Step 26: disable swap

```shell
systemctl disable dphys-swapfile
```

### Step 27: update APT index and upgrade packages

```console
$ apt update

$ apt upgrade -y
```

👍
