Restricted sendmail command
===========================

A safer sendmail command to send email without passwords, over SSH.

Objective
---------

This command aims at replacing the builtin `sendmail` command which
gives too much privileges to the caller. For example, [Postfix](http://www.postfix.org/)'s
[sendmail(1)](http://www.postfix.org/sendmail.1.html) command can list the mail queue (`-bp`), rehash the
alias database (`-bi`), start a daemon (`-bl`, `-bd`), or flush the
queue (`-q`); all remnants of the old [Sendmail](https://en.wikipedia.org/wiki/Sendmail) binary, which
probably is Turing-complete on its own.

Instead, rsendmail can easily queue mails on a system without giving
any extra privileges to the client. In turn, this makes configuring a
satellite system like a laptop or a workstation as simple as adding an
SSH key to an `authorized_keys` file. That key can then send email,
but *only* send email: no shell access or server management.

This can of course be accomplished by a regular SMTP client, but that
requires passwords, and passwords are weak.

Quickstart
----------

    scp rsendmail.py example.net:/usr/local/bin/rsendmail

Wherever you would call `sendmail`, you can now call this instead:

    ssh example.net rsendmail

See below for instructions on how to add a queue for when you're
offline, restrict the connection to rsendmail, or integrate with
existing MTAs.

Installation
------------

This system is made of two parts:

 * `rsendmail.py` - a wrapper script installed on a remote SSH server
   that restricts the connection to only accepting and relaying mail

 * `sshsendmail.py` - a local [MDA][] that acts as a compatibility shim
   with the remote rsendmail. this part is optional, as you'll see
   below.

 [MDA]: https://en.wikipedia.org/wiki/Mail_delivery_agent

### Basic configuration

The following assumes your relay host is `example.net` and is already
configured to accept SSH connections on a user called `rsendmail`. It
also assumes there's an email `devnull@localhost` that accepts
delivery.

 1. find the `$PATH` on the remote host:

        ssh rsendmail@example.net 'echo $PATH'

 2. install `rsendmail.py` somewhere in your `$PATH` as `rsendmail`:

        scp rsendmail.py rsendmail@example.net:/usr/local/bin/rsendmail

 3. generate an SSH key for rsendmail:

        ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_rsendmail

 4. copy the key to your `authorized_keys` file:

        ( printf 'command="rsendmail",restrict '; cat ~/.ssh/id_ed25519_rsendmail.pub ) | ssh rsendmail@example.net 'cat >> .ssh/authorized_keys'

 5. send a test email:

        printf "Subject: test\n\nThis is a test" | ssh -i ~/.ssh/id_ed25519_rsendmail  rsendmail@example.net rsendmail devnull@localhost

 6. verify the mail was properly delivered and the message content is
    complete. if so, then `rsendmail` is properly configured

Now you can send email, but there are some bits missing. Most tools
will expect a `sendmail` command to be available and you might want to
queue up mails locally to avoid failing when the network is not
available. So you need some sort of wrapper, a [MDA][] to actually
deliver the mail in a standard way. Queuing will be handled by a
[MTA][] that will call the MDA.

 [MTA]: https://en.wikipedia.org/wiki/Message_transfer_agent

MDA configuration
-----------------

So the next step is to setup a local MDA to talk with
`rsendmail`. Here's a quick comparison of the possible configurations
documented below:

| MDA        | Advantages | Disadvantages |
| ---------- | ---------- | ------------- |
| Standalone | Simplest   | Single user, no queue |
| Nullmailer | Minimalist | Unusual standard, reliability concerns |
| Postfix    | Well-known | Queue expires |

Integration with other MTAs are quite possible as well and
documentation to accomplish that is welcome.

### Standalone

The simplest configuration is to use a simple wrapper script for an
MDA, without any other MTA. For example, here is the content of a
possible `sendmail` command:

    #!/bin/sh -e

    exec ssh -i /var/mail/.ssh/id_ed25519 rsendmail@example.net rsendmail "$@"

The above assumes the private key is stored in the `~mail` home
directory. The private key needs to be readable by all callers of the
command, which might be a security issue for multi-user systems. This
also assumes a `rsendmail` user was created on the remote system.

### Nullmailer compatibility

[Nullmailer][] is a "simple relay-only mail transport agent" which
some people use to queue up mails locally when the network is
unavailable. We can't use a simple wrapper like the above because
nullmailer has a non-standard way of passing recipients to MDA. This
is where the `sshsendmail.py` wrapper comes in.

[Nullmailer]: https://untroubled.org/nullmailer/

 1. generate an SSH key for the `mail` user:

        sudo -u mail ssh-keygen -t ed25519

 2. make sure the remote server identity is verified:

        sudo -u mail ssh rsendmail@example.net true

 3. install the `nullmailer` package, version at least 2.0:

        apt install -t buster nullmailer

 4. deploy the MDA wrapper:

        install sshsendmail.py /usr/lib/nullmailer/sshsendmail

 5. add it as a remote in `/etc/nullmailer/remotes`:

        example.net sshsendmail --mta=nullmailer --identity=/var/mail/.ssh/id_ed25519 --user=rsendmail

Again, adapt the `example.net` host and `rsendmail` user to your
configuration.

I have found the wire protocol used by nullmailer to be rather
unusual. It seems to be completely non-standard which was annoying to
deal with. Worse, the above instructions will only work with
Nullmailer 2.x - previous versions had a different protocol which is
not supported here. Furthermore, I have concerns over the reliability
of the software: during tests, nullmailer segfaulted while failing to
handle a bug in `rsendmail`...

### Postfix compatibility

Postfix can talk to a remote `rsendmail` server easily through the
[pipe](http://www.postfix.org/pipe.8.html) service. Here are the steps to configure a Postfix client,
once `rsendmail` is installed on a server and the `authorized_keys` is
setup:

 1. Install Postfix

         apt-get install postfix

 2. configure it as a `satellite` system and use the recommended
    hostname. as a `relayhost`, use the hostname (and username!)
    of the SSH server, e.g. `rsendmail@example.net`, in other words:

        postconf -e 'relayhost=rsendmail@example.net'

 3. configure the `pipe` service in `/etc/postfix/master.cf`:

        rsendmail unix  -       n       n       -       -       pipe
           user=mail argv=ssh ${nexthop} rsendmail -f ${sender} ${recipient}

 4. configure that transport as the default relay:

        postconf -e 'default_transport=rsendmail:'

 5. Make sure the `mail` user can login to the relay server
    automatically and send mail:

        sudo -u mail ssh rsendmail@example.net rsendmail devnull@localhost < /dev/null

 6. The above will ask for host verification. Once that works, reload
    Postfix, which should start relaying mail through the other
    server:

        postfix reload

Note that the above configuration will bounce messages if SSH cannot
reach the remote server. That is because SSH returns non-standard (as
per `sysexits.h`) error codes (i.e. `255` on failure) which Postfix
cannot directly parse. To handle this correctly, the `sshsendmail.py`
wrapper can be installed instead, again in `master.cf`:

    rsendmail unix  -       n       n       -       -       pipe
      user=mail argv=/usr/local/bin/sshsendmail --host ${nexthop} -f ${sender} ${recipient}

Example:

    avr 23 20:38:06 curie postfix/pickup[28657]: 61947125AA4: uid=0 from=<root>
    avr 23 20:38:06 curie postfix/cleanup[28716]: 61947125AA4: message-id=<20180424003806.61947125AA4@curie.example.net>
    avr 23 20:38:06 curie postfix/qmgr[28658]: 61947125AA4: from=<root@curie.example.net>, size=386, nrcpt=1 (queue active)
    avr 23 20:38:06 curie postfix/pipe[28718]: 61947125AA4: to=<anarcat@example.net>, relay=rsendmail, delay=0.49, delays=0.03/0/0/0.46, dsn=2.0.0, status=sent (delivered via rsendmail service (sending message through command: ['sendmail', '-f',
    avr 23 20:38:06 curie postfix/qmgr[28658]: 61947125AA4: removed

Note that Postfix bounces emails from the queue after 5 days. If you
stay offline longer than that period, you might want to tweak the
[`maximal_queue_lifetime`](http://www.postfix.org/postconf.5.html#maximal_queue_lifetime) setting to something larger:

    postconf -e maximal_queue_lifetime=30d
    postfix reload

Implementation details
----------------------

We drastically restrict the number of options accepted from
`sendmail`. Only those options are considered valid:

 * `<recipient> [ <recipient> [ ... ] ]` - email addresses to send the
   email to. those cannot start with a dash and must not contain
   spaces. each email must be passed as its own separate argument to
   rsendmail
 * `-t`: deduce recipients from the `To` or `Cc` email headers. This
   is passed directly to the underlying sendmail command, no parsing
   is done by rsendmail directly. This assumes there is no
   vulnerability in the `-t` option on the other side.
 * `-f <sender>`: Set  the  envelope sender address. This is the
   address where delivery problems are sent to.
 * `-oi`: Do not treat `.` on its own line specially.

The following options are deliberately ignored, even though they might
eventually be implemented:

 * `-R <return>` and `-N <dsn>`: we do not really care about
   status. just accept the default from the remote server.
 * `-r <sender>`: same as `-f`
 * `-v`: might be useful in the future, but keeping it simple for now

All other options will cause an error or might be ignored in the
future for backwards compatibility purposes, but should never have an
effect. Unless otherwise noted, `sendmail` arguments in this document
refer to the Postfix [sendmail(1)](http://www.postfix.org/sendmail.1.html) manual page.

The `mail` logging facility is used to send messages to syslog.

Pitfalls and caveats
--------------------

 * `sshd` makes some noises about `no-pty` and `command=` regarding
   8-bit clean channels. we assume an 8-bit clean channel, so make
   sure the `authorized_keys` file has a `no-pty` setting. best is to
   use the `restrict` argument, but that is available only starting
   from OpenSSH 7.2
 * creating a dedicated user might be more appropriate than reusing a
   privileged account.
 * Emacs' `sendmail-send-it` function will fail if there is *any*
   output from the sendmail command, if `mail-interactive` is enabled
   (the default). This means changing the log level to anything more
   verbose than `WARNING` will cause Emacs to think there is a failure
   even if the email is actually sent. This will mean `Fcc` will fail
   as well and multiple emails be sent if the user doesn't realize the
   problem.
 * Armstrong's script uses a (MD5) checksum to ensure the message's
   integrity. This was introduced in [this commit](https://git.donarmstrong.com/?p=bin.git;a=commitdiff;h=0c9b112effdd642db560859dbae5fb77d1bfea56) as a way to
   "avoid having a dropped connection send a truncated file". We do
   not know if rsendmail suffers from this bug.

Prior art
---------

 * [LMTP](https://en.wikipedia.org/wiki/Local_Mail_Transfer_Protocol) somewhat does what we want here, but there's not a real
client that we can run on the other side, so it's not really useful.

 * [msmtp](http://msmtp.sourceforge.net/) is pretty close to what we need, but only talks SMTP, which
means storing secrets on the client. We *could* try to pipe an SMTP
socket through the SSH connection, but that feels rather messier and
less general-purpose-y. It also does not have a local queue.

 * [nullmailer](https://packages.debian.org/sid/nullmailer) is *almost* what we need, but still talks SMTP.

 * [dma](https://github.com/corecode/dma) (DragonFly Mail Agent) is similar and does weird things like
modifying the message in flight (e.g. removing `Bcc`).

 * [esmtp](http://esmtp.sourceforge.net/) is more of the above and "no longer being maintained"
(accessed on 2018-04-21)

 * [ssmtp](https://tracker.debian.org/pkg/ssmtp) is similar to msmtp except it has no active upstream out of
Debian.

 * [masqmail](http://marmaro.de/prog/masqmail/) is yet more of the above, except it seems to have its
own alias database and other complicated stuff.

 * [UUCP](https://en.wikipedia.org/wiki/UUCP) (Unix-to-Unix CoPy) is designed with this in mind and
sendmail ships a [rmail](https://manpages.debian.org/stretch/rmail/rmail.8.en.html) command that reads emails from UUCP
clients, but those have their own idiosyncrasies. Still, it should be
possible to configure UUCP clients to send email through an SSH
connexion, but that seems needlessly complicated.

 * [NNCP](http://www.nncpgo.org/) (Node to Node copy) "is a collection of utilities
simplifying secure store-and-forward files and mail exchanging." It's
interesting in theory, but it practice it does much more than what we
actually need here. But if I were to redo this, I would probably use
it instead of my setup, because it's fairly easy to integrate into
Postfix *and* it is more resilient than SSH (e.g. email over Ham radio
anyone?)

 * [Don Armstrong](https://donarmstrong.com/) wrote a [nullmailer](https://untroubled.org/nullmailer/) remote called
[sshsendmail](https://git.donarmstrong.com/?p=bin.git;a=blob_plain;f=sshsendmail) which basically does what we want, but it injects a
nullmailer shim through the SSH connection as a `perl -e`
executable. This makes it difficult to restrict the SSH
connection. [David Bremner](https://www.cs.unb.ca/~bremner/) repackaged an earlier version of this as
[nullmailer-ssh](https://salsa.debian.org/bremner/nullmailer-ssh) which at least does not use `perl -e` but still
has a nullmailer-specific dialect in the `rsendmail` command.

 * Some IMAP servers have support for an `Outbox` folder that will send
an email that is dropped on that folder through a configured mail
server. Only the [Courier MTA](https://www.courier-mta.org/) seems to have that functionality
(called [IMAP send](https://www.courier-mta.org/imap/INSTALL.html#imapsend)) and I have stopped using that server a while
ago. My server of choice ([Dovecot](https://www.dovecot.org/)) [debated the feature in
2006](https://www.dovecot.org/list/dovecot/2006-November/017285.html) but it was never implemented.

Future work
-----------

This could be made in a Debian package or two: one would be
`rsendmail` for the server side and `sshsendmail` for the client side,
and maybe plugin packages for the various integration mechanisms. I'm
too lazy for this now.

Piping stuff through SSH makes it difficult to distinguish between
temporary failures (e.g. DNS or TCP fails) and configuration errors
(SSH key mismatch). I'm not even sure what should bounce, so I have
avoided that issue altogether by treating all SSH failures as
temporary, but it might be relevant to re-implement this using
[Paramiko](http://www.paramiko.org/) or some other library in the future.

Credits
-------

On top of the above "prior art", I stand on Bremner and Armstrong's
shoulders as they provided the basic idea for this program.

This software was written by Antoine Beaupré in 2018 and is released
under the Affero GPLv3.
