Package: rsendmail
Restricted sendmail commandA safer sendmail command to send email without passwords, over SSH.ObjectiveThis command aims at replacing the builtinsendmailcommand which
gives too much privileges to the caller. For example,Postfix'ssendmail(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 oldSendmailbinary, 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 anauthorized_keysfile. That key can then send email,
butonlysend 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.Quickstartscp rsendmail.py example.net:/usr/local/bin/rsendmailWherever you would callsendmail, you can now call this instead:ssh example.net rsendmailSee below for instructions on how to add a queue for when you're
offline, restrict the connection to rsendmail, or integrate with
existing MTAs.InstallationThis 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 mailsshsendmail.py- a localMDAthat acts as a compatibility shim
with the remote rsendmail. this part is optional, as you'll see
below.Basic configurationThe following assumes your relay host isexample.netand is already
configured to accept SSH connections on a user calledrsendmail. It
also assumes there's an emaildevnull@localhostthat accepts
delivery.find the$PATHon the remote host:ssh rsendmail@example.net 'echo $PATH'installrsendmail.pysomewhere in your$PATHasrsendmail:scp rsendmail.py rsendmail@example.net:/usr/local/bin/rsendmailgenerate an SSH key for rsendmail:ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_rsendmailcopy the key to yourauthorized_keysfile:( printf 'command="rsendmail",restrict '; cat ~/.ssh/id_ed25519_rsendmail.pub ) | ssh rsendmail@example.net 'cat >> .ssh/authorized_keys'send a test email:printf "Subject: test\n\nThis is a test" | ssh -i ~/.ssh/id_ed25519_rsendmail  rsendmail@example.net rsendmail devnull@localhostverify the mail was properly delivered and the message content is
complete. if so, thenrsendmailis properly configuredNow you can send email, but there are some bits missing. Most tools
will expect asendmailcommand 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, aMDAto actually
deliver the mail in a standard way. Queuing will be handled by aMTAthat will call the MDA.MDA configurationSo the next step is to setup a local MDA to talk withrsendmail. Here's a quick comparison of the possible configurations
documented below:MDAAdvantagesDisadvantagesStandaloneSimplestSingle user, no queueNullmailerMinimalistUnusual standard, reliability concernsPostfixWell-knownQueue expiresIntegration with other MTAs are quite possible as well and
documentation to accomplish that is welcome.StandaloneThe simplest configuration is to use a simple wrapper script for an
MDA, without any other MTA. For example, here is the content of a
possiblesendmailcommand:#!/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~mailhome
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 arsendmailuser was created on the remote system.Nullmailer compatibilityNullmaileris 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 thesshsendmail.pywrapper comes in.generate an SSH key for themailuser:sudo -u mail ssh-keygen -t ed25519make sure the remote server identity is verified:sudo -u mail ssh rsendmail@example.net trueinstall thenullmailerpackage, version at least 2.0:apt install -t buster nullmailerdeploy the MDA wrapper:install sshsendmail.py /usr/lib/nullmailer/sshsendmailadd it as a remote in/etc/nullmailer/remotes:example.net sshsendmail --mta=nullmailer --identity=/var/mail/.ssh/id_ed25519 --user=rsendmailAgain, adapt theexample.nethost andrsendmailuser 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 inrsendmail...Postfix compatibilityPostfix can talk to a remotersendmailserver easily through thepipeservice. Here are the steps to configure a Postfix client,
oncersendmailis installed on a server and theauthorized_keysis
setup:Install Postfixapt-get install postfixconfigure it as asatellitesystem and use the recommended
hostname. as arelayhost, use the hostname (and username!)
of the SSH server, e.g.rsendmail@example.net, in other words:postconf -e 'relayhost=rsendmail@example.net'configure thepipeservice in/etc/postfix/master.cf:rsendmail unix  -       n       n       -       -       pipe
   user=mail argv=ssh ${nexthop} rsendmail -f ${sender} ${recipient}configure that transport as the default relay:postconf -e 'default_transport=rsendmail:'Make sure themailuser can login to the relay server
automatically and send mail:sudo -u mail ssh rsendmail@example.net rsendmail devnull@localhost < /dev/nullThe above will ask for host verification. Once that works, reload
Postfix, which should start relaying mail through the other
server:postfix reloadNote that the above configuration will bounce messages if SSH cannot
reach the remote server. That is because SSH returns non-standard (as
persysexits.h) error codes (i.e.255on failure) which Postfix
cannot directly parse. To handle this correctly, thesshsendmail.pywrapper can be installed instead, again inmaster.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: removedNote that Postfix bounces emails from the queue after 5 days. If you
stay offline longer than that period, you might want to tweak themaximal_queue_lifetimesetting to something larger:postconf -e maximal_queue_lifetime=30d
postfix reloadImplementation detailsWe drastically restrict the number of options accepted fromsendmail. 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 theToorCcemail 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-toption 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 nowAll 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,sendmailarguments in this document
refer to the Postfixsendmail(1)manual page.Themaillogging facility is used to send messages to syslog.Pitfalls and caveatssshdmakes some noises aboutno-ptyandcommand=regarding
8-bit clean channels. we assume an 8-bit clean channel, so make
sure theauthorized_keysfile has ano-ptysetting. best is to
use therestrictargument, but that is available only starting
from OpenSSH 7.2creating a dedicated user might be more appropriate than reusing a
privileged account.Emacs'sendmail-send-itfunction will fail if there isanyoutput from the sendmail command, ifmail-interactiveis enabled
(the default). This means changing the log level to anything more
verbose thanWARNINGwill cause Emacs to think there is a failure
even if the email is actually sent. This will meanFccwill 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 inthis commitas a way to
"avoid having a dropped connection send a truncated file". We do
not know if rsendmail suffers from this bug.Prior artLMTPsomewhat 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.msmtpis pretty close to what we need, but only talks SMTP, which
means storing secrets on the client. Wecouldtry 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.nullmailerisalmostwhat we need, but still talks SMTP.dma(DragonFly Mail Agent) is similar and does weird things like
modifying the message in flight (e.g. removingBcc).esmtpis more of the above and "no longer being maintained"
(accessed on 2018-04-21)ssmtpis similar to msmtp except it has no active upstream out of
Debian.masqmailis 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 armailcommand 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
Postfixandit is more resilient than SSH (e.g. email over Ham radio
anyone?)Don Armstrongwrote anullmailerremote calledsshsendmailwhich basically does what we want, but it injects a
nullmailer shim through the SSH connection as aperl -eexecutable. This makes it difficult to restrict the SSH
connection.David Bremnerrepackaged an earlier version of this asnullmailer-sshwhich at least does not useperl -ebut still
has a nullmailer-specific dialect in thersendmailcommand.Some IMAP servers have support for anOutboxfolder that will send
an email that is dropped on that folder through a configured mail
server. Only theCourier MTAseems to have that functionality
(calledIMAP send) and I have stopped using that server a while
ago. My server of choice (Dovecot)debated the feature in
2006but it was never implemented.Future workThis could be made in a Debian package or two: one would bersendmailfor the server side andsshsendmailfor 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 usingParamikoor some other library in the future.CreditsOn 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.