Secure Headless Raspberry Pi Setup

Boot a Raspberry Pi with no monitor, no keyboard and no insecure credentials!

The beauty of the Raspberry Pi lies in its community support - by being the de facto board for "tinkerers", it's really common that if you have an issue you'll find something on the internet which will help you solve it.

I wanted to write something to contribute to that community a little, so this is a guide to securely setting up your own Pi as a headless server for the home, all without the need to ever attach a monitor or keyboard to the Pi - even during setup!

The nature of a headless server is that you'll need to use the command line to do anything with it, so this guide does assume you're comfortable using a terminal. It's written assuming you're running Linux, and that your Pi will be networked entirely using wired Ethernet; if you want a wireless headless Pi you might need to tweak the attached setup script a little.

I should also say upfront that there are some tools to configure a headless Pi built right into the official tooling which can work great. Note however that that at the time of writing, following the official guide will potentially leave your Pi wide open on boot until you fix some settings; that's what this guide is intended to avoid!

What Needs Changing?

It's easy to say we'll be creating a "secure" system, but what does that mean in practise? First and foremost, we don't want to ever be in a situation where our Pi is connected to the internet with SSH access enabled and with the default user - "pi" - accessible with its default password. Preventing this is our biggest goal.

That said, there are other changes we'll choose to make:

  • Using Mozilla's "modern" recommendations for SSH
  • Changing SSH to use a non-standard port
  • Adding a new sudo user with a custom name
  • Adding a custom hostname
  • Setting up a static IP address and DNS servers ahead-of-time
  • Setting the timezone to UTC and enabling NTP
It's worth pointing out that using a non-standard port doesn't in theory provide a security benefit since attackers can just point their attacks at the new port. However, there are lots of scripts which scan the internet looking for port 22 specifically since it's the most common and by changing the default port we'll stop a few of them from working.

Preparing our Base Image

First and foremost we need to download our copy of Raspberry Pi OS from the official download page. Use the "lite" image, since we're targeting a headless machine, and be sure to verify the SHA-256 hash before unzipping the image. Your filename may differ but the idea is the same:

TOP TIP: Don't delete the zip file yet! If something goes wrong in one of the following steps, you can just delete the unzipped image and run unzip again. It's a free backup!

Next we'll mount the image file directly on our development machine so we can add some setup files. Mounting the image locally can seem slightly complicated, but it's worth the effort! We'll broadly be doing the same as is recommended in Azeria's guide to emulating the Raspberry Pi using QEMU.

We need to find the start point of the ext4 filesystem part of the image; that means finding the second value in the "start" column using fdisk -l and multiplying it by 512. Here's a 1 liner which works at least with fdisk 2.35.2 for calculating the value we need and storing it:

let imgstart=$(fdisk -l 2020-05-27-raspios-buster-lite-armhf.img -o device,start | tail -1 | awk '{print ($2 * 512)}')

After we have the start point, we can mount the image:

sudo mkdir -p /mnt/sd
sudo mount -o loop,offset=$imgstart 2020-05-27-raspios-buster-lite-armhf.img /mnt/sd

Now we've mounted our base image, we can add our files for bootstrapping and then unmount the image:

  1. authorized_keys, the list of SSH keys which will be allowed to log in
  2. - a bash script which will make various changes to the system including securing SSH and creating a new user

You can get from this blog's sister repo. It also includes a script - - which takes your GitHub username as input, retrieves the SSH keys from your GitHub account and saves them as "authorized_keys".

git clone [email protected]:SgtCoDFish/winfra-bootstrap.git
cd winfra-bootstrap
./ <github-username-here>
sudo rm -f /mnt/sd/etc/init.d/resize2fs_once
sudo mkdir -p /mnt/sd/etc/winfra-bootstrap
sudo cp authorized_keys /mnt/sd/etc/winfra-bootstrap/
sudo umount /mnt/sd

Emulating the Pi

Our next step might seem a little unusual, but has some logic behind it: we emulate a Raspberry Pi using the bootstrap image we've just created, and run inside the emulator. The reasoning behind using an emulator is twofold:

  • we guarantee that when we start the SSH server on the actual Pi, we don't have password log in enabled with the "pi" user present with a default password
  • we can enable the SSH server so it starts on boot, allowing us to SSH in directly without us needing to ever physically attach a keyboard and monitor to the Pi

We proceed again similarly to Azeria's guide for emulating a Pi.

First we need to install QEMU with support for ARM - this varies by distro and I can't cover them all; for Arch Linux you'll want qemu-headless-arch-extra - basically, whatever provides the command qemu-system-arm

Next, we've got to download a kernel and a dtb file which we need to pass into QEMU; these are available in dhruvvyas90/qemu-rpi-kernel. Be sure to get the kernel and dtb files which reference "buster" - they need to match the version of Raspberry Pi OS we're using.

Next, we can actually run the emulated Pi. You might need to change the "-hda" parameter to match the name of your img file, and possibly the name of the "-dtb" or "-kernel" parameters.

qemu-system-arm \
	-M versatilepb \
	-cpu arm1176 \
	-m 256 \
	-hda 2020-05-27-raspios-buster-lite-armhf.img \
	-dtb versatile-pb-buster.dtb \
	-kernel kernel-qemu-4.19.50-buster \
	-append 'root=/dev/sda2 panic=1' \
	-serial stdio \

This will drop us into our emulated system, and after the brief startup process we'll be able to log in using the username "pi" and the password "raspberry". Then we can run our setup commands inside the emulated Pi:

CAUTION: uses Google and Cloudflare's DNS servers. If you're uncomfortable with that you'll need to change the value of $DNSSERVERS now.

sudo /etc/winfra-bootstrap/ myuser

The arguments you pass will, of course, vary based on your local network. Whatever you pass, the script will force you to set a new password for "pi", and will make various changes which are detailed elsewhere in this post. In addition, all of these steps are documented in the script.

After the script finishes run sudo poweroff and wait for the emulated Pi to shut down.

Burn and Boot

Finally, burn the img file onto an SD card the same way you normally would for any Pi image. I use dd:

sudo dd if=2020-05-27-raspios-buster-lite-armhf.img of=/dev/mmcblk0 conv=sync,noerror status=progress bs=1k

(There's a problem with my SD card reader that forces me to use small values for "bs" - you might be able to use larger values for faster transfers)

Put the SD card into your pi, plug in the network cable and power it on; it'll take a while to come fully online but you should be able to SSH into your pi using the static IP and username you configured, e.g. ssh -p5541 [email protected]

Now you just need to run sudo ./ to delete the pi user and to resize the root FS - if you don't resize, you'll likely run out of disk space on your root FS very quickly.

You're all set up and you've avoided:

  1. Having SSH enabled for any period of time with the default user + password combination
  2. Having to connect a monitor any keyboard for any kind of setup