16

I am writing an HTTP server daemon in C (there are reasons why), managing it with systemd unit file.

I am rewriting an application designed 20 years ago, around 1995. And the system they use is that they chroot and then setuid, and the standard procedure.

Now in my previous work, the usual policy was that you never ever run any process as root. You create a user/group for it and run from there. Of course, the system did run some things as root, but we could achieve all business logic processing without being root.

Now for the HTTP daemon, I can run it without root if I don't chroot inside the application. So isn't it more secure for the application to never ever run as root?

Isn't it more secure to run it as mydaemon-user from the beginning? Instead of starting it with root, chrooting, then setuid to mydaemon-user?

mur
  • 263
  • 3
    you need to be run as root to use port 80 or 443. otherwise, you can do what tomcat, and other webapp/web-server software does and run on a higher port (say, 8080, 9090, etc) and then use either apache/nginx to proxy the connection to your web server software, or use the system's firewall to NAT/forward the traffic to your webserver from port 80. If you don't need port 80 or 443, or can proxy or forward the connection, then you have no need to run as root, in a chroot or otherwise. – SnakeDoc Jun 01 '18 at 16:08
  • 3
    @SnakeDoc on Linux no longer true. Thanks to capabilities(7). – 0xC0000022L Jun 01 '18 at 20:21
  • @SnakeDoc you can use authbind as well – Abdul Ahad Jun 02 '18 at 07:41

3 Answers3

30

It seems that others have missed your point, which was not reasons why to use changed roots, which of course you clearly already know, nor what else you can do to place limits on dæmons, when you also clearly know about running under the aegides of unprivileged user accounts; but why to do this stuff inside the application. There's actually a fairly on point example of why.

Consider the design of the httpd dæmon program in Daniel J. Bernstein's publicfile package. The first thing that it does is change root to the root directory that it was told to use with a command argument, then drop privileges to the unprivileged user ID and group ID that are passed in two environment variables.

Dæmon management toolsets have dedicated tools for things like changing root directory and dropping to unprivileged user and group IDs. Gerrit Pape's runit has chpst. My nosh toolset has chroot and setuidgid-fromenv. Laurent Bercot's s6 has s6-chroot and s6-setuidgid. Wayne Marshall's Perp has runtool and runuid. And so forth. Indeed, they all have M. Bernstein's own daemontools toolset with setuidgid as an antecedent.

One would think that one could extract the functionality from httpd and use such dedicated tools. Then, as you envision, no part of the server program ever runs with superuser privileges.

The problem is that one as a direct consequence has to do significantly more work to set up the changed root, and this exposes new problems.

With Bernstein httpd as it stands, the only files and directories that are in the root directory tree are ones that are to be published to the world. There is nothing else in the tree at all. Moreover, there is no reason for any executable program image file to exist in that tree.

But move the root directory change out into a chain-loading program (or systemd), and suddenly the program image file for httpd, any shared libraries that it loads, and any special files in /etc, /run, and /dev that the program loader or C runtime library access during program initialization (which you might find quite surprising if you truss/strace a C or C++ program), also have to be present in the changed root. Otherwise httpd cannot be chained to and won't load/run.

Remember that this is a HTTP(S) content server. It can potentially serve up any (world-readable) file in the changed root. This now includes things like your shared libraries, your program loader, and copies of various loader/CRTL configuration files for your operating system. And if by some (accidental) means the content server has access to write stuff, a compromised server can possibly gain write access to the program image for httpd itself, or even your system's program loader. (Remember that you now have two parallel sets of /usr, /lib, /etc, /run, and /dev directories to keep secure.)

None of this is the case where httpd changes root and drops privileges itself.

So you have traded having a small amount of privileged code, that is fairly easy to audit and that runs right at the start of the httpd program, running with superuser privileges; for having a greatly expanded attack surface of files and directories within the changed root.

This is why it is not as simple as doing everything externally to the service program.

Notice that this is nonetheless a bare minimum of functionality within httpd itself. All of the code that does things such as look in the operating system's account database for the user ID and group ID to put into those environment variables in the first place is external to the httpd program, in simple standalone auditable commands such as envuidgid. (And of course it is a UCSPI tool, so it contains none of the code to listen on the relevant TCP port(s) or to accept connections, those being the domain of commands such as tcpserver, tcp-socket-listen, tcp-socket-accept, s6-tcpserver4-socketbinder, s6-tcpserver4d, and so on.)

Further reading

JdeBP
  • 68,745
  • +1, guilty as charged. I found the title and last paragraph ambiguous, and if you're right I missed the point. This answer gives a very practical interpretation. Personally I would explicitly note that having to build the chroot environment like this is an extra effort, that most people would want to avoid. But the 2 security points here are already well made. – sourcejedi Jun 01 '18 at 14:57
  • Another point to remember is that if the server drops privileges before it process any network traffic, then the privileged code is not exposed to any remote exploits. – kasperd Jun 02 '18 at 11:09
7

I think many details of your question could apply equally to avahi-daemon, which I looked at recently. (I might have missed another detail that differs though). Running avahi-daemon in a chroot has many advantages, in case avahi-daemon is compromised. These include:

  1. it cannot read any users home directory and exfiltrate private information.
  2. it cannot exploit bugs in other programs by writing to /tmp. There is at least one whole category of such bugs. E.g. https://www.google.co.uk/search?q=tmp+race+security+bug
  3. it cannot open any unix socket file which is outside the chroot, which other daemons may be listening and reading messages on.

