FreeBSD quark server on the Raspberry Pi

  1. Get a preinstalled image
  2. How to find your SD card’s device
  3. Get a shell into the installed environment
  4. Setting up the root and freebsd users
  5. Serving a directory with quark
  6. Daemonizing quark
  7. A word on SSH security
  8. Public key authentication
  9. SSH configuration
  10. Packet filtering
  11. Setting CPU frequency limits
  12. Hardening your specific application, and liftoff

Get a preinstalled image

Download an SD card image from www.freebsd.org. At the time of writing, it was available under the Get FreeBSD portion, in the directory where the aarch64 releases are located. Look for the image that has RPI in its name.

Working from preinstalled images is common in this scenario. The reason for this mainly lies in only having one bootable SD card slot, which means you would have to plug in another SD card reader to install to (then swap them around once it’s ready to boot), or an SSD/HDD the pi can reliably power (in which case the hassle is in doing the firmware update which makes booting possible from the microUSB port).

How to find your SD card’s device

BEWARE, for all who don’t verify their device IDs before using it as an output of dd are subject to the whims of fate. Make sure that when you pass /dev/sdX to any command that writes to it, it is exactly the device you believe it to be, so that you don’t accidentally wipe your system.

To find the name of your SD card, you can either list your block devices with lsblk or devices with ls /dev, before and after you plug in your SD card. Compare their outputs, and choose the new name in its topmost/simplest form (not any partitions, but the device itself).

Get a shell into the installed environment

One method is to run the OS on the raspberry itself (as opposed to running it inside an emulator such as QEMU). In this case, we’ll need to copy the image to bootable media. As always, be careful when using /dev/sdX as the destination of a write operation (a safer way would be to use disk IDs directly).

# dd if=<preinstalled>.img of=/dev/sdX status=progress

If you have a .img.xz compressed archive, you can either extract it to an image with xz --decompress <your-image>.img.xz, or you can pipe the output of xzcat into dd, like so.

# xzcat <your-image>.img.xz | sudo dd of=/dev/<your-device> status=progress

Grab a cup of your beverage of choice while this completes, gaze out your window, or pet your pet. Contemplate the marvelous microlith, the molten glass into which you’re etching this vast array of symbols.

After completion, plug in your device to your target hardware, and boot it. To get access, you can either hook up your own peripherals, or you could do the setup completely headless (over an ethernet cable plugged into your router, for example).

By default, the configuration should start listening for SSH connections (after a minute-long boot, in my case). Next, we should try to find the machine on the local network with the nmap network exploration tool. The address block 192.168.0.0/24 refers to the 256-wide sub-block of 192.168.0.0/16 defined as “private use” in the IANA IPv4 Special-Purpose Address Registry.

# nmap -sn 192.168.0.0/24
    -sn (No port scan)
        This option tells Nmap not to do a port scan after host discovery, and only print out the available hosts that responded to the host discovery probes.

I’ve read that the Pi boards should have a common MAC address prefix, which could help with determining their assigned IP address, but mine did not happen to have the prefix that I would have expected. To show MAC addresses, nmap needs to run as root, and your machines have to be on the same subnet. In my case, I found the target address based on the (Raspberry Pi Trading) bit present on the same line.

Regardless, you don’t necessarily need nmap to find devices on the local network. If you have to resort to sticks and stones, you could also try to ping every address on the local network (with a shell script, possibly) to see if they respond, and then attempting to log in if they do.

Once you’ve determined the address in the form 192.168.0.<0-255>, you should be able ssh onto the machine with the default non-root user freebsd with the same password.

$ ssh freebsd@192.168.0.<0-255>

Setting up the root and freebsd users

Change the password for freebsd.

$ passwd

Afterwards, you can become root with su (with the same root password).

$ su

Change the password for root.

# passwd

Serving a directory with quark

Install git to be able to clone repositories.

# pkg install git

Clone the quark repository.

$ git clone git://git.suckless.org/quark

Build quark, and install it as root.

$ cd quark
$ make all
# make install

