Trying to figure out how to safely and securely connect to your RDS or Aurora database that’s in a private subnet? This is the article for you.
Many getting started tutorials that use Amazon RDS or Aurora create a public IP address as part of setup. This makes it easy for developers to gain access to their database during development, but is a huge security vulnerability that can compromise sensitive data.
The recommended approach by AWS is to place private resources (such as a database) in a private subnet. A private subnet has no ability to communicate with the public internet directly. This allows us to isolate our data from the outside world while still making it accessible to our back end applications.
But now, by following security best practices, we’ve made it hard for ourselves to access our own database during development – say, for instance, from our home or work machine. This creates a dilemma for developers – we want to lock down our database for security, but need access to it for administrative/development purposes.
This article is going to show you how to use a ec2 bastion host (sometimes also called a jump box) that will allow you to connect remotely to your RDS/Aurora instance via SSH tunnelling. I’m going to walk you through all the network setup that you’ll need to run through including setting up private subnets, security groups, your bastion host, and your RDS instance. We’ll be using the Default VPC (Virtual Private Cloud) that comes with every AWS account.
By the end of this article, you’ll have an RDS/Aurora instance in a private subnet that is safely protected from the outside internet. You’ll be able to use tools like pgAdmin, DataGrip, or other RDBMS IDEs to access your database. This will be a step by step tutorial that walks you through every single step.
So let’s get started.
Note: this tutorial will work for all types of databases including on RDS (MySQL, Postgres, Oracle, MSSQL, etc) and Aurora (Postgres or MySQL). The endpoint you connect to in the final step may be slightly different, but the process to establish VPC connectivity is the same.
In recent years, AWS has added tools in the RDS console to set up your database with connectivity to your EC2 instance. This certainly has helped people unblock themselves quickly, but my problem with this approach is that you don’t actually learn the concepts that you need to learn to understand what’s going on behind the scenes. My intention in this tutorial is for you to both understand how to set up connectivity, and show you how to do it properly.
Below is what our final end state will look like (or atleast very close to it).
Our solution will consist of a EC2 t2.micro instance placed in a public subnet. It will have a security group associated with it that allows SSH connection requests initiated from the outside internet (e.g. your home/office). You could also make this more restrictive to only allow connections from your home IP address.
We will also have a private subnet (two actually, for reasons we’ll get into later) that has our Aurora cluster placed inside it. The instances in this cluster will have a different security group that allows Postgres connections from the bastion host’s security group. This will allow your Aurora instance to be able to be connected to from your bastion / EC2 host.
Finally, we’ll use initiate a SSH tunnel from our home machine using our EC2 machine as the entrypoint to the VPC. The SSH tunnel will forward requests to our Aurora instance so that we can access it.
This is our end state. If it’s not immediately clear what’s going on, don’t worry – we’re going to walk through every step.
Starting State – Default VPC Public Subnets
When you create an AWS account, AWS automatically creates a default VPC for you. A VPC (or Virtual Private Cloud) is an isolated network space in the AWS cloud. VPCs allow you to control network connectivity of the infrastructure that you deploy in your account. For example, you have the ability to create infrastructure in certain network spaces of your VPC that are visible to the outside world. This typically includes public facing resources like web application endpoints. On the contrary though, you definitely do not want to put things like databases in a public facing network space. This creates a very real vulnerability that attackers can easily exploit.
One of the building blocks of a VPC is a subnet. A subnet is quite simply a range of IP addresses that exist within your VPC. We use them to logically isolate groups of resources from one another. For example, for resources such as a web app that require incoming requests from the internet, we’ll place them in a public subnet so that they can be called from the internet. For resources such as an RDS database that we want to hide from the public internet, but that can remain callable from our web application, we place it in a private subnet.
Your default VPC already comes with subnets across each availability zone in every region. So for example, in us-east-1, my account has six public subnets across us-east-1a, 1b, 1c, 1d, 1e, and 1f. You can see this in the image below under the subnets section in the VPC section of the AWS console.
By default, when you launch an EC2 instance into any one of these subnets, they will be assigned a public ip address that you can use to connect to it. The public subnet
us-east-1a is one that we’ll be using later in the tutorial section of this article.
Our starting state (just using one subnet for simplicity) looks like the following:
As you can see in the image, all we need is a single public subnet to get this setup working. From here, we need to add private subnets that we will later use to host our Aurora database in.
Adding Private Subnets
Our next step will look like the image below. We’ll be creating a private subnet that has no connectivity from the outside world. Note that in order for this subnet to be truly private, we’ll need to create a custom route table that has no access to the public internet. By default, your subnets will inherit the route table from your main VPC which does have internet access enabled. I’ll show you how to do this in the walkthrough section of this tutorial.
One thing I wanted to point out that isn’t reflected in the picture above is that we’ll need to create two private subnets, not just one. The reason is because when launching an RDS/Aurora instance, you need to create something called a Subnet Group.
Subnet groups are collections of subnets that need to cover atleast two availability zones. Since a subnet (public OR private) can only exist in a single availability zone, we’ll need to create two in order to successfully launch our instance. Below is the error you will get if you try to create a subnet group with subnets that belong to only 1 AZ.
Despite the fact that we’re creating two private subnets, in reality we’ll only be deploying our database instance into one of them. I suspect the reason this is enforced is because there is a feature called multi-az deployments that allow you to place a standby instance in a second AZ. If you don’t have atleast two AZs in your subnet group, you wouldn’t be able to use this feature. Learn more about the feature here.
After we create our basic network topology, we’ll be ready to create our Bastion Host / Jump Box.
Create and Configure Our Bastion Host / Jump Box
In this step, we will be launching a t3.micro instance (free-tier eligible) into our public subnet. Our public subnet will be capable of receiving connection requests from the public internet, however by default our EC2 instance will refuse these requests. This is because we need to modify our EC2 instances security group.
Security Groups are entities that get attached to your AWS instances (both EC2 and RDS in this case). For our EC2 instance, in order to truly open it up to the public internet, we will need to modify the associated security group to allow inbound connections from the public internet. This is a quick configuration that I’ll show you how to do later in the console.
Note that as an alternative to opening up your EC2 instance to be accessible from anyone on the public internet, you can configure it to only be accessible from certain IP addresses (i.e. your home network). This allows you to reduce the risk of exposing this instance to unintended outside audiences. You have to be careful with this approach though since home ISPs can regularly assign you a new IP address and cause you to lose the ability to connect.
The image below shows our setup after this step.
A quick side bar, the name bastion host or jump box are terms that denote how we are using this instance. This instance serves no purpose other than to allow us an entry point into our AWS VPC. By placing it in a public subnet and configuring it with access from the internet, we are creating a “path” for us to connect to it from our home machines. Once we are SSH’d into this machine, we can access all instances in our VPC (provided our route table allows it, which by default it does). Later, we will use this host to facilitate our connection to our RDS instance by using SSH tunnelling. The image below attempts to illustrate this concept.
Create and Configure Our RDS Subnet Group and Instance
As we discussed earlier in the subnet creation step, we will need to create a subnet group that spans our two private subnets. This is a pre-requisite step that we must do before we create our database instance. The image below only shows one for simplicity sake.
Afterwards, we will launch create our database instance and launch it into our VPC and it’s first private subnet. During the creation phase, we will also need to specify the subnet group that we created earlier. After this step, our setup will look like the following:
After you launch your database instance, you may think you’re done and ready to connect to it. However, this is wrong and often where most people start getting frustrated.
Notice that our database instance also has a security group. This is by design as each piece of provisioned infrastructure you create on AWS always has a security group.
By default, your database’s security group will block all connection requests from instances that it does not recognize – this includes your EC2 instance that you just crea