Skip to main content

email delivery over SSH and restricted sendmail

Project description

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's sendmail(1) 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 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.

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.

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.

  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 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 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) 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 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 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 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 is almost what we need, but still talks SMTP.

  • dma (DragonFly Mail Agent) is similar and does weird things like modifying the message in flight (e.g. removing Bcc).

  • esmtp is more of the above and "no longer being maintained" (accessed on 2018-04-21)

  • ssmtp is similar to msmtp except it has no active upstream out of Debian.

  • masqmail is yet more of the above, except it seems to have its own alias database and other complicated stuff.

  • UUCP (Unix-to-Unix CoPy) is designed with this in mind and sendmail ships a rmail 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 (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 wrote a nullmailer remote called 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 repackaged an earlier version of this as 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 seems to have that functionality (called IMAP send) and I have stopped using that server a while ago. My server of choice (Dovecot) debated the feature in 2006 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 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

rsendmail-1.1.4-py3-none-any.whl (26.9 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page