indexpost archiveatom feed syndication feed icon

System Administration, Isolation

2022-01-30

I have been thinking about how to run different internet-facing services in a way that is secure without being too onerous. This got me reading more about the capabilities built into the operating system.

An Example

I would like to run an SMTP server that at least appears to be running on port 25, so that standard mail clients (basically gmail and the likes) can talk to it. I have run SMTP servers on non-standard ports in the past and it proves a real hurdle (if not a total barrier) for most mail clients.

While I won't be running a traditional mail server I am running a networked program that speaks SMTP. The simplest example to start testing things out with is a little debugging SMTP server that listens on the specified host and port and prints any messages received to the console:

#!/usr/bin/tclsh                                                                                                                                                                           
package require smtpd

proc deliver {sender recipients data} {
    set mail "FROM $sender\nTO $recipients\nTIME [clock format [clock seconds]]\n"
    append mail [join $data "\n"]
    puts $mail
}

smtpd::configure -deliver ::deliver
smtpd::start $HOST $PORT
vwait ::smtpd::stopped

Making It More Secure

Ports

Reading the documentation for smtpd highlights an issue:

On Unix platforms binding to the SMTP port requires root privileges. I would not recommend running any script-based server as root unless there is some method for dropping root privileges immediately after the socket is bound.

While dropping privileges is an option, I would rather the program never had the privilege in the first place. Pursuing this line of thought led me to consider how else you might accomplish this.

It has been a few years since I did much of anything particularly interesting with my server firewall. I did make the switch in the intervening years from plain iptables to ufw (which is really just a front-end for iptables). For this scenario I want my unprivileged user to run a service that is "listening" on a privileged port - so I'll use a port forward!

For the above program I might select $PORT to be 1025, so a regular user can run it and my SMTP server never has privileges that it needs to drop. This requires adding the following to /etc/ufw/before.rules:

*nat
:PREROUTING ACCEPT [0:0]
-A PREROUTING -p tcp --dport 25 -j REDIRECT --to-port 1025
COMMIT

One minor gotcha to keep in mind: you have to remember to make a firewall rule to allow those ports being used and forwarded to - in my case I default-deny everything so this had me scratching my head for a few moments until I remembered to ufw allow 1025

User Permissions

Getting a non-privileged user running the server does a lot to improve the attack surface of the program but there is still more low hanging fruit. Most of these kinds of programs are hosted on a machine with numerous different services. As a result there are a variety of different users and configuration lying around. I could go through and clean up everything but frankly that does little to keep things tidy in the future.

Instead of making a user per service and locking down the file system in an ad hoc manner I would like to run the program as a user without any particular permissions. systemd includes the DynamicUser directive for exactly this sort of scenario and can create a new user on the fly each time the service starts. Additionally DynamicUser:

I additionally use the InaccessiblePaths property to disallow any access to a few different directories. In order to save program data in a read-only filesystem I carve out a single persistent data directory for the dynamic user with the StateDirectory property.

All told, the following is one way to run such a service at least while still prototyping. I will eventually write it into a service file in order to add things like automatic restarts:

$ sudo systemd-run -G -t \
      -p DynamicUser=yes \
      -p StateDirectory=smtp-receiver \
      -p InaccessiblePaths=/home \
      -p InaccessiblePaths=/var/log \
      -p InaccessiblePaths=/etc/letsencrypt \
    /usr/local/bin/smtpd-server

As a nice bonus, I've tried sharing the StateDirectory across multiple services (thinking about the possibility of delineating readers and writers to a database) and things worked without issue.

At some point I should probably get around to doing the full container bit and try out systemd-nspawn. I think there is more work involved in setting up the filesystem than I am quite ready for, to say nothing of the networking configuration involved. Until I muster the energy for all of that I'm happy enough to have improved the state of things with a minimum of effort.