Securing Gemini Servers

2022-02-23

In what is bound to be a refreshing break from whining about software I thought I might make a short post on some avenues toward securing gemini servers. There were a few recent instances of path traversal bugs exposing peoples' servers — what are some options for mitigating that sort of risk?

Obviously everyone these days uses containers for everything but personally I don't love them as a technology. All of my old skills go out the window and the options are to sacrifice observability or pull in a whole new ecosystem of tooling that goes out of fashion faster than my wardrobe. I like running a boring Linux server using a long term support release, in my case Debian stable.

Without delving into the holy war of "is systemd a bad idea or a worse idea" I will instead point out that it is widely available and mostly maintained. How about using functionality built into the operating system to secure things?

From namespaces to capabilities, permissions to syscalls, the number of knobs available is staggering. Especially if you simply want to run a server and focus more on writing posts, pages, or neat little CGI widgets. To ease some of the burden you can use a tool called `systemd-analyze security` and provide the service you would like assessed.

I happen to use molly-brown for this site. The repository provides a sample systemd service file:

[Unit]
Description=Molly Brown gemini server
After=network.target

[Service]
Type=simple
Restart=always
User=molly
ExecStart=/usr/local/bin/molly-brown -c /etc/molly.conf

[Install]
WantedBy=multi-user.target

It is pretty good! There are certainly worse places to start (like launching the program as your login user inside a screen session, ahem). Running the service with the above leaves loads of functionality available that is not necessary though. There is no need for the molly-brown service to ever modify permissions, or adjust the system clock, or create namespaces, or load kernel modules. The list goes on.

Here is an alternative service file that will disallow all of those things and more:

[Unit]
Description=gemini server

[Service]
User=molly
Restart=always
Type=simple
CapabilityBoundingSet=
RestrictAddressFamilies=AF_INET
ProtectControlGroups=yes
PrivateTmp=yes
PrivateDevices=yes
PrivateUsers=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectClock=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectProc=invisible
ProtectSystem=strict
ProcSubset=pid
RestrictNamespaces=yes
RestrictRealtime=yes
NoNewPrivileges=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
LockPersonality=yes
RestrictSUIDSGID=yes
RemoveIPC=yes
UMask=177
SystemCallFilter=~@clock @debug @module @reboot @privileged @cpu-emulation @obsolete @mount @resources
ReadWritePaths=/var/log/molly/access.log /var/log/molly/error.log 
ExecStart=/opt/molly-brown -c /etc/molly-brown/site.conf

[Install]
WantedBy=multi-user.target

The above is strictly limiting on the available functionality, it disallows entire categories of behavior that could have been used by my gemini server.

Perhaps most relevant to the path traversal issues are the flags ProtectHome, which will make all home directories invisible to the process and PrivateTmp which creates a ... private tmp directory so that the process can't snoop on the system tmp directory. The whole file system has been made read-only via ProtectSystem, except for the two log files which are exempted with ReadWritePaths. There is also the option to add InaccessiblePaths if you want to make some path unnavigable, perhaps /etc/letsencrypt or similar.

In truth the above might be good enough to lock down a server, but can we do better?

I opted for one more layer of protection which is to isolate the entire process inside a chroot, so that even if the process gains read access it will find the entire filesystem basically empty. The way to accomplish this on Debian begins with:

# debootstrap --variant=minbase stable /srv/gemini-jail

To root the systemd service inside the chroot you add the directive `RootDirectory=/srv/gemini-jail`. One tricky piece of configuration requires the ReadWritePaths be absolute from the host system, while the ExecStart call is relative to the chroot. The result looks like this:

[Unit]
Description=a gemini server service

[Service]
RootDirectory=/srv/gemini-jail
User=molly
Restart=always
Type=simple
CapabilityBoundingSet=
RestrictAddressFamilies=AF_INET
ProtectControlGroups=yes
PrivateTmp=yes
PrivateDevices=yes
PrivateUsers=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectClock=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectProc=invisible
ProtectSystem=strict
ProcSubset=pid
RestrictNamespaces=yes
RestrictRealtime=yes
NoNewPrivileges=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
LockPersonality=yes
RestrictSUIDSGID=yes
RemoveIPC=yes
UMask=177
SystemCallFilter=~@clock @debug @module @reboot @privileged @cpu-emulation @obsolete @mount @resources
ReadWritePaths=/srv/gemin-jail/var/log/molly/access.log /srv/gemini-jail/var/log/molly/error.log 
ExecStart=/opt/molly-brown -c /etc/molly-brown/site.conf

[Install]
WantedBy=multi-user.target

Of course you will have to copy the server executable into the chroot along with those pages you will be serving. There is no need to give the service user more permissions though, in my case I gave my login user write permission to /srv/gemini-jail/var/gemini so I can write pages as normal. The systemd service has a read-only view to a chrooted filesystem with no permission to SUID, chroot, change permissions, mount, load kernel modules or pretty much do anything except answer gemini requests.

Enabling _all_ of these options together ends up being a bit "belt and suspenders" but why not? There's no real overhead and it is that much more annoying to hack compared to the next site (maybe they are running the server in a screen¹ session as a user with sudo² access). Remember, you don't have to outrun the bear.

There are still more knobs to tweak if you run CGI applications that enable you to limit CPU usage or the share of memory available to the service. I will have to think up a good example another time.

1

2