When traveling with my laptop, I find myself on all manner of different wireless networks, and these diverse range of networks always seem to do seomthing funky with SMTP. To get around this I’ve resorted to sending my e-mail through an SSH tunnel. I had previously tried using both SSL and TLS as methods of securely connecting to my upstream mail server, but neither of these methods appeared to work as reliably as a good old SSH tunnel.
On my laptop I use mutt which,
unlike most GUI email clients, relies on a mail server running locally
on the machine to deliver the mail upstream. The default local mail
server on Mac OS is postfix,
which works pretty well. In postfix’s main configuration file (main.cf),
you can set the relayhost option to use a specific mail
server as a relay (instead of delivery mail directly form the laptop).
Usually you would set this to your ISP’s outgoing mail server. E.g:
relayhost = [smtp.example.com]
Now, instead of directly contacting smtp.example.com, I want
to go through an SSH tunnel. So I change the relay host to:
relayhost = [127.0.0.1]:8025
I use the dotted decimal rather than localhost to avoid an DNS lookup, and
force things to use IPv4. The square brackets force it to directly use
the host, rather than performing any MX lookups. Finally the last part is the
port number. So, now rather than trying to contact smpt.example.com
the local mailserver will try to send any email to port 8025 on the localhost.
So, the next question is how to actually make it so that port 8025
is a tunnel to port 25 on the real mailserver. One option is to use
ssh’s -L option. Something like: ssh
mail.example.org -L 8025:mail.example.org:25. This works fine,
except that it is not very robust. The ssh session will drop out from
time-to-time, and it gets up when moving between networks. The
solution to this is to create the tunnel on-demand. Each time a
program tries access port 8025 on localhost, we set up the tunnel
then. If things were really hammering port 8025 this might be a bit of
overhead, but realistically, this isn’t a problem, and things become
much more robust and reliable.
On a traditional UNIX system you would use (x)inetd to
automatically run a program when a socket is opened. Mac OS X instead uses
launchd. A file, /System/Library/LaunchDaemon/mailtunnel.plist
is created, which is our config file for creating the SSH tunnel on demand. It looks
something like:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>au.id.benno.mail</string>
<key>UserName</key>
<string>benno</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/ssh</string>
<string>-i</string>
<string>tunnel_key</string>
<string>-q</string>
<string>-T</string>
<string>mail.example.com</string>
</array>
<key>inetdCompatibility</key>
<dict>
<key>Wait</key>
<false/>
</dict>
<key>Sockets</key>
<dict>
<key>Listeners</key>
<dict>
<key>SockServiceName</key>
<string>8025</string>
</dict>
</dict>
<key>SessionCreate</key>
<true/>
</dict>
</plist>
The import bits here are the Sockets fragment, where we say that
we are going to run the service whenever something connect to port 8025. The
other important part is the ProgramArguments part, which is a verbose
way of specifying what command to run to create the tunnel. In this case
we run ssh -i tunnel_key -q -T mail.example-com.
The final part is actually setting up the command to create the tunnel on demand.
First we create an ssh key-pair (using ssh-keygen). This is going
to be a key without a passphrase so that we can ssh to our mailserver without
any interaction requiring a password to be entered. One you create the public
keypair, you copy the .pub file across to your mail server, and
keep the private one locally. Now, ordinarily, you don’t want a passphraseless
key, because it would be very powerful and if someone got access to the key,
they would have full access to your account on the mailserver. The next trick
we play is to limit what the key can be used for. Specifically, we ensure that
the key can only be used to access port 25 on the mailserver. We do this
by setting some options when adding the public key to the list of authorized
keys on the mailserver.
The .ssh/authorized_keys2 file on the mailserver contains the list
of keys which can be used to gain access to the server via ssh. We want to add
our newly create public key to this list, but add some options to limit the scope
of what can be done when using the key. The format of the authorized_keys2
file is: <options> ssh-dss <key-data> <comment>. So,
instead of just adding the key, we are going to put some options in place:
command="nc localhost 25",no-X11-forwarding,no-agent-forwarding,no-port-forwarding
What these options do is disable any forwarding using the key, and then specifically
set the command to run when using the key to be nc localhost 25. nc
a.k.a netcat is a very nifty little program
that is useful for these scenarios. So with this set up, when using ssh, it will connect
to the mailserver, but you won't get a console, or be able to use any other programs. We can test this
out:
$ SSH_AUTH_SOCK= ssh -i tunnel_key mail.example.com 220 mail.example.com ESMTP quit 221 mail.example.com closing connection
The
-icommand tells ssh to use a specific key, instead of the default key in
.ssh. The SSH_AUTH_SOCK= is important as it disables the
use of an ssh agent that is running, which would otherwise override your key choice.
With that working, you should now be able to directly attach to the tunnel using
telnet localhost 8025.