diff --git a/how-to-configure-hardened-raspberry-pi/README.md b/how-to-configure-hardened-raspberry-pi/README.md index 24f97a3..3856a23 100644 --- a/how-to-configure-hardened-raspberry-pi/README.md +++ b/how-to-configure-hardened-raspberry-pi/README.md @@ -36,33 +36,33 @@ $ mkdir ~/.ssh $ cd ~/.ssh -$ ssh-keygen -t rsa -C "pi" -Generating public/private rsa key pair. -Enter file in which to save the key (/Users/sunknudsen/.ssh/id_rsa): pi +$ 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:NZkr0Bmu6tpQfDp2GHTIPyBz253uZk/gOmoqzEszGM0 pi +SHA256:U3hEUQC0GAyCOPaks1Xv04ouoN9ezwtfK4CnUxKqAms pi The key's randomart image is: -+---[RSA 3072]----+ -| . | -| . . o o o | -| o * o + = | -| o * * + o o | -|. E = * S . | -|.. . * + o | -|++. * . o . | -|ooo=.o.oo. | -| o+++..+... | ++--[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-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCzQpX9uqDP8L2gSZNJxYEi04Y1pZWz28v4zANY5dU6M35OFzXZcRcBqi2ZxiQofgxRrX9QlAcmcPFz8/CkpPw2WgQTflm+46ZrVEZcwwGwJsJwm7QVLQLd44/xtejEvMjzsuYDjJ1q4WhEvMSleTfOrix4yP0mjn83Zk1l6AMxR5J8DDumiHsGSYfcp+1XS9x4r4HP0mS2RpIy3rcoxLoJaYEKvVTj9qdvPMK7SDymZcvuBsgObEARVr77q4qhUfTP+xR91hHNEYD9FnCHF3qQBzlTlmTwpwhH6vOdWE3uUXCug9Ugw42Zj3PW0zd5rQ7EEpD9SDLbUqajpn2M5AlhkS9OrLpnIptocetRKNI9HzyAV1KqdNiQeL7/59d4y+HuZ9y032SaNzR1fw0nYMoHzTN9d+zPvziDZ183/pwtEXZNVVGzYO1r56n3S4vLx8YCpYqiHYVQVDF8aweoHYs3dAGAfPxmQ85+45UKpFR18XSGCqCO2fwbyTGDhkxCzU= pi +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHLwQ2fk5VvoKJ6PNdJfmtum6fTAIn7xG5vbFm0YjEGY pi ``` -### Step 2: generate heredoc (the output of following command will be used at [step 10](#step-10-configure-pi-ssh-authorized-keys)) +### 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 @@ -78,47 +78,48 @@ EOF > WARNING: DO NOT RUN THE FOLLOWING COMMANDS AS-IS. -Run `diskutil list` to find disk ID of SD card to overwrite with “Raspberry Pi OS Lite” (`disk2` in the following example). +Run `diskutil list` to find disk ID of SD card to overwrite with “Raspberry Pi OS Lite” (`disk4` in the following example). -Replace `diskn` and `rdiskn` with disk ID of SD card (`disk2` and `rdisk2` in the following example) and `2021-03-04-raspios-buster-armhf-lite.img` with current image. +Replace `diskn` and `rdiskn` with disk ID of SD card (`disk4` and `rdisk4` in the following example) and `2021-10-30-raspios-bullseye-armhf-lite.img` with current image. ```console $ diskutil list -/dev/disk0 (internal, physical): +/dev/disk0 (internal): #: TYPE NAME SIZE IDENTIFIER - 0: GUID_partition_scheme *500.3 GB disk0 - 1: EFI EFI 209.7 MB disk0s1 - 2: Apple_APFS Container disk1 500.1 GB disk0s2 + 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/disk1 (synthesized): +/dev/disk3 (synthesized): #: TYPE NAME SIZE IDENTIFIER - 0: APFS Container Scheme - +500.1 GB disk1 + 0: APFS Container Scheme - +494.4 GB disk3 Physical Store disk0s2 - 1: APFS Volume Macintosh HD - Data 340.9 GB disk1s1 - 2: APFS Volume Preboot 85.9 MB disk1s2 - 3: APFS Volume Recovery 529.0 MB disk1s3 - 4: APFS Volume VM 3.2 GB disk1s4 - 5: APFS Volume Macintosh HD 11.3 GB disk1s5 + 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/disk2 (internal, physical): +/dev/disk4 (external, physical): #: TYPE NAME SIZE IDENTIFIER - 0: FDisk_partition_scheme *15.9 GB disk2 - 1: Windows_FAT_32 boot 268.4 MB disk2s1 - 2: Linux 15.7 GB disk2s2 + 0: FDisk_partition_scheme *15.9 GB disk4 + 1: Windows_NTFS Untitled 15.9 GB disk4s1 $ sudo diskutil unmount /dev/diskn -disk2 was already unmounted or it has a partitioning scheme so use "diskutil unmountDisk" instead +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 disk2 was successful +Unmount of all volumes on disk4 was successful -$ sudo dd bs=1m if=$HOME/Downloads/2021-03-04-raspios-buster-armhf-lite.img of=/dev/rdiskn -1772+0 records in -1772+0 records out -1858076672 bytes transferred in 40.449002 secs (45936280 bytes/sec) +$ sudo dd bs=1m if=$HOME/Downloads/2021-10-30-raspios-bullseye-armhf-lite.img of=/dev/rdiskn +1864+0 records in +1864+0 records out +1954545664 bytes transferred in 37.454965 secs (52183887 bytes/sec) $ sudo diskutil unmountDisk /dev/diskn -Unmount of all volumes on disk2 was successful +Unmount of all volumes on disk4 was successful ``` ### Step 5: log in as pi (using keyboard) and change password using `passwd` @@ -160,47 +161,15 @@ ip a ### Step 9: log in to Raspberry Pi over SSH -Replace `10.0.1.248` with IP of Raspberry Pi. +Replace `10.0.1.181` with IP of Raspberry Pi. 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.248 +ssh pi@10.0.1.181 ``` -### Step 10: 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-10) - -```shell -cat << "_EOF" > ~/.ssh/authorized_keys -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCzQpX9uqDP8L2gSZNJxYEi04Y1pZWz28v4zANY5dU6M35OFzXZcRcBqi2ZxiQofgxRrX9QlAcmcPFz8/CkpPw2WgQTflm+46ZrVEZcwwGwJsJwm7QVLQLd44/xtejEvMjzsuYDjJ1q4WhEvMSleTfOrix4yP0mjn83Zk1l6AMxR5J8DDumiHsGSYfcp+1XS9x4r4HP0mS2RpIy3rcoxLoJaYEKvVTj9qdvPMK7SDymZcvuBsgObEARVr77q4qhUfTP+xR91hHNEYD9FnCHF3qQBzlTlmTwpwhH6vOdWE3uUXCug9Ugw42Zj3PW0zd5rQ7EEpD9SDLbUqajpn2M5AlhkS9OrLpnIptocetRKNI9HzyAV1KqdNiQeL7/59d4y+HuZ9y032SaNzR1fw0nYMoHzTN9d+zPvziDZ183/pwtEXZNVVGzYO1r56n3S4vLx8YCpYqiHYVQVDF8aweoHYs3dAGAfPxmQ85+45UKpFR18XSGCqCO2fwbyTGDhkxCzU= pi -_EOF -``` - -### Step 11: log out - -```shell -exit -``` - -### Step 12: log in - -Replace `10.0.1.248` with IP of Raspberry Pi. - -When asked for passphrase, enter passphrase from [step 1](#step-1-create-ssh-key-pair-on-macos). - -```shell -ssh pi@10.0.1.248 -i ~/.ssh/pi -``` - -### Step 13: disable pi Bash history +### Step 10: disable pi Bash history ```shell sed -i -E 's/^HISTSIZE=/#HISTSIZE=/' ~/.bashrc @@ -210,46 +179,59 @@ history -c; history -w source ~/.bashrc ``` -### Step 14: configure pi `.vimrc` +### Step 11: configure pi SSH authorized keys + +#### Create `.ssh` directory ```shell -cat << "EOF" > ~/.vimrc -set encoding=UTF-8 -set termencoding=UTF-8 -set nocompatible -set backspace=indent,eol,start -set autoindent -set tabstop=2 -set shiftwidth=2 -set expandtab -set smarttab -set ruler -set paste -syntax on -EOF +mkdir ~/.ssh ``` -### Step 15: switch to root +#### 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 + +Replace `10.0.1.181` with IP of Raspberry Pi. + +When asked for passphrase, enter passphrase from [step 1](#step-1-create-ssh-key-pair-on-macos). + +```shell +ssh pi@10.0.1.181 -i ~/.ssh/pi +``` + +### 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: disable root Bash history - -```shell -echo "HISTFILESIZE=0" >> ~/.bashrc -history -c; history -w -source ~/.bashrc -``` - -### Step 18: set root password +### Step 17: set root password When asked for password, use output from `openssl rand -base64 24` (and store password in password manager). @@ -260,26 +242,7 @@ Retype new password: passwd: password updated successfully ``` -### Step 19: configure root `.vimrc` - -```shell -cat << "EOF" > ~/.vimrc -set encoding=UTF-8 -set termencoding=UTF-8 -set nocompatible -set backspace=indent,eol,start -set autoindent -set tabstop=2 -set shiftwidth=2 -set expandtab -set smarttab -set ruler -set paste -syntax on -EOF -``` - -### Step 20: disable root login and password authentication +### Step 18: disable root login and password authentication ```shell sed -i -E 's/^(#)?PermitRootLogin (prohibit-password|yes)/PermitRootLogin no/' /etc/ssh/sshd_config @@ -287,9 +250,9 @@ sed -i -E 's/^(#)?PasswordAuthentication yes/PasswordAuthentication no/' /etc/ss systemctl restart ssh ``` -### Step 21: disable Bluetooth and Wi-Fi +### Step 19: disable Bluetooth and Wi-Fi -> Heads-up: will take effect after reboot. +> Heads-up: step will take effect after reboot. #### Disable Bluetooth @@ -303,63 +266,9 @@ echo "dtoverlay=disable-bt" >> /boot/config.txt echo "dtoverlay=disable-wifi" >> /boot/config.txt ``` -### Step 22: update APT index, install `iptables-persistent` and Vim and upgrade system +### Step 20: configure sysctl (if network is IPv4-only) -#### Update APT index - -```shell -apt update -``` - -#### Install `iptables-persistent` and Vim - -When asked to save current IPv4 or IPv6 rules, answer “Yes”. - -```shell -apt install -y iptables-persistent vim -``` - -#### Upgrade packages - -> Heads-up: if asked which version of `/etc/ssh/sshd_config` to keep, select “keep the local version currently installed”. - -```shell -apt upgrade -y -``` - -### Step 23: reboot - -```shell -systemctl reboot -``` - -### Step 24: log in - -Replace `10.0.1.248` with IP of Raspberry Pi. - -When asked for passphrase, enter passphrase from [step 1](#step-1-create-ssh-key-pair-on-macos). - -```shell -ssh pi@10.0.1.248 -i ~/.ssh/pi -``` - -### Step 25: switch to root - -```shell -sudo su - -``` - -### Step 26: set timezone (the following is for Montreal time) - -See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones - -```shell -timedatectl set-timezone America/Montreal -``` - -### Step 27: configure sysctl (if network is IPv4-only) - -> Heads-up: only run the following if network is IPv4-only. +> Heads-up: only run following if network is IPv4-only. ```shell cp /etc/sysctl.conf /etc/sysctl.conf.backup @@ -371,70 +280,64 @@ EOF sysctl -p ``` -### Step 28: configure firewall +### Step 21: enable nftables and configure firewall rules + +#### Enable nftables ```shell -iptables -N SSH_BRUTE_FORCE_MITIGATION -iptables -A SSH_BRUTE_FORCE_MITIGATION -m recent --name SSH --set -iptables -A SSH_BRUTE_FORCE_MITIGATION -m recent --name SSH --update --seconds 300 --hitcount 10 -m limit --limit 1/second --limit-burst 100 -j LOG --log-prefix "iptables[ssh-brute-force]: " -iptables -A SSH_BRUTE_FORCE_MITIGATION -m recent --name SSH --update --seconds 300 --hitcount 10 -j DROP -iptables -A SSH_BRUTE_FORCE_MITIGATION -j ACCEPT -iptables -A INPUT -i lo -j ACCEPT -iptables -A INPUT -p tcp --dport 22 --syn -m conntrack --ctstate NEW -j SSH_BRUTE_FORCE_MITIGATION -iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT -iptables -A OUTPUT -o lo -j ACCEPT -iptables -A OUTPUT -p tcp --dport 53 -m state --state NEW -j ACCEPT -iptables -A OUTPUT -p udp --dport 53 -m state --state NEW -j ACCEPT -iptables -A OUTPUT -p tcp --dport 80 -m state --state NEW -j ACCEPT -iptables -A OUTPUT -p udp --dport 123 -m state --state NEW -j ACCEPT -iptables -A OUTPUT -p tcp --dport 443 -m state --state NEW -j ACCEPT -iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT -iptables -P FORWARD DROP -iptables -P INPUT DROP -iptables -P OUTPUT DROP +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 -ip6tables -P FORWARD DROP -ip6tables -P INPUT DROP -ip6tables -P OUTPUT DROP +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 -ip6tables -A INPUT -i lo -j ACCEPT -ip6tables -A INPUT -p ipv6-icmp --icmpv6-type destination-unreachable -j ACCEPT -ip6tables -A INPUT -p ipv6-icmp --icmpv6-type packet-too-big -j ACCEPT -ip6tables -A INPUT -p ipv6-icmp --icmpv6-type time-exceeded -j ACCEPT -ip6tables -A INPUT -p ipv6-icmp --icmpv6-type parameter-problem -j ACCEPT -ip6tables -A INPUT -p ipv6-icmp --icmpv6-type router-advertisement -m hl --hl-eq 255 -j ACCEPT -ip6tables -A INPUT -p ipv6-icmp --icmpv6-type neighbor-solicitation -m hl --hl-eq 255 -j ACCEPT -ip6tables -A INPUT -p ipv6-icmp --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT -ip6tables -A INPUT -p ipv6-icmp --icmpv6-type redirect -m hl --hl-eq 255 -j ACCEPT -ip6tables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT -ip6tables -A OUTPUT -o lo -j ACCEPT -ip6tables -A OUTPUT -p ipv6-icmp --icmpv6-type destination-unreachable -j ACCEPT -ip6tables -A OUTPUT -p ipv6-icmp --icmpv6-type packet-too-big -j ACCEPT -ip6tables -A OUTPUT -p ipv6-icmp --icmpv6-type time-exceeded -j ACCEPT -ip6tables -A OUTPUT -p ipv6-icmp --icmpv6-type parameter-problem -j ACCEPT -ip6tables -A OUTPUT -p ipv6-icmp --icmpv6-type router-solicitation -m hl --hl-eq 255 -j ACCEPT -ip6tables -A OUTPUT -p ipv6-icmp --icmpv6-type neighbour-solicitation -m hl --hl-eq 255 -j ACCEPT -ip6tables -A OUTPUT -p ipv6-icmp --icmpv6-type neighbour-advertisement -m hl --hl-eq 255 -j ACCEPT -ip6tables -A OUTPUT -p tcp --dport 53 -m state --state NEW -j ACCEPT -ip6tables -A OUTPUT -p udp --dport 53 -m state --state NEW -j ACCEPT -ip6tables -A OUTPUT -p tcp --dport 80 -m state --state NEW -j ACCEPT -ip6tables -A OUTPUT -p udp --dport 123 -m state --state NEW -j ACCEPT -ip6tables -A OUTPUT -p tcp --dport 443 -m state --state NEW -j ACCEPT -ip6tables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT -ip6tables -P FORWARD DROP -ip6tables -P INPUT DROP -ip6tables -P OUTPUT DROP +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 29: log out and log in to confirm firewall didn’t block SSH +### Step 22: log out and log in to confirm firewall is not blocking SSH #### Log out @@ -445,23 +348,59 @@ exit #### Log in -Replace `10.0.1.248` with IP of Raspberry Pi. +Replace `10.0.1.181` with IP of Raspberry Pi. ```shell -ssh pi@10.0.1.248 -i ~/.ssh/pi +ssh pi@10.0.1.181 -i ~/.ssh/pi ``` -### Step 30: switch to root +### Step 23: switch to root ```shell sudo su - ``` -### Step 31: make firewall rules persistent +### Step 24: make firewall rules persistent ```shell -iptables-save > /etc/iptables/rules.v4 -ip6tables-save > /etc/iptables/rules.v6 +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 system + +#### Update APT index + +```shell +apt update +``` + +#### Upgrade packages + +```shell +apt upgrade -y ``` 👍