PHP Developer / Blog

October
10th, 2008

Secure mysql replication between colos over an ssh tunnel

Digg this article · Save to del.icio.us · Stumble it!

MySql replication is pretty painless to implement.  But that doesn’t mean you shouldn’t consider exactly how you roll it out.  There’s plenty of things to consider and for me one of them was how to best replicate between colos.  Such a deployment raises issues (security, latency, etc) that aren’t as big of a concern when you’re doing it across a LAN. I haven’t had a chance to tackle the latency issue yet (that’s another blog post) but here were my security concerns:

  • Data transmitted across the Internet shouldn’t be easily read (in my case mainly email addresses and passwords)
  • MySql shouldn’t be listening publicly on ANY port
  • Whatever the solution is needs to be simple…complex solutions are often - wrong

For the sake of this post I’ll be referring to the master database server as Server-A and the slave server as Server-B.

Secure transmission of data over the Internet
Before I began searching for options I knew in the back of my head that ssh tunnels were probably the way to go.  I began my search for different tips and tricks to creating a persistent ssh tunnel.  I came across several methods but liked the one proposed by Brandon Checketts the best.  You should, of course, create an unprivileged user (named tunnel perhaps) on both Server-A and Server-B.  With a little bit of tweaking I decided to go with the script below which sits on Server-B.

#!/bin/sh
echo "*******************************"
echo date
user=tunnel
server=Server-A
createTunnel() {
  /usr/bin/ssh -f -N -L4306:127.0.0.1:3306 -L19922:127.0.0.1:22 ${user}@${server}
  if [[ $? -eq 0 ]]; then
    echo Tunnel to ${server} created successfully
  else
    echo An error occurred creating a tunnel to ${server} RC was $?
  fi
}
## Run the 'ls' command remotely.  If it returns non-zero, then create a new connection
/usr/bin/ssh -p 19922 tunnel@localhost ls
if [[ $? -ne 0 ]]; then
  echo Creating new tunnel connection
  createTunnel
else
  echo Tunnel already exists
fi
echo "*******************************"

I threw in a few extra echos in there since I plan on having this run in cron every minute or so and output to a log file.  Let’s quickly go over what this does.  The script sets up a few variables.  Then the createTunnel function is defined.  It attempts to create an ssh connection specifying several things: -f sends it to the background, -N tells it not to execute a command after connecting, -L binds a local port to a remote one over which we can tunnel.  Easy enough, no?

You may have noticed that we mapped 2 different ports.  Brandon decided to map a port for MySql and one which he later users to check if the connection is up.  On to that check now.  The first action the script takes is to do a remote ls command issued from Server-B on Server-A.  This is done by ssh’ing on port 19922 to localhost which actually goes over the tunnel to Server-A.  It then issues the ls command.  The next step is to check if the command returned 0 which would tell us that the connection (or more specifically the mapping of local port 19922 to remote port 22) is not working.  In that case we call the createTunnel function we defined earlier which should create the tunnel.  This script should rarely have to do this once the connection is created the first time.

We’re not finished yet.  Since this is being automated in cron we’ll want to set up key based authentication.  This will allow the tunnel user to connect from Server-B to Server-A without having to provide a password.

tunnel@Server-B# ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/home/tunnel/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/tunnel/.ssh/id_rsa.
Your public key has been saved in /home/tunnel/.ssh/id_rsa.pub.

tunnel@Server-B# scp ~/.ssh/id_rsa.pub Server-B:/home/tunnel/
tunnel@Server-B# ssh Server-A
password:
tunnel@Server-A# cat id_rsa.pub >> ~/.ssh/authorized_keys
tunnel@Server-A# rm id_rsa.pub
tunnel@Server-A# vi /etc/sshd/sshd_config
# Make sure the following lines are uncommented
RSAAuthentication yes
PubkeyAuthentication yes
tunnel@Server-A# su - privileged_user
privileged_user@Server-A# sudo /etc/init.d/sshd restart
password:
privileged_user@Server-A# exit
tunnel@Server-A# exit
tunnel@Server-B# ssh Server-A
tunnel@Server-A#
# Yay no password required

Keeping MySql a secret
Noone needs to know that you’re running MySql.  There are several ways to do this using a firewall or binding MySql to 127.0.0.1 depending on your setup.  I’m assuming a single webserver with MySql running on it in which case we’d want to bind to 127.0.0.1.  Ensure this is the case by looking at my.cnf on both Server-A and Server-B.

