Accessing a private RDS instance via an ssh tunnel

Grig Gheorghiu
May 18, 2018 · 3 min read

If you go by AWS best practices, you launch all your RDS instances in a VPC and make them private. That means you can’t access them from an outside IP except by jumping through some hoops. One type of hoop is setting up an ssh tunnel. Most articles I’ve seen talk about setting up an ssh tunnel on your local machine. I wanted to set up a tunnel on a bastion host, so that I can also set up replication from the RDS master to a non-AWS MySQL instance if needed.

I assume you have an EC2 instance provisioned in a public subnet of your VPC. You need to allow that instance to access port 3306 on the private RDS instance.

My EC2 instance runs Ubuntu 16.04. I wanted to use systemd to ensure that the ssh tunnel is up and running at all times. I created this file:

$ cat /etc/systemd/system/
Description=SSH Tunnel to non-public RDS instance
ExecStart=/usr/bin/ssh -N -R ubuntu@

The -N flag tells ssh to not execute any command. We use ssh purely for port forwarding.

Here is the man page documentation for the -R flag I use:

 -R [bind_address:]port:host:hostport
-R [bind_address:]port:local_socket
-R remote_socket:host:hostport
-R remote_socket:local_socket
Specifies that connections to the given TCP port or Unix socket on the remote (server) host are to be forwarded to the given host and port, or Unix socket, on the local side.
This works by allocating a socket to listen to either a TCP port or to a Unix socket on the remote side. Whenever a connection is made to this port or Unix socket, the connec‐
tion is forwarded over the secure channel, and a connection is made to either host port hostport, or local_socket, from the local machine.
Port forwardings can also be specified in the configuration file. Privileged ports can be forwarded only when logging in as root on the remote machine. IPv6 addresses can be
specified by enclosing the address in square brackets.
By default, TCP listening sockets on the server will be bound to the loopback interface only. This may be overridden by specifying a bind_address. An empty bind_address, or the
address ‘*’, indicates that the remote socket should listen on all interfaces. Specifying a remote bind_address will only succeed if the server’s GatewayPorts option is enabled
(see sshd_config(5)).
If the port argument is ‘0’, the listen port will be dynamically allocated on the server and reported to the client at run time. When used together with -O forward the allocated
port will be printed to the standard output.

As the documentation says, I also had to set

GatewayPorts yes

in /etc/ssh/sshd_config and restart the ssh service.

One more thing I had to do for the ssh command to work was to add the public ssh key for user ubuntu to the same user’s authorized_keys file, so that I can ssh ubuntu@

I then ran

$ sudo systemctl daemon-reload
$ sudo systemctl start rds-ssh-tunnel

and I was able to see the tunnel up and running:

/usr/bin/ssh -N -R ubuntu@

At this point, I can connect with MySQL client tools or from a remote MySQL instance to the external IP of my bastion host on port 3306, and the connection will be forwarded to the private RDS instance.


For upstart-based systems, I created this file:

$ cat /etc/init/rds-ssh-tunnel.conf
description “SSH Tunnel to non-public RDS instance”
start on (local-filesystems and net-device-up IFACE!=lo)
stop on runlevel [06]
chdir /home/ec2-user
exec su -s /bin/sh -c ‘exec “$0” “$@”’ ec2-user — /usr/bin/ssh -N -R ec2-user@
respawn limit 10 10
kill timeout 10

To start this service:

$ sudo start rds-ssh-tunnel

Grig Gheorghiu

Written by

Head of DevOps at Reaction Commerce. System infrastructure scaling, cloud computing, Golang and Python programming, automated testing.