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.
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
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
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
:
/tmp
directory to prevent it
from reading temporary files from other programs
SUID
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.