SSH Tunneling SMTP on Mac OS X

Mon, 01 Dec 2008 13:31:50 +0000
tech article

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

-i
command 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.

blog comments powered by Disqus