Under your home directory, create a directory for quark to serve, and an index.html with some test text of your choosing inside it.

$ cd ~
$ mkdir <your-site>
$ vi <your-site>/index.html

Start quark as root. This will run it interactively; you can exit it with ^C (ctrl-c).

# /usr/local/bin/quark -h 0.0.0.0 -p 80 -d /home/freebsd/<your-site>

On your usual machine, use curl to fetch your site, and bask in the beauty of the text you just sent yourself.

$ curl 192.168.0.<0-255>

Daemonizing quark

To start quark as part of the boot process, we’ll use the rc and daemon utilities. Check out their manual pages with man <name>.

The rc utility is the command script which controls the automatic boot process after being called by init.
The daemon utility detaches itself from the controlling terminal and executes the program specified by its arguments.

Create a script, /usr/local/etc/rc.d/quark with the following contents.

#!/bin/sh

# PROVIDE: quark
# REQUIRE: LOGIN

. /etc/rc.subr

name="quark"
rcvar=quark_enable

command="/usr/sbin/daemon"
command_args="-r /usr/local/bin/${name} -h 0.0.0.0 -p 80 -d /home/freebsd/<your-site>"

load_rc_config $name
run_rc_command "$1"

Make the script executable.

# chmod +x /usr/local/etc/rc.d/quark

For a decent introduction to rc, check out Practical rc.d scripting in BSD. Below, I’ll briefly mention the main points of interest in this script.

PROVIDE is an rcorder keyword, and its line is meant to signify that this script provides a condition named quark.

LOGIN refers to a placeholder script that, among others, helps with ordering operations.

It’s sourcing /etc/rc.subr, where a number of sh functions are defined for an rc.d script to use.

The command executed by this script is /usr/sbin/daemon.

-r, --restart
    Supervise and restart the program after a one-second delay if it has been terminated.

Enable the newly created quark service to be started at boot time, with the same rcvar name specified in the script by adding a new line to /etc/rc.conf containing quark_enable="YES".

# vi /etc/rc.conf

Start the quark service.

# service quark start

You can restart or stop it with those exact words as arguments.

Check the status of the service.

# service quark status

A word on SSH security

When changing SSH configurations, you should ensure that you either don’t test them on a live system, and that you have another way to access your machine, or you could end up locking yourself out otherwise.

If you are unacquainted with the basics of opsec, I strongly recommend that you read up on this section on your own time. In my case, I didn’t realize at first how bad of an idea is leaving a password-authenticated SSH port open on the internet.

There are some mitigations against brute force attacks, such as:

  • Using a port different than 22 to eliminate most unsophisticated worms,
  • Restricting access to known IP addresses,
  • Rate limiting SSH sessions using iptables,
  • Using an IPS (intrusion prevention system) such as fail2ban,
  • Disabling root logins and restricting access of the users that can log in this way.

However, all of these are susceptible to a man-in-the-middle attack (or some kind of DNS hijacking), if you don’t check the SSH trust-on-first-use-style ECDSA key fingerprint carefully every time you first try to connect from another computer. Another problem is that as long as password authentication is enabled, the knocking will forever continue, leeching resources such as bandwidth, disk writes & space. This is why disabling password authentication completely is recommended, switching to keypairs instead.

An SSH key passphrase is a secondary form of security that gives you a little time when your keys are stolen, so this option is still not immune to brute forcing in that case. If your RSA key has a strong passphrase, it might take your attacker a few hours to guess by brute force. That extra time should be enough to log in to any computers you have an account on, delete your old key from the ~/.ssh/authorized_keys file, and add a new key. This shifts the focus of security to the machines you keep your keys on. Note, however, that if your keys are stolen and you are unaware of it, the playing field is essentially reduced to brute-force (or dictionary) attacks again.

Public key authentication

On the host system(s) where you will be connecting to the target system from via SSH you will need to generate a private and public keypair, and then add the public key of the host system to the target system. Generate a key using ssh-keygen, a tool that “generates, manages and converts authentication keys for ssh”. If # ssh-keygen is invoked without any arguments, it will generate an RSA key of 3072 bits with 16 rounds of key derivation when saving the private key. You’ll also need to use a passphrase to protect the key. Look into diceware for an easy way of generating easy-to-remember, cryptographically strong passwords.

