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.
-
find the
$PATH
on the remote host:ssh rsendmail@example.net 'echo $PATH'
-
install
rsendmail.py
somewhere in your$PATH
asrsendmail
:scp rsendmail.py rsendmail@example.net:/usr/local/bin/rsendmail
-
generate an SSH key for rsendmail:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_rsendmail
-
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'
-
send a test email:
printf "Subject: test\n\nThis is a test" | ssh -i ~/.ssh/id_ed25519_rsendmail rsendmail@example.net rsendmail devnull@localhost
-
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.
-
generate an SSH key for the
mail
user:sudo -u mail ssh-keygen -t ed25519
-
make sure the remote server identity is verified:
sudo -u mail ssh rsendmail@example.net true
-
install the
nullmailer
package, version at least 2.0:apt install -t buster nullmailer
-
deploy the MDA wrapper:
install sshsendmail.py /usr/lib/nullmailer/sshsendmail
-
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:
-
Install Postfix
apt-get install postfix
-
configure it as a
satellite
system 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 the
pipe
service 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 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
-
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 theTo
orCc
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 aboutno-pty
andcommand=
regarding 8-bit clean channels. we assume an 8-bit clean channel, so make sure theauthorized_keys
file has ano-pty
setting. best is to use therestrict
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, ifmail-interactive
is enabled (the default). This means changing the log level to anything more verbose thanWARNING
will cause Emacs to think there is a failure even if the email is actually sent. This will meanFcc
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 useperl -e
but still has a nullmailer-specific dialect in thersendmail
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
Built Distribution
File details
Details for the file rsendmail-1.1.4-py3-none-any.whl
.
File metadata
- Download URL: rsendmail-1.1.4-py3-none-any.whl
- Upload date:
- Size: 26.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.11.1
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 15543b5b17e6f588b6b3e98793c48f612692603be23555137d8c7023bd5b142c |
|
MD5 | 1ca44e6646fa59e571189ef13ac6c50c |
|
BLAKE2b-256 | fd4b82e1305e91fd711dab149fdb9ebfc1a66e96bc1046334896b16bf3b9c642 |