Sandboxing with systemd
I've worked with systemd for a long time, but I've never really understood it. I know how to start and stop, enable and disable, and check the status and logs of a service that's running. I'll often create very simple services for running my own code.
I'm also aware of some the controversy and debate surrounding its
integration into distributions like Debian. From my perspective, I
never really understood the big deal. It seemed like a small
change. Instead of running /etc/init.d
I would be running something
like systemctl
. I suppose that's a very naive point of view, but I
feel pretty pragmatic when it comes to Linux.
Getting into Gemini has given me some excuses to get a deeper understanding of Linux, TLS, and even some basic networking. After writing my extremely simple Gemini server (Melchior), I wanted to figure out how to deploy it. My initial pass was to run it as a systemd service. The Gemini server itself is supposed to be secure and avoid things like directory traversal. I added some basic checks for this type of vulnerability, but I've still been nervous about my deployment. Hardening my Gemini environment has been a useful playground. How do I protect my server and network if it turns out Melchior has a serious vulnerability?
I considered moving Melchior into a Docker container. A lot of the benefits of Docker would help alleviate my security fears. The downside is that docker is a big / complicated beast in its own right. I don't really want to run / administer Docker if I can avoid it. I started searching around and ended up finding a very helpful talk from Lennart Poettering, the creator of systemd: Lennart Poettering: "Containers without a Container Manager, with systemd"
The subject of the talk was exactly what I was looking for. Containers without a container manager.
After watching the first half of the talk, I paused and made some
changes to my systemd setup for Melchior. Most importantly, I added
the RootDirectory=
option. This is very similar to doing a
chroot
. If I get it setup properly, even if Melchior is setup
poorly, I can avoid leaking information via directory traversal. My
/var/gemini
directory like this:
One risk here is that my certificate and key directory would still be accessible via directory traversal possibly, but I'm not super worried about that.
In addition to RootDirectory=
there is a option RootImage=
which
could work a lot like an image. We could use a tool like mkosi to
bundle all of our folders and assets together and deploy a full
image. In my case, that didn't seem as convenient.
The systemd config that I'm running now is on Github. The changes were very small.
The only "gotcha" that I ran into here was that Melchior wouldn't run
without being compiled statically. The behavior is very similar to
running a go binary in a FROM scratch
image in Docker. Here is the
command I'm using now:
go build -ldflags "-linkmode external -extldflags -static" -a melchior.go
After rebuilding Melchior this way, everything worked prefectly (I think).
I'm realizing how little I really know about systemd. Even for this simple Gemini server, I feel like there's a lot more that I could leverage. I'm looking forward to learning even more.