Creating an SSH Tunnel for Fabric

Our Django production server doesn’t accept SSH connections from any but a handful of hosts—just our office and a couple of bastion machines. For many applications, we install deploy scripts on bastion hosts, but we wanted to get Django deployment right as well as honor the original intent of Fabric without sacrificing security.

This pattern uses the SSH binary available on *nix systems, so the tunnel probably won’t work on Windows unless you have Cygwin installed. It also seems to be possible to create a tunnel with Paramiko, the Python SSH library which Fabric uses, but we had trouble with this approach; the tunnel took a long time to open and often caused the client to throw errors. Using the SSH binary directly works fairly seamlessly, and Python’s atexit module allows us to automatically close the connection when script execution completes.

from fabric.api import *
from fabric.contrib.console import confirm
from local_settings import remote_user
from time import time
import subprocess, shlex, atexit, time
from settings import DATABASES
from os import remove
env.use_ssh_config = True
env.context = local
tunnels = []
local_db = DATABASES[default]
class SSHTunnel:
def __init__(self, bridge_user, bridge_host, dest_host, bridge_port=22, dest_port=22, local_port=2022, timeout=15):
self.local_port = local_port
cmd = ssh -vAN -L %d:%s:%d %s@%s % (local_port, dest_host, dest_port, bridge_user, bridge_host)
self.p = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
start_time = time.time()
while not Entering interactive session in self.p.stderr.readline():
if time.time() > start_time + timeout:
raise SSH tunnel timed out
def entrance(self):
return localhost:%d % self.local_port
def live():
env.user = ops
prod = SSHTunnel(remote_user,,
env.hosts = [prod.entrance()]
env.context = live = /usr/share/django/alleyinteractive/alleyinteractive
env.db_config = DATABASES[default]
view raw
hosted with ❤ by GitHub