___________________________________________
title: Gemini Capsule in a FreeBSD Jail running on a RaspberryPi
tags: freebsd hack gemini 100daystooffload
date: 2021-04-25
___________________________________________

Intro

With the recent release of FreeBSD 13, I wanted to test it out on a
spare RaspberryPi 3 that was part of my old Kubernetes cluster.

  [recent release of FreeBSD 13]: https://www.freebsd.org/releases/13.0R/announce/
  [old Kubernetes cluster]: https://www.ecliptik.com/Raspberry-Pi-Kubernetes-Cluster/

In particular, FreeBSD Jails have always interested me, although I’ve
never used them in practice. Over the years I’ve managed operating
system virtualization through Solaris Zones and Docker containers, and
Jails seem like and good middle ground between the two - easier to
manage than zones and closer to the OS than Docker.

  [FreeBSD Jails]: https://docs.freebsd.org/en/books/handbook/jails/
  [operating system virtualization]: https://en.wikipedia.org/wiki/OS-level_virtualization

I also want to run my own Gemini capsule locally to use some of the
features that my other hosted capsules don’t have (like SCGI/CGI) and
setting up a capsule in a Jail is a good way to learn both at the same
time.

  [Gemini]: https://gemini.circumlunar.space

Installing FreeBSD on a RaspberryPi

Installing FreeBSD on a RaspberryPi is relatively easy, downloading
the FreeBSD 13 RPI image and booting from the SD card to get started.
Everything will come up automatically, and you can ssh in with the
default user:pass of freebsd:freebsd.

  [downloading the FreeBSD 13 RPI image]: https://download.freebsd.org/ftp/releases/arm64/aarch64/ISO-IMAGES/13.0/

A few post-install things I did to secure the host more,

-   Add another non-root user other than freebsd
-   Disable password logins and require ssh-keys
-   Setup doas for new user
-   Remove the freebsd user with doas rmuser freebsd
-   Set strong root password

  [doas]: https://man.openbsd.org/doas

Setting up NTP

Since the RPI doesn’t have a real-time clock, setting up NTP is
crucial for accurate time, which if not set can cause all sorts of
issues with TLS and other commands.

    # Enabe ntpd
    host$ echo 'ntpd_enable="YES"' | doas tee -a /etc/rc.conf

    # Force sync time
    host$ doas ntpdate pool.ntp.org

    # Start ntpd
    host$ doas service ntpd onestart

Setting up the Jail

Creating the Jail

The Jails guide is straightforward, but contains two different methods
of configuring jails. The built-in jail commands or ezjail. I ended up
using ezjail which seems more robust and featureful.

  [Jails guide]: https://docs.freebsd.org/en/books/handbook/jails/
  [using ezjail]: https://docs.freebsd.org/en/books/handbook/jails/#jails-ezjail

Following the instructions first add the second loopback interface,

    host$ echo 'cloned_interfaces="lo1"' | doas tee -a /etc/rc.conf
    host$ doas service netif cloneup

Then install ezjail and a few other packages we’ll need later on,

    host$ doas pkg install ezjail ca_root_nss openssl
    host$ echo 'ezjail_enable="YES"' | doas tee -a /etc/rc.conf

Create a new jail named thesours, using the new second loopback and a
new LAN IP on the interface em0,

    host$ doas ezjail-admin create thesours 'lo1|127.0.1.1,em0|192.168.7.223'

This installs a FreeBSD 13 (default version is the host version) jail
filesystem in /usr/jails/thesours/ and will take a while to download
and extract.

Once complete, list the new jail,

    host$ doas ezjail-admin list
    STA JID  IP              Hostname                       Root Directory
    --- ---- --------------- ------------------------------ ------------------------
    DR  1    127.0.1.1       thesours                       /usr/jails/thesours
        1    ue0|192.168.7.223

Setting up the Jail

Now that there’s a running jail, connect to it’s console to start
setting it up.

    doas ezjail-admin console thesours

Many of the directories are shared with the basejail and are
immutable, but adding users and packages, configuring services, and
/etc are all independent of the host OS.

