Secure AWS Environments by deploying apps in Private/Public Subnets
Tags: aws, security
Published: 2020-02-02 06:01:19

This is a series of posts to introduce the importance of using public and private subnets to keep your infrastructure secure in AWS. 

In this post, we would: (i) create an EC2 instance, (ii) install NGINX there, and (iii) see the simple NGINX homepage in our browser. This would be a straightforward task if we made the EC2 instance publicly accessible. We would just create an EC2 instance in the public subnet, give it a public IP address, install NGINX, and see the NGINX homepage in the browser via the public IP address. _That's it_.

But this would also mean we are increasing the number of entry points that we have on our VPC. This pattern establishes a dangerous precedent. What if we had to deploy _more_ 10 different client-facing apps? Then, we would have to create 10 new publicly accessible entry points. _This gives more and more ways for hackers to get into our VPC_.

# VPC design patterns to keep our VPC secure

The best practice is to limit the number of entry points to our VPC by using the Application Load Balancer (ALB) for HTTP/HTTPS traffic and the bastion host for SSH traffic. With this, we can deploy hundreds of applications in our VPC yet still keep the entry points to our VPC to just the ALB and the bastion host.

The AWS environment I used for this post is detailed below. I don't discuss how to setup it up in this post but I will do so in another post. I will link it here when it's finished.

- VPC with 4 subnets: 2 private subnets and 2 public subnets. Instances in the private subnet cannot be accessed directly from the internet but the instances themselves can access the internet (i.e to get software updates and patches, etc).
- An application load balancer placed on the 2 public subnets. The ALB should be able to serve HTTP/HTTPs from anywhere. By design, an ALB has servers on the public subnets. Traffic goes in to these servers, and based on the request's path and host header, it should decide where to direct traffic. 
- A bastion host that can serve SSH traffic from anywhere. We will use this as a way to access all of our instance in the private subnet.

For now, let's start to create our EC2 instance.

# 1 | Creating an EC2 instance

**(1.1)** In the services tab, go to EC2

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/rg4waiig3r0duxsiemot.png)

**(1.2)** On the left-hand side menu, choose Instances. On that page, click "Launch Instance"

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/d02sh44uqg3qsudbsuv7.png)

**(1.3)** Amazon Machine Image

The first step in creating EC2 instances is choosing an Amazon Machine Image (AMI) to create an instance from. Choose the Ubuntu 18.04 AMI.

The most basic AMIs are just plain installations of popular operating systems like Ubuntu, CentOS, Windows, etc. This saves us the pain of installing an OS from scratch (that usually takes hours!!). With AMIs, we get to use our Ubuntu 18.04 EC2 instance in less than a minute.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/9igjium1k0yxoanonugv.png)

**(1.4)** EC2 instance type

We now choose our EC2 instance type. The instance type determines the amount of compute (CPU), memory, and networking resources that will be available to our EC2 instance. AWS provides a wide array of EC2 instance types for every possible workload. You can learn more about them [here](https://aws.amazon.com/ec2/instance-types/)

Choosing which EC2 instance is appropriate for your workload ultimately depends on how much resources your application will use. For this workload, choose t2.micro. The resources should be enough since we would just be installing an NGINX server.

Then, click next.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/tmjkr0kdk58vwmzqkocc.png)

**(1.5)** Network Configuration of the EC2 instance

Now, we will configure our instance. Make sure you are in the correct VPC. For the subnet, choose any *private subnet* with an available IP address (you should see how many available IP addresses there are for that subnet below the field). Set _Auto-assign Public IP_ to disabled.

Then, click next.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ck8kc2w0jse8u4mjge02.png)

**(1.5)** Storage

For the storage of our EC2 instance, keep the default. As of the writing of this post, the default for a root volume is an 8GB volume. A root volume is where the operating system will be installed.

You can provision more volumes (or increase the size of your root volume) as your workload demands.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/k4qv2djabawewfpbahy6.png)

**(1.6)** Tags

Now, we would add tags to our EC2 instance. Tags are key-value pairs. They serve as a way to sort and classify our AWS resources across our account.

Click "add a Name tag". This should show a field where you can add what the instance's Name tag would be. The value should be "nginx-one"

Then, click next.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ys2cy8gsn19dmt9kaeh5.png)

**(1.7)** Security Groups

