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.
Solution Overview
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 created in the public subnet. What we need to do to correct this is to modify our database’s security group to allow connection requests from our EC2 instance’s security group. This will allow our EC2 instance to communicate with our database instance, and later, allow US on our home network to communicate it with it as well.
Create and Configure Our RDS Subnet Group and Instance
At this point, we’re established everything we need to facilitate network connectivity. All we need to do is use a powershell / terminal command that will form a SSH tunnel from our home machine to our database instance.
Afterwards, we can connect to our database using tools like pgAdmin as if the database was on our local network!
So enough of the theory / explanation, now let’s jump in to the AWS console and actually set this thing up.
Tutorial
Step 1 – Create our Private Subnets
Since we’re using our default VPC, we don’t need to create public subnets since they are already available for us. What we do need to do is to create our two private subnets that we’ll be using in our later steps.
To do this, head over into the VPC section of the AWS console and click on Subnets on the left hand navigation bar (see 1 below).
One thing that you need to understand about VPCs (and networking in general) is a concept called CIDR block notation. CIDR stands for classless inter-domain routing. It is a method for allocating certain chunks of IP addresses for the purpose of networking.
Every default vpc starts with 172.31 and has /16 at the end. This tutorial isn’t a lesson on CIDR subnetting, but all you really need to know is that this notation helps us define ranges of IP addresses for each subnet we create. If you take a look at (2), you can see that each subnet within our VPC has similar, but slightly different numbers.
If we look at row 1 (subnet 1) in the table (2), it has a CIDR block of 172.31.0.0/20. This means that any piece of infrastructure deployed in this subnet can have an IP address that ranges from 172.31.0.0 to 172.31.15.255. Note that 255 is the maximum range a section of the block can go up to. This pattern continues for our second subnet with a CIDR block of 172.31.16.0/20. It has a range of 172.31.16.0 to 172.31.31.255.
For our two private subnets, we need to carve out a section of our VPC’s network space. We can do this by following the existing pattern of our default VPC’s CIDR. So for example, if you look at the pattern of our subnets from the image above, you can see that each subsequent row has 16 added to it in the 3rd position. The highest CIDR block in the network so far is 172.31.80.0/20. We are going to follow this pattern and add two private subnets separated by 16 digits each. So the subnets we will create will be 172.31.96.0/20 and 172.31.112.0/20
You can play around with different CIDR blocks to find the minimum and maximum values within each using this neat tool.
To proceed, click on Create Subnet as seen in (3).
First, select the VPC ID that you would like to create the subnet for. Most accounts only have one VPC (the default one) – we’ll be using that. Note that the id of your VPC and subnets may be different than mine.
Second, give your subnet a name that is indicative of it’s function. It can get really confusing working with subnets if you don’t name them correctly. In this case, since this will be a private subnet that we place in availability zone (AZ) 1a, we’ll name it private-subnet-1a
.
Third, select the availability zone to place this subnet in. I’m going to place ours in 1a which is the same subnet I’ll be placing our database into later.
Fourth, input the IPv4 CIDR block that we determined earlier. In our case, it is 172.31.96.0/20.
Fifth, repeat this process for your second subnet, but this time naming it private-subnet-1b
, placing it in AZ 1b, and giving it the CIDR block of 172.31.112.0/20.
After you’re done, you should see an image like below under the subnets section. Tip: remember to click ‘Clear Filters’ to remove subnet specific filters from your view.
Before we can move on from the land of networking, we need to actually make our subnets private.
Step 2 – Make Our Subnets Private
When creating our subnets via the wizard, our subnets automatically are set to not assign a public ip address when an infrastructure components is created within the subnet. You can see this in the large image below highlighted in yellow.
This is a good first step, but in order for us to make these two subnets truly private, we need to modify the route table they are associated with to dis-allow internet access. As a quick summary, a route table defines the networking rules of which direct how network traffic travels within your VPC. You can learn more about them here.
By default, when you create a new subnet, it uses the route table that is assigned to the VPC. In our case, that is rtb-632e551d
. If we click on it, and go to Routes, we can the rules of this route table as seen below.