Now add the key to the machine’s authorized keys using # ssh-copy-id -i <pubkey> <username>@<hostname>, where <pubkey> is the name of the public key on the host system, and <hostname> is where you originally ssh’d onto the machine, which in my case, was the 192.168.0.<0-255> local address.

To enable public key authentication, add the following to your ssh configuration file.

PubkeyAuthentication yes

If the key was added successfully to the machine (should be visible in ~/ssh/authorized_keys), test it out by attempting to log in using it by # ssh -i <path-to-pubkey> <username>@<hostname>. If it all seems to work, disable password authentication by adding the following to your configuration. This isn’t strictly necessary, because the default seems to be PasswordAuthentication no.

PasswordAuthentication no

After editing, reload the configuration file.

# sh /etc/rc.d/sshd reload

SSH configuration

Edit /etc/ssh/sshd_config, adding the following configuration changes, if you deem them appropriate. Most of the defaults seemed sane, except for password authentication, which is something we’ll disable soon. For now, I would add the Protocol 2 and UsePAM no lines somewhere.

# SSHv2 is usually the default, but it is worth making sure.
Protocol 2
# If you don't need pluggable authentication module, disable it.
UsePAM no

Packet filtering

Enable the pf service to be started at boot time by adding pf_enable="YES" to /etc/rc.conf.

# vi /etc/rc.conf

Create /etc/pf.conf, the packet filter configuration file which will contain rules or definitions that configure the pf packet filtering utility.

# touch /etc/pf.conf

Here’s where things get a bit specific, and where you’ll have to exercise caution and your own judgement to compose your packet filtering rules. Instead of giving you a specific firewall configuration, I would recommend getting acquainted with the OpenBSD user’s guide for pf.

Still, here are some ideas on what to look for while configuring your packet filter for a purpose similar to what’s outlined in this guide:

  • block all traffic by default, and only allow exactly what you need
  • figure out whether return or drop is the appropriate block policy for you
  • allow TCP traffic to port 80 for your webserver, and look into rate-limiting that connection
  • allow TCP traffic to your SSH port from the local network for headless local access
  • consider limiting state table sizes
  • look into: antispooof, synproxy
  • contemplate whether outgoing connections should always pass or only when necessary (e.g. updates)
  • keep some peripherals at hand for when you inevitably lock yourself out of SSH access

Start the packet filtering service.

# service pf start

After changing the packet filtering rules, load them again, and test the expected connections.

# pfctl -f /etc/pf.conf

Setting CPU frequency limits

For my system, running at the maximum frequency for extended periods does not constitute normal operation, as it’s supposed to be a low-traffic HTTP server. Instead of trying to service requests as fast as possible, I would lean towards a slight increase in latency in exchange for not having to worry about wasting power in the pathological case that the cores are pinned for any reason while not performing useful work.

If these highly specific and subjective criteria apply to your system as well, you can configure powerd in your /etc/rc.conf to set the maximum frequency to throttle up to. First, you should check out the possible frequency levels your cores can operate at with sysctl.

$ sysctl dev.cpu.0.freq_levels

Add the following to your /etc/rc.conf, substituting <frequency-in-mhz>.

powerd_enable="YES"
powerd_flags="-M <frequency-in-mhz>"

In my case, the RPi4 I had could operate at 600Mhz and 1500Mhz, so I passed the argument -M 600.

Hardening your specific application, and liftoff

Once you made sure that the general configuration of the server is reasonably safe for your purposes, it might be time to look into the security of whatever service it is going to provide. If you are serving a static website on port 80, that’s fairly simple, but if you’re running something more complicated, such as a git, email, or game server, it might be time to look into the security of that.

I hope you’ve found this resource useful, and that it will be of help in configuring your system for whatever cool purpose you have in mind for it.

created

modified