If we deploy our EC2 instance now, we would not be able to access it at all. This is because the security group of our EC2 instance aren't set up to accept any connections. Security groups are a set of rules for incoming and outgoing traffic. They govern which resources can communicate with a specific set of resources and in what way (i.e allow only connections via port 22 "ssh").

By default, a security group's rules for outgoing traffic are a pass-all (all traffic leaving the instance is allowed). For incoming traffic, however, we are left with the discretion of what resources we want to allow to connect to our EC2 instance and what kind of connections with them we would allow. We can specify these resources in 3 ways:
- a range of IP addresses (i.e allow all computers within the IP range of 192.168.0.0/24 to connect to my instance across all ports).
- a specific IP address (i.e allow 192.168.12.1 to connect to my ec2 instance via port 80 [http]), 
- a security group (i.e allow instances with the security group "bastion-host-sg" to connect to my instance via port 22). 
  - Using this option is easier if the resources you are giving access to are within AWS. This is because you can just keep on adding instances into the chosen security group rather than add a new rule in this security group for every new instance we want to give access to
  - For example: rather than creating a rule to allow SSH traffic from 192.168.12.1 ("EC2 instance A") and another rule to allow SSH traffic from 192.168.12.2 ("EC2 instance B"), we can create a security group ("bastion-host-sg"), add EC2 instance A and B there, and add this security group to the rules of the security group for this EC2 instance ("ec2-nginx-sg"). 
  - With this, security groups serve 2 purposes. They contain a set of rules to govern incoming and outgoing traffic. It also serves as a grouping of AWS resources. This grouping can be referred to by other security groups in their own rules. 

We have to set up the security group of our EC2 instance to be able to accept SSH traffic (so we can connect to it via SSH) and accept traffic from port 80 (http) from the load balancer.

For our setup, we will create a new security group and name it "ec2-nginx-sg". We would allow: 
- port 22 (SSH) connections from the security group of the _bastion host_
- port 80 (HTTP) connections from the security group of the _application load balancer_.

Then, click _Review and Launch_.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/cul4v3wp8kuetrsbtwnv.png)

**(1.8)** Review and choose key pair

After configuring the security group, we would be able to review all the configuration we made. Double check the configuration you made with the instructions in this document. If you're satisfied, click Launch. 

Before launching, AWS will ask us to choose a key pair (or create a new one). A key pair is 2 mathematically related keys: one key you keep, and the other one AWS keeps. In the next section, we would connect to our EC2 instance using the key that we have. AWS will use a mathematical function to verify if the key that we have is "related" to the key that they have. (thus, the basis of "asymmetric cryptography"). If it is, we can access our EC2 instance.

For our example, we would create a new key-pair called "ec2_nginx_kp". We would be asked to download the file (keeping one key) and AWS will keep the other.

Then, finally, click "Launch Instances".

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/z3sc9nlz5wd0zdz151yz.png)

You should see this screen:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ltdj9o4bv8bs2rpi6zdv.png)

# 2 | Configure our EC2 instance

_Understanding Bastion Hosts_

Bastion hosts are EC2 instances located on the public subnet. These instances are usually publicly accessible from anywhere (or from a set of IP addresses). The best practice is that anyone who needs access to any of the computers inside the VPC _must_ SSH into the bastion host first before doing another SSH to the instance they want to go to.

In doing this practice, the point of SSH entry to the VPC is reduced to just the bastion host. It is also best practice that the bastion host is hardened and tightly monitored. Measures like access logging (who accesses the bastion host), automated intrusion detection, tougher security controls are usually in place for the bastion host./

**(2.1)** Access the bastion host via SSH. Copy the keypair you downloaded in 1.8 to the bastion host. I usually just copy paste the PEM file via `nano`. You can use `scp` if you want. Then, run `chmod 400 yourkeypairname.pem`.

**(2.2)** Follow steps 1.1 and 1.2. In the instances page, find the instance you just launched. You can identify it with the name you gave it in step 1.6 (see how tags already made our lives easier~!). Then, click "Connect".

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/72k9bzynf3dl4vknv4o6.png)


**(2.3)** Once inside the EC2 instance, install NGINX. Then, validate if NGINX is really running inside the EC2 instance.

```sh
# update apt and install nginx
sudo apt update
sudo apt install nginx

# you should see its "active (running)"
systemctl status nginx

# validate if NGINX really is running
# - you should see "Welcome to nginx!" in the console
curl localhost
```

