1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
|
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.
|