How depressing. These essays were supposed to turn up reasonably frequently, but I got hung up on the original plan for essay 3 and it still isn’t quite writable. In essay 2 I argued that it’s very hard indeed to implement strong partitions within a program running on Unix, even when it runs as multiple processes. Essay 3 was supposed to be a more nuanced argument about security engineering, and therefore more positive about partitioning, but it’s hard to be opinionated when you don’t have a definite answer.
So let’s go back to basics and take a look at another public interface of an MTA. In essay 1 I talked about local message submission, in which I argued that every current MTA does it wrong. This time I’m going to look at the local delivery agent - LDA - and again, I think every current MTA does it wrong.
The “almost” arises from the fact that some parts of the process may be run under the identity of the destination user, in particular the process of writing the message to a mailbox or delivering it to a program. Interpreting .forward files may also be done as the user, which is particularly important if they contain complicated filtering scripts as for Exim or Sieve. But still, the end result of routing the address is a filename or program, and you have to trust the MTA to get it right.
Most interestingly, it adds a couple of security boundaries. The first one occurs between deciding that an address is local and the whole process of local delivery. In this case the qmail-lspawn daemon has the necessary privilege and is responsible for switching to the appropriate user and running the program that delivers the message. The second one occurs at a redirection (including aliases and forwards), in which case the message is re-injected into the queue.
Less interestingly, it has its own way of describing aliases and forwards. The .qmail file has a very slightly simpler format than a .forward file, but essentially the same features. It also has an alternative aliases file format which is reminiscent of a password file, and symultaneously less flexible and more redundant than a normal aliases file. Finally, it falls back to looking at a special “alias” user’s .qmail files.
To some extent, this reduces and divides up the complexity of local delivery. But not much. It reduces it by using simpler file formats (“don’t parse”), but that doesn’t achieve much, since the .qmail processing is done under the identity of the user who created the file. It divides it by separating alias handling from forward handling, but this means the whole thing requires four programs and extra re-injections.
Essentially all of the traditional local delivery logic is implemented inside Postfix’s “local” daemon. This implies that it must do a lot of work as root, in particular alias processing: it has to work out which local user the alias redirects to before it can drop privileges. The manual’s “security” section is remarkably coy about the details of this, saying only that it is “security sensitive” (duh).
What is worse is that it has to include a lot of support for Postfix’s infrastructure: It has lots of options in the configuration file which it must therefore parse, and it also has to support all of Postfix’s database types. Some of these can be fiendishly complicated: LDAP, SQL, embedded databases, regex matches, sockets, etc. etc. As a result it is one of the largest parts of Postfix. Are you really happy running all this as root, even for a “secure” MTA?
This mention of other database types hints at the complexity quagmire that email can be. Unix MTAs often have to do the interoperability between the Internet and “enterprise messaging systems” - which often means LDAP is necessary. And they have to do virtual hosting of various kinds: a virtual domain may be just an aliases file without local users, or it may be a pile of local mailboxes with a local user per domain, or it may be more complicated.
Postfix’s LDA has the traditional Unix logic hard-coded. It can’t do virtual domains. Postfix implements alias-based virtual domains as part of its main routing logic (but trad aliases are not done this way - huh?), and it has a separate parallel local delivery daemon for domains with mailboxes which duplicates much of the local(8) code (double-huh?).
We want all the complicated routing logic to be performed by the main MTA process, running as a non-privileged user (with only access to the email queue and anciliary databases). The LDA does not worry about .forward or /etc/aliases. It does not link to database libraries.
However, we cannot trust the result of this routing: if we did, the MTA could alter any file on the system, so it could gain root privilege by appending an appropriate “message” to the password file. The LDA must have some way of verifying that the MTA’s instructions are legitimate.
For Sendmail and Exim, the LDA necessarily trusts the MTA, because the MTA already is root. For qmail and Postfix, the LDA doesn’t need to verify anything because it does the work itself, either as the destination user or as root.
The LDA just gets an instruction from the MTA: deliver this message to this file as this user. The LDA switches to the user, checks that the file is listed as being OK, checks filesystem permissions, and writes the message. It then sends back a success or failure indication to the MTA. Really simple. Works for trad Unix delivery and virtual delivery and weird delivery.
Delivery to programs is similar - and in fact, here there are precedents for an anciliary permitted-destinations list. The Sendmail restricted shell, smrsh (which is unrelated to the James Bond villains), has a list of commands that it is willing to run. This is also a bit like userv.
One problem - easily solved - is that a simple list of destinations is not enough. You need some pattern matching, such as “anything under this directory”, or “this file in my home directory, wherever that is”. You want a system-wide default so that users don’t have to be bothered with this detail, but you still want clever/demanding users to be able to refine it - e.g. “I want my inbox in ~/mailbox so never deliver to the default /var/mail/me”. (This is still much simpler than .foward syntax.) You also want a tool to auto-generate the allowed-paths file from a .forward file or aliases file, so the common case remains easy to use.
We’d also like to be able to handle redirections from more complicated filtering scripts in this efficient way, but at the same time we would prefer these scripts (written by untrusted users) to be run with the privileges of their authors, not with the privileges of the MTA. A small extension to the MTA->LDA protocol allows the MTA to ask for a script to be run and to get the results back: redirections, rejections, deliveries, etc. It can even do this when verifying recipient addresses at SMTP time, so that scripted rejections don’t turn into accept-then-bounce.
Exim does the redirection short-cut, but it lost the filter verification short-cut when its security was improved for version 4. It’s nice that my LDA design allows these performance and anti-spam features whilst remaining secure.