# 3 | Connecting your EC2 instance to the load balancer

_Understanding Application Load Balancers (ALBs)_

Traditional load balancers distribute the traffic of a single application to many servers. Application load balancers also distribute traffic to many servers. But it can support many applications, each having their own set of servers called target group. To make this possible, we need to configure rules in the ALB so it can discern where to direct a request.

In understanding ALBs, it is useful to think of a request as something like the hash below. The ALB, operating on the application layer of the OSI stack, sees this hash and uses it to determine where to direct the request. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/nt4qgwwf8s9zn02t2d8m.png)

The ALB has several layers of filtering. The first layer is to check which listener the packet belongs to. It is common for ALBs to have two listeners: port 80 for HTTP and port 443 for HTTPS. If the request does not belong to a listener, it is ignored / dropped (i.e traffic bound for port 22). 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ancqlw84phfaxxufjz67.png)

Each listener has its own set of rules. For the given packet, it is directed to the HTTPS listener. The rules under a listener looks like a long `if` block. The condition on each `if` statement on the block dictate what kind of traffic can get routed to a target group. In the image, the `if` statement with the condition `packet[:host_header] == stocks.jambyblogsite.com` will redirect traffic to the `store_tg(packet)`.

The ordering of the `if` conditions in the block also determine how the request will get served. If a broader `if` condition is above a more narrow `if` condition, the narrow `if` condition won't get served. For example, if the first `if` condition is `packet[:host_header] == '*.jambyblogsite.com'` and the second `if` condition is `packet[:host_header] == finance.jambyblogsite.com`; then, the second `if` condition will never get served.


![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/f8xp0ke13z8wem3nowi6.png)

Once a particular target group has been chosen, an algorithm will choose which instance in the target group the packet will be directed to.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/18xjev7yokhgycrpe2l7.png)

With this functionality, the ALB can truly allow you to serve traffic to thousands of different HTTP/HTTPS-based applications and yet still maintain 2 points of entry in your VPC.

Now, let's dive in on how to make this work with our EC2 instance.

**(3.1)** Follow step 1.1, and then on the left-hand side menu, go to Target Groups. On that page, click "Create Target Group"

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/k9zsksbdgfcaqse7mvqa.png)

**(3.2)** In the window, name the target group "nginx-tg" and leave the defaults, just make sure to select the correct VPC

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4xexszdyfyv3i5980miw.png)

**(3.3)** Select the "nginx-tg" target you just created. Under the targets tab, select "Edit"

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/cfwrfd9afebve538sm31.png)

**(3.4)** A modal will appear. Select the `nginx-one` server you created in Section 1, then click "Add to registered", and click save.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/yczkjfictsle7wr1ryb6.png)

You will see that the "Target group is not configured to receive traffic from the load balancer". We will work on that next.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/62oyefzo1kw6xilyvs67.png)

**(3.5)** Follow step 1.1, and then on the left-hand side menu, go to Load Balancer. Select the appropriate ALB. In the listeners tab, select the HTTP (port 80) listener and select "View / Edit Rules".

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1m1dxyi6b9ryo31dhl3j.png)


_3.6 If you have a public hosted zone_

If you bought your own domain name and you have connected it with Route 53.

**(3.6.1)** On a separate window, open Route 53. Under your Public Hosted Zone, create your own subdomain. For me, my subdomain would be `nginx-web.<mydomainname>.com`

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/8onodpefatvg805yf4j1.png)


**(3.6.2)** From step 3.5, you should see the screen below. Click the (+) button and then select the uppermost "(+) Insert Rule".

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4jy4ixma5qxfd0cb3m88.png)

**(3.6.3)** ALB rules consists of conditions and actions. For this rule, set the condition as "Host Header" with a value of `nginx-web.<mydomainname>.com`. For the action, forward it to target group `nginx-tg`, the one you made in step 3.2

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/0jczr3hjly6j96hub9ce.png)

Then, click the check button. You should see this screen. Then, click save on the upper right.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2kqm59531d6e6ioolxw8.png)


**(3.10)** After a few minutes, you should be able to see the NGINX web page displayed on your browser with the host value `nginx-web.<mydomainname>.com`

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/25siwhlbqmkeis9dhfmg.png)