██████╗  ██████╗ ██╗  ██╗ █████╗ 
╚════██╗██╔═══██╗██║  ██║██╔══██╗
 █████╔╝██║   ██║███████║███████║
 ╚═══██╗██║   ██║██╔══██║██╔══██║
██████╔╝╚██████╔╝██║  ██║██║  ██║
╚═════╝  ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝

Welcome to 3OHA, a place for random notes, thoughts, and factoids that I want to share or remember.

1 June 2022

The nobody user

Saltzer and Schroeder wrote in 1975 an enormously influential paper titled The Protection of Information in Computer Systems. Some say this is one of the most cited and least read papers in the history of computer security. What everyone seems to remember about that paper are the eight fundamental principles of computer security ("protection" was the term used back then) that they put forth. In my experience, two of them—separation of privilege and least privilege—are confused very frequently. I recently came across one such error when someone pointed me to the Wikipedia entry on Privilege Separation. This took place while I was discussing the historical context of some special UNIX system accounts, such as nobody and daemon, which incidentally are also wrongly described in Wikipedia.

UID 65534, or -2

Most UNIX systems have an entry in the password file for a user called nobody. This user name, which traditionally has been associated with the user id 65534 (or -2), is not enabled as a user account—i.e., it cannot log in and has no home directory. It does not belong to any privileged groups either. It is also common to find a companion nogroup, which is the group equivalent to the nobody user identifier.

The nobody user is an artifact that exists to be used by the Network File System (NFS) in a very specific situation. For those unfamiliar with it, NFS allows you to access files located in a remote host transparently, pretty much as if they were local objects. This is achieved through a userland client (sometimes supported by a kernel module, others operating entirely in userland) that makes RPC calls to an NFS daemon on the server side. When a user accesses a file provided over NFS, the daemon receives an RPC call that contains the name and handle of the file along with the user UID and GID that is placing the request. The credentials are used in determining access rights, and both UID and GID must be the same on the client and server hosts. Reconciling different UID and GID on the client and server for the same user could be problematic, and that’s why NFS used to rely on a different service (rpc.ugidd) to do the mapping.

If you pause and think about this whole UID/GID mapping between NFS client and server you’ll soon realize that root (UID 0) on one machine will be automatically mapped to root on the other machine. In other words, superusers on a client machine will have superuser access to your exported directories. Whether this may or may not be a security issue is totally up to you, but it is a case that cannot be ignored. That’s exactly why the NFS daemon can be launched with an option called root_squash. If enabled, any NFS access attempt coming from UID 0 will be automatically mapped to an artificial user with UID 65534, historically named nobody, which is an untrusted account. This, and no other, is the fundamental reason why the nobody user account exists.

NFS can also use the nobody account when the credentials provided by the client are not trusted, but the underlying security principle is the same. Incidentally, if you are interested in knowing why NFS has been such a wonderful source of opportunities and headaches (depending on which color you play), I recommend you to start with Why NFS Sucks.

Only recently I learned that there is at least one other use case for the UID 65534. When systemd-nspawn is launched with bind mounts and the --private-users option, the resulting mount points are owned by nobody because the real owners do not exist in the container (see this).

Running daemons with least privilege

The original purpose of nobody is often confused with another famous system account: daemon (id 1). The daemon user id was introduced to give unprivileged daemons (i.e., daemons that do not run as root) restricted access to the system. Running a service as daemon was a strategy to limit the potential harm caused by an attacker who gained control over it, as described for example in the AIX manual. This approach is definitely an application of the principle of least privilege, though the implementation is not very solid here because running multiple daemons under the same UID creates a whole different set of threats—for example, it makes all of them vulnerable if just one of them is compromised. For this reason, more security conscious daemons should run under their own dedicated UID and GID. This not only limits the damage caused by a daemon's misbehavior, but also isolates daemons from each other. In doing so we follow another of Saltzer and Schroeder's principles—least common mechanism, or compartmentalization.

From time to time I come across someone recommending to use nobody to run an unprivileged daemon (the Wikipedia entry is, again, very misleading in this case). This is bad advice, and it’s arguably a worse decision than running it under daemon.

Dropping root

Least privilege also implies that if a subject needs some elevated privilege only to complete a task, those extra privileges should be relinquished immediately on completion of the task. In some cases, a daemon needs to run as root because otherwise it would not be able to complete some specific privileged action, such as binding to low ports or opening a raw socket. A very good practice in such cases is to isolate the code requiring elevated privileges and, once the goal is accomplished, relinquish root privileges and keep running under a different, unprivileged and dedicated UID and GID. This process is known as dropping root and is executed by calling setuid() and setgid(). The major caveat here is that you need to drop the group before the user and not the other way around. Why? Because setgid() must be run with root privileges, so a prior call to setuid() leaves the EUID as nonzero.

What if the process has some supplementary group IDs in addition to its EGID, and some of those supplementary groups have elevated privileges? You can check it by calling getgroups() and then drop those IDs with setgroups(). The latter requires root (so you need to drop supplementary group IDs before dropping root) and it is not POSIX, although it is present in BSD and Linux systems.

Funnily enough, I did not find out about dropping root as a principled way of improving the security of a system service, but as an operational security tactic to avoid overprivileged implants that might be more easily flagged. For reference, the best way of dropping root that I know is the method described in Rule 50 POSC36-C - Observe correct revocation order while relinquishing privileges of the SEI CERT C Coding Standard.

Privilege separation

The idea of separating privileged from non-privileged parts in network services (and giving low privileges to the latter) has been present in UNIX for a long time. The good old superserver inetd does exactly this: you can specify in inetd.conf an account under which the service process is run. The newer systemd that is found in some Linux distributions also follows the same principles—open the socket, bind it to a port, call listen() and accept() as root, and then spawn a service process under a different unprivileged account.

Contrary to popular belief, the above is not an application of the privilege separation principle. The privilege separation principle simply states that granting access based on the satisfaction of two or more independent conditions is better than relying on just one condition. Meeting multiple conditions raises the bar for the attacker because it requires them to compromise more system components to succeed. This is the underlying idea of many robust designs, including the two-person rule that governs the launch of nuclear weapons in some countries and the use of two factors of authentication.

© 2022 Juan Tapiador