Point 3 could be particularly nice when you're not using dbus or similar... I think avahi-daemon uses dbus, so it makes sure to keep access to the system dbus even from inside the chroot. If you don't need the ability to send messages on the system dbus, denying that ability might be quite a nice security feature.

managing it with systemd unit file

Note that if avahi-daemon was re-written, it could potentially choose to rely on systemd for security, and use e.g. ProtectHome. I proposed a change to avahi-daemon to add these protections as an extra layer, along with some additional protections that are not guaranteed by chroot. You can see the full list of options I proposed here:

https://github.com/lathiat/avahi/pull/181/commits/67a7b10049c58d6afeebdc64ffd2023c5a93d49a

It looks like there are more restrictions which I could have used if avahi-daemon did not use chroot itself, some of which are mentioned in the commit message. I'm not sure how much this applies though.

Note, the protections I used would not have limited the daemon from opening unix socket files (point 3 above).

Another approach would be to use SELinux. However you would kind of be tying your application to that sub-set of Linux distributions. The reason I thought of SELinux positively here, is that SELinux restricts the access that processes have on dbus, in a fine-grained way. For example, I think you could often expect that systemd would not be in the list of bus names that you needed to be able to send messages to :-).

"I was wondering, if using systemd sandboxing more secure than chroot/setuid/umask/..."

Summary: why not both? Let's decode the above a bit :-).

If you think about point 3, using chroot provides more confinement. ProtectHome= and its friends don't even try to be as restrictive as chroot. (For example, none of the named systemd options blacklists /run, where we tend to put unix socket files).

chroot shows that restricting filesystem access can be a very powerful, but not everything on Linux is a file :-). There are systemd options that can restrict other things, that are not files. This is useful if the program is compromised, you can reduce the kernel features available to it, which it might try to exploit a vulnerability in. For example avahi-daemon doesn't need bluetooth sockets and I guess your web server doesn't either :-). So don't give it access to the AF_BLUETOOTH address family. Just whitelist AF_INET, AF_INET6, and maybe AF_UNIX, using the RestrictAddressFamilies= option.

Please read the docs for each option you use. Some options are be more effective in combination with others, and some are not available on all CPU architectures. (Not because the CPU is bad, but because the Linux port for that CPU wasn't as nicely designed. I think).

(There's a general principle here. It's more secure if you can write lists of what you want to allow, not what you want to deny. Like defining a chroot gives you a list of files you're allowed to access, and this more robust than saying you want to block /home).

In principle, you could apply all the same restrictions yourself before setuid(). It's all just code which you could copy from systemd. However, systemd unit options should be significantly easier to write, and since they are in a standard format they should be easier to read and review.

So I can highly recommend just reading through the sandboxing section of man systemd.exec on your target platform. But if you want the most secure design possible, I wouldn't be afraid to try chroot (and then drop root privileges) in your program as well. There is a tradeoff here. Using chroot imposes some constraints on your overall design. If you already have a design that uses chroot, and it seems to do what you need, that sounds pretty great.

sourcejedi
  • 50,249
  • +1 especially for the systemd suggestions. – mattdm Jun 01 '18 at 13:09
  • i learned quite a bit from ur answer, if stack over flow allowed multiple answer i would accept ur also. I was wondering, if using systemd sandboxing more secure than chroot/setuid/umask/... – mur Jun 01 '18 at 16:24
  • @mur glad you liked it :). That is a very natural response to my answer. So I've updated it again, to try and answer your question. – sourcejedi Jun 01 '18 at 19:59
1

If you can rely on systemd, then it is indeed safer (and simpler!) to leave the sandboxing to systemd. (Of course, the application can also detect if it has been launched sandboxed by systemd or not, and sandbox itself if it is still root.) The equivalent of the service you describe would be:

[Service]
ExecStart=/usr/local/bin/mydaemon
User=mydaemon-user
RootDirectory=...

But we don’t have to stop there. systemd can also do a lot of other sandboxing for you – here are some examples:

[Service]
# allocate separate /tmp and /var/tmp for the service
PrivateTmp=yes
# mount / (except for some subdirectories) read-only
ProtectSystem=strict
# empty /home, /root
ProtectHome=yes
# disable setuid and other privilege escalation mechanisms
NoNewPrivileges=yes
# separate network namespace with only loopback device
PrivateNetwork=yes
# only unix domain sockets (no inet, inet6, netlink, …)
RestrictAddressFamilies=AF_UNIX

See man 5 systemd.exec for a lot more directives and more detailed descriptions. If you make your daemon socket-activatable (man 5 systemd.socket), you can even use the network-related options: the service’s only link to the outside world will be the network socket it received from systemd, it will not be able to connect to anything else. If it’s a simple server that only listens on some ports and doesn’t need to connect to other servers, this can be useful. (The file system related options can also make the RootDirectory obsolete, in my opinion, so perhaps you don’t need to bother setting up a new root directory with all the required binaries and libraries anymore.)

Newer systemd versions (since v232) also support DynamicUser=yes, where systemd will automatically allocate the service user for you only for the runtime of the service. This means you don’t have to register a permanent user for the service, and works fine as long as the service doesn’t write to any file system locations other than its StateDirectory, LogsDirectory, and CacheDirectory (which you can also declare in the unit file – see man 5 systemd.exec, again – and which systemd will then manage, taking care to correctly assign them to the dynamic user).