user# vi /etc/mysql/my.cnf
# Make sure this is uncommented
bind-address = 127.0.0.1

Since we specified -L4306:127.0.0.1:3306 the connection request from Server-B to Server-A will appear to MySql as coming from 127.0.0.1.

user@server-B# mysql -u root -p -h 127.0.0.1 -P 4306
Enter password:
mysql>

It’s important to connect to host 127.0.0.1 and not localhost.  MySql connects to the local socket and bypasses TCP/IP when using localhost.

Time to replicate
At this point you can set up replication as normal. Here’s a basic setup. On Server-A add the following to your my.cnf. I’m not going to go into populating the slave server with data in this post but you’ll undoubtably need to do that.

user@Server-A# sudo vi /etc/mysql/my.cnf
log-bin
server-id    = 1
binlog-do-db = name_of_db
binlog-do-db = name_of_another_db
user@Server-A# sudo /etc/init.d/mysql restart

On Server-B add the following to your my.cnf

user@Server-B# sudo vi /etc/mysql/my.cnf
master-host     = 127.0.0.1
master-user     = tunnel
master-password = tunnel_users_password
master-port     = 13306
server-id       = 2
binlog-do-db    = name_of_db
binlog-do-db    = name_of_another_db
binlog-ignore-db= mysql
binlog-ignore-db= test
binlog-ignore-db= information_schema
user@Server-B# sudo /etc/init.d/mysql restart

The beauty of simplicity
This solution in my mind is elegantly simple.  It uses completely standard applications which are already installed on both servers.  The application doesn’t have to do much custom work since it can connect to the master as needed by specifying port 4306.  I haven’t had this running for an extended period of time (just a week so far) but plan on keeping an eye on it.  If you have any experiences or suggestions I’d love to hear them in the comments.

Resources

7 Responses to “Secure mysql replication between colos over an ssh tunnel”

  1. Brandon Checketts Says:

    Hi. Glad that you find my instructions helpful. I have been running a server like this with tunnels to 3 other servers for several months now and it works very well.

    The only thing I have noticed, is that if the connection between the server goes down for a while, then there can get to be multiple ‘check_tunnel’ scripts running that have to be killed manually. Other than that, I’ve had the connection between the servers die multiple times, and replication always starts right back up.

    You might also be interested in the script I created to Monitor the replication to ensure that it doesn’t get behind.

    Also, you’ll want to Enable replication compression between those servers to minimize the bandwidth used.

  2. Tom Says:

    Or, you could just use MySQL’s built in encryption support…

    What is the point of this?

  3. jaisen Says:

    @Tom, not sure what built in encryption support you’re referring to. Even if MySql did have some sort of encryption support this method still allows you to keep MySql from listening publicly on port 3306.

  4. jaisen Says:

    @Brandon, Thanks for the tips. I’ll definitely check out the two posts you mentioned (and subscribe via RSS).

  5. Tom Says:

    Built in MySQL encryption: http://dev.mysql.com/doc/refman/5.0/en/secure-using-ssl.html

    Having port 3306 open is probably just as secure or insecure as having a random ssh port open somewhere.

  6. jaisen Says:

    @Tom, Thanks for pointing that out - I’ll add that option to the post. While this addresses #1 and #3, there’s still security to be gained by not having mysql listening publicly.

    I’m assuming that there’s some ssh entry point to the machine (if not directly through a firewall). So having ssh open on some port is a given. The more applications you have listening publicly the more chances you have for being exploited. Note, I’m not saying Apache is (or isn’t) more secure than MySql, but the more entry points the more likely something can happen.

    Personally, I prefer ports 80, 443 and a non standard port for ssh.

  7. PHP Programming India Says:

    Very informative. Thanks for the information!

    Raj Malhotra
    PHP Programming India

Leave a Reply

Captcha
Enter the letters you see above.


About this site:
This is my (Jaisen Mathai) personal site for potential employers who want to see my resume or portfolio. My ideal job would be to work as a PHP developer on a large scale consumer website. My experience is in using PHP, MySQL, Ajax and JSON. I really enjoy creative brainstorming...taking a problem apart and narrowing 100 solutions down to the best one.

Thanks for stopping by. Be sure to drop me a line.