I appreciate OpenBSD for being reliable and unbloated. Also I get a warm fuzzy feeling knowing certain things are done correctly. For instance, when a service runs without unneeded priviliges and in a chroot “jail.”

OpenBSD services generally run this way. But occasionally I want to run a service that is not yet packaged for the OS. Lately for me, most of those services are written in go. Because golang binaries are statically linked, it’s quite straightfoward to set up a chroot environment for them. Let’s take a look at how this is done.

In this post I’ll use Miniflux as an example go program. Keep in mind that any statically linked web service would work the same.

And I’ll use OpenBSD to host a chroot environment as our jail (although pretty much any OS that supports a chroot would do).

Prepare the Executable

I built miniflux for openbsd with…

go generate
GOOS=openbsd GOARCH=amd64 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=205aef5' -X 'github.com/miniflux/miniflux/version.BuildDate=`date +%FT%T%z`'" -o miniflux-openbsd-amd64 main.go

Prepare Chroot Jail

  1. Create User

doas useradd -L daemon -d /var/miniflux -s /sbin/nologin -g =uid _miniflux

  1. Create Directories

doas mkdir -p /var/miniflux/bin doas chown _miniflux:_miniflux /var/miniflux/

  1. Install Executable

Use rsync to move the miniflux executable to /var/miniflux/bin.

Ensure that the executable is statically linked, meaning that no additional dependencies need to be moved to the chroot.

$ ldd /var/miniflux/bin/miniflux-openbsd-amd64
not a dynamic executable

Prepare Database

miniflux requires a postgres database. Install postgres is not already installed.

doas pkg_add postgresql-server postgresql-client
doas -u _postgresql initdb /var/postgresql/data

Edit unix_socket_directories in /var/postgresql/data…

unix_socket_directories = '/tmp,/var/miniflux/tmp'
doas /etc/rc.d/postgresql start


  doas -u _postgresql createuser _miniflux

76 man createdb 77 doas -u _postgresql createdb -O _miniflux miniflux

Caddy (deprecated in favor of miniflux)

We’ll use Caddy as our go program (although pretty much any statically linked service would do). And we’ll use OpenBSD 6.0 to host a chroot environment as our jail (although pretty much any OS that supports a chroot would do).

Prepare Chroot Jail

  1. Create User

    When our service runs, it should run as an unprivileged user. Use adduser (or, useradd) to create a daemon class user. I named mine “caddy” in the example that follows.

  2. Create Directories

    We’ll use /var/jail/caddy as the root of our jail. Our executable will be installed to /var/jail/caddy/bin/caddy (which becomes simply /bin/caddy inside of the chroot environment.)

    # mkdir -p /var/jail/caddy/bin
    # chown -R caddy /var/jail/caddy

Install Executable

  1. Build and Install

    We can avoid installing go or any special tools on our server, if you have another machine available to develop with. Personally, I have an OpenBSD laptop with go 1.8 installed. I built caddy there following normal instructions to build from source:

    $ go get github.com/mholt/caddy/caddy

    Then use rsync (or other means) to copy the compiled executable to the server. Place it in /var/jail/caddy/bin.

  2. Dependencies

    One thing often said of go is that it builds statically linked binaries. You might think those binaries have no dependencies whatsoever. ldd can tell us precisely:

    $ ldd /var/jail/caddy/bin/caddy
    Start            End              Type Open Ref GrpRef Name
    0000000000400000 0000000000e3a000 exe  2    0   0      /var/jail/caddy/bin/caddy
    0000000294092000 00000002944a0000 rlib 0    1   0      /usr/lib/libpthread.so.22.0
    000000027d33e000 000000027d807000 rlib 0    1   0      /usr/lib/libc.so.88.0
    0000000251800000 0000000251800000 rtld 0    1   0      /usr/libexec/ld.so

    The last three lines identify dependencies that need to be copied from the root system to the chroot:

    # pax -rw -u -p e /usr/lib/libpthread.so.22.0 /var/jail/caddy/
    # pax -rw -u -p e /usr/lib/libc.so.88.0 /var/jail/caddy/
    # pax -rw -u -p e /usr/libexec/ld.so /var/jail/caddy/

Configure and Run

A quick way to get caddy running for the first time is with caddyserver examples, which can be downloaded from https://github.com/caddyserver/examples. Install those examples into /var/jail/caddy/examples.

At this point we could (but won’t) run the service outside of jail with:

$ cd /var/jail/caddy/examples/markdown
$ caddy

Here’s how we will run the service, as the caddy user confined to the jail environment:

# chroot -u caddy /var/jail/caddy /bin/caddy -conf /examples/markdown/Caddyfile -root /examples/markdown

Privileged Ports

The examples/markdown/Caddyfile is configured to listen on port 8080. Normally a HTTP server uses port 80, but we are running our service as a user named “caddy” who does not have privilige to open port 80. This is a good thing. Also a good thing is that openbsd has a nice tool that can redirect packets between ports.

Try this line in /etc/pf.conf:

# redirect port 80 to caddy listening on port 8080.
match in on egress proto tcp from any to (self) port  80 rdr-to port 8080

Reload pf’s configuration:

# pfctl -f /etc/pf.conf 

Now, you should be able to query your server on the default HTTP port. The same technique will work for HTTP on port 443, if you have configured caddy to serve over TLS.

That’s It

I was pleased to learn how strightforward it is to host golang services in this relatively secure way. I hope sharing these notes proves useful to you, if you’re looking to do the same.