Add a new non-root user using adduser, install doas and set up this
user for root privileges. Enabling sshd also allows ssh sessions into
the jail,

    jail$ echo 'sshd_enable="YES"' | doas tee -a /etc/rc.conf

Setting up a Gemini Capsule

Now that the jail is setup, the next step is installing and
configuring the Gemini server Molly Brown, which has a lot of features
such as ~ support for user gemini folders and SCGI/CGI scripting.

  [Molly Brown]: https://tildegit.org/solderpunk/molly-brown

Building Molly Brown

Molly Brown requires go, which was built in the host and not the jail
in order to keep jail packages to a minimum.

    host$ doas pkg install go

Build Molly Brown,

    host$ mkdir ~/go
    host$ export GOPATH=~/go
    host$ go get tildegit.org/solderpunk/molly-brown

Copy the resulting ~/go/bin/molly-brown binary to the jail,

    host$ doas cp molly-brown /usr/jails/thesours/usr/local/sbin/

Also create the TLS certs that molly brown will require later, and
copy them to the jail,

    host$ doas mkdir -p /usr/jails/thesours/etc/ssl/gemini/
    host$ cd /usr/jails/thesours/etc/ssl/gemini/
    host$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 1826 -nodes -subj '/CN=thesours.ecliptik.com'

Go back into the jail and setup a few configurations for Molly Brown
with the following assumptions,

-   Config in /etc/molly.conf
-   Logs in /var/log/molly
-   TLS certs in /usr/jails/thesours/etc/ssl/gemini
-   Document root in /var/gemini/
-   Run as daemon

Create the required paths, create/copy files and set the proper
permissions for daemon,

    jail$ doas mkdir -p /var/log/molly /var/gemini/
    jail$ doas chown -R daemon:daemon /var/log/molly /usr/jails/thesours/etc/ssl/gemini /var/gemini/

Molly Brown Configuration

Create configuration in /etc/molly.conf,

    ## Molly basic settings
    Port = 1965
    Hostname = "thesours.ecliptik.com"
    CertPath = "/etc/ssl/gemini/cert.pem"
    KeyPath = "/etc/ssl/gemini/key.pem"
    DocBase = "/var/gemini/"
    HomeDocBase = "users"
    GeminiExt = "gmi"
    DefaultLang = "en"
    AccessLog = "/var/log/molly/access.log"
    ErrorLog = "/var/log/molly/error.log"

Creating a Molly Brown Service

Create etc/rc.d/molly to manage the service and have it start when the
jail does. It will run as the daemon user to improve security.

    #!/bin/sh
    #
    # $FreeBSD$
    #

    # PROVIDE: molly
    # REQUIRE: networking
    # KEYWORD: shutdown

    . /etc/rc.subr

    name="molly"
    desc="Gemini Protocol daemon"
    rcvar="molly_enable"
    command="/usr/local/sbin/molly-brown"
    command_args="-c /etc/molly.conf"
    molly_brown_user="daemon"
    pidfile="/var/run/${name}.pid"
    required_files="/etc/molly.conf"

    start_cmd="molly_start"
    stop_cmd="molly_stop"
    status_cmd="molly_status"

    molly_start() {
            /usr/sbin/daemon -P ${pidfile} -r -f -u $molly_brown_user $command
    }

    molly_stop() {
            if [ -e "${pidfile}" ]; then
                    kill -s TERM `cat ${pidfile}`
            else
                    echo "${name} is not running"
            fi

    }

    molly_status() {
            if [ -e "${pidfile}" ]; then
                    echo "${name} is running as pid `cat ${pidfile}`"
            else
                    echo "${name} is not running"
            fi
    }

    load_rc_config $name
    run_rc_command "$1"

Enable the service,

    jail$ echo 'molly_enable="YES"' | doas tee -a /etc/rc.conf

Add a default /var/gemini/index.gmi file with some basic gemtext and
start the molly service,

    jail$ doas service molly start

Running Example

The gemini capsule gemini://thesours.ecliptik.com is running Molly
Brown in a FreeBSD jail.

  [gemini://thesours.ecliptik.com]: gemini://thesours.ecliptik.com