Skip to main content

Creating a Secure Web Application from Scratch with a Bastion Host, NAT Gateway and Application Load Balancer in AWS

Hands-On Lab


Photo of

Training Architect





In this live environment, you will learn how to create a highly-secure web application on AWS. This application will use an Application Load Balancer to route traffic between EC2 webservers that are otherwise inaccessible from the open internet. Because the application instances cannot be accessed over the public internet, we will create a bastion host instance and allow it to access those instances. Additionally, we will create a NAT Gateway in order to allow instances themselves to establish connections with outside servers, for the purposes of installing software packages. For information on using PuTTY for Windows to SSH through a Bastion Host, see: The code for this lab is below: chmod 400 security.pem ssh-add -K security.pem (note: 'ssh-add -K' is mac specific. For alternatives, see: ) ssh -A ec2-user@BASTION-HOST-PUBLIC-IP ssh ec2-user@PRIVATE-IP-OF-EC2-INSTANCE sudo yum update -y sudo yum install -y httpd sudo service httpd start sudo chkconfig httpd on exit

What are Hands-On Labs?

Hands-On Labs are scenario-based learning environments where learners can practice without consequences. Don't compromise a system or waste money on expensive downloads. Practice real-world skills without the real-world risk, no assembly required.

Creating a Secure Web Application from Scratch with a Bastion Host, NAT Gateway and Application Load Balancer in AWS

In this lab, we are going to create secure web application on AWS from scratch, with a bastion host, NAT gateway, and an Application Load Balancer.

For information on using PuTTY for Windows to SSH through a Bastion Host, see:

Here's a quick outline of what we'll be creating:

  • Gateway VPC
  • Four subnets
    • Two private and two public
  • Three security groups
    • They will control access in or out of different parts of our application
  • Three EC2 instances

Create the VPC and the Internet Gateway

Navigate to Services, and then click VPC in the Networking & Content Delivery section.

Here, we'll see a Launch VPC Wizard button. If we click that, many of the VPC's features would be configured for us. The point of this lab, though, is to get familiar with all of these options, so we'll do everything manually. Instead of Start VPC Wizard, click Your VPCs in the left-hand menu.

Now, we can click the blue Create VPC button up at the top. We'll land on a new screen. Enter Lab-VPC in the Name tag field, and in the IPv4 CIDR block field, type We can leave the IPv6 CIDR block selection as No IPv6 CIDR Block, since we don't need any IPv6 addresses here. We can also leave Tenancy set to Default. Click Create when everything looks good.

Click Close to get back to the Your VPCs page, and we can see our Lab_VPC was created with all the options we set. Note the VPC ID column. That column for our newly created VPC will say something like vpc-xxxxxxxx.

Create an Internet Gateway

In order for anything within this VPC to talk to the open internet, we've got to set up an internet gateway.

Click Internet Gateways in the left menu, and click Create internet gateway near the top of the screen.

All we need is something in the Name tag field, so let's enter "Lab-IG" (short for "lab internet gateway") and click Create on the right. When it's created successfully, it's safe to hit Close on the right.

Back in the Internet Gateway screen, we can see the gateway was created, but it has a state of detached. We've got the two components created, but they're not yet talking to each other.

Make sure the box is selected (the left-most column), and then choose Attach to VPC from the Actions menu up near the top of the screen. We'll be taken to another screen with a dropdown containing our VPCs. We've only got one, so let's select the one we just created (that has the VPC ID from the last step, vpc-xxxxxxxx), and click Attach.

As things sit currently, we've got a VPC:

  • It resides in the us-east-1 region.
  • It has a CIDR block of

We've also created an internet gateway and attached it to our new VPC.

We're good to go here. Let's move on to subnets and route tables.

Creating Subnets

What we've done so far is great, but there are still no routes for traffic between the various moving parts we'll have inside our VPC. We need to set up four subnets: two public and two private. And we'll be setting one each (a public one and a private one) in each of the two availability zones.

Create First Public Subnet

First off, navigate to Subnets (a link in the left-hand menu), and we'll see we've got nothing set up here. Click Create Subnet to make our first public subnet.

For a Name tag, we can call it whatever we want. To stick with our current naming convention, though, and maybe just keep things simpler, we'll use "public1", like in the lab video. The VPC should have autopopulated, but choose vpc-xxxxxxxx | Lab_VPC from the dropdown if it didn't.

Set us-east-1a as an Availability Zone, and set the IPv4 CIDR block to Click Create, and then Close. We will run through this process three more times, using these specifications:

Create Second Public Subnet

  • Name tag: public2
  • Same VPC
  • Availability Zone: us-east-1b
  • IPv4 CIDR block:

Create First Private Subnet

  • Name tag: private3
  • Same VPC
  • Availability Zone: us-east-1a
  • IPv4 CIDR block:

Create Second Private Subnet

  • Name tag: private4
  • Same VPC
  • Availability Zone: us-east-1b
  • IPv4 CIDR block:

Note the names of all four subnets once we're done creating them. The words public and private are in the names themselves, but have no bearing on whether they are in fact public or private subnets. What does determine this is whether or not the subnets can talk to the outside world, and we'll set this up with route tables.

Before we do that, though, let's have a look at NACLs (network ACLs).


If we check a box next to any of our subnets, the lower half of the screen will show some data. There will be four tabs, each containing relevant information we can edit if we want. Check any of the subnets, and look at the Network ACL tab in the lower half of the screen.

They're all set up the same way, allowing all traffic to and from the subnets. This is fine for this environment, because we're going to be controlling traffic via security groups. But it's handy to know we can also block or allow traffic with NACLs.

Okay, let's get back on track and head into route tables.

Creating Route Tables

We'll be setting up two route tables: a private and a public. The public route table will have a connection to our gateway. This will allow any instances we deploy in those subnets to communicate with the outside world. Let's get this process moving by clicking Route Tables in the left-hand VPC Dashboard menu.

We'll see right off there's already a default route table. All VPCs have these, and it's best to leave them alone. If we need to customize something, we're better off creating a new one. So let's click Create Route Table. In the dialog that pops up, we'll use "RT-Public" as a Name tag, ensure the VPC dropdown is set to our new vpc-xxxxxxxx | Lab_VPC, and click Create.

Now, we've got to create the private route table. We'll again click Create Route Table, use "RT-Private" as a Name tag this time, make sure we've got vpc-xxxxxxxx | Lab_VPC set in the VPC dropdown, and hit Create.

Associating Route Tables

The Public Route Table

We're not quite done with the RT-Public yet. We've got to create an additional route, besides the route to local. Make sure the checked box next to Public-RouteTable is highlighted (and that it is the only box checked) in our list of route tables. On the bottom of the dashboard screen, click the Subnet Associations tab, and then click Edit subnet associations.

We're going to associate the two public subnets by checking their boxes and clicking Save. Once we've done that, we can see back in the console that the Explicitly Associated column now shows 2 Subnets for our RT-Public route table.

Then, also on the bottom of the dashboard screen, click the Routes tab, and the Edit routes button. We need to click Add route near the bottom. First, we'll set a Target, and we'll choose Internet Gateway, then select the internet gateway we created named something like igw-xxxxxxxx. As a Destination, we'll enter "", which essentially means any destination. This rule, once we click Save routes, will route any traffic through the public route table and out onto the internet.

We can hit Close and get back to the Route Tables screen.

The Private Route Table

We weren't quite done with the RT-Private yet either. Let's make sure the checkbox next to it is selected (and only the RT-Private checkbox) and head down to the bottom section of the dashboard.

We can leave the Route tab alone, because this route table will only be routing locally. But in the Subnet Associations tab, click the Edit subnet associations button, and associate this route table with the two private subnets by checking the boxes next to them. Once we click Save, we're done with this part of the exercise.

Make a note, though, about something that might be handy to remember down the road: A subnet can only be associated with one route table at a time. Route tables can be associated with multiple subnets, as we just illustrated, but the reverse isn't true.

Create Security Groups

Let's click the Services dropdown menu, way up at the very top of the screen, and open EC2 in a new tab. In the left sidebar, click Security Groups under Network & Security.

There's a default one sitting there already, but we're going to need some custom ones with custom security rules for each.

Bastion Security Group

Click Create Security Group up near the top of the window. In the window that pops up, we've got some data to enter. As a Security group name, we'll use "Bastion-SG", and we'll use that as a Description too. For VPC, we need to select vpc-xxxxxxxx | Lab_VPC (the one we created earlier).

We're not done, though, because we've still got to make some rules. In the lower half of that popout window, we can click Add Rule. In the Inbound tab, we're going to allow SSH (in the Type dropdown), then set the Protocol as TCP, a Port Range of 22 (SSH's port), and a Source of Anywhere. There's a spot for adding particular allowed IP addresses, but we're going to just set it for wide open IPv4 and IPv6 by having, ::/0 in this text box. We can put whatever Description we want, but something like "SSH access" will be fine.

Clicking Create will set it.

Back in the main Security Groups window, we can highlight the Bastion-SG we just created, and go look at the Inbound tab. We made one rule, but we see two: one for IPv4 and one for IPv6. And if we go look at the Outbound tab, we'll see all traffic is allowed. This is fine, so we can leave it alone.

ALB Security Group

We've got more work to do with security groups and rules for them. Click Create Security Group again to make another with these specs:

  • Security group name: ALB-SG
  • Description: ALB-SG
  • VPC: vpc-xxxxxxxx | Lab_VPC
  • Rules:
    • Inbound:
      • Type: HTTP
      • Protocol: TCP
      • Port range: 80
      • Source: Anywhere and, ::/0
      • Description: Web Traffic
    • Outbound:
      • The defaults are fine. They allow traffic to go out anywhere.

> Note: After the ALB group is created, we ought to refresh our browser. There appears to be some sort of caching (UI or browser) going on, and, with no refresh, these two security groups won't show up in a list of sources during the next step.

EC2 Security Group

This will be a similar process, but pay attention here. We need two Inbound rules. Also note that when we get to the Source column, our previously created security groups might not show up in the list.

  • Security group name: EC2-SG
  • Description: EC2-SG
  • VPC: vpc-xxxxxxxx | Lab_VPC
  • Rules:
    • Inbound:
      • Type: SSH
      • Protocol: TCP
      • Port range: 22
      • Source: Custom and Bastion-SG Group ID (start typing "sg-" here and we can pick our Bastion-SG from a list)
      • Description: SSH Access for Bastion
    • Inbound:
      • Type: HTTP
      • Protocol: TCP
      • Port range: 80
      • Source: Custom and ALB-SG Group ID (start typing "sg-" here and we can pick our ALB-SG from a list)
      • Description: Web Access for ALB
    • Outbound: Again, we can leave this alone.

Notice this HTTP rule is only allowing traffic to the EC2 instances from our Application Load Balancer, not just the general internet.

As it sits, we have three custom security groups:

  • Bastion-SG, which is allowing 22 in and then allowing it along to the two EC2 instances.
  • ALB-SG, an Application Load Balancer that will take all HTTP traffic and pass it along to the two EC2 instances.
  • EC2-SG, a security group that will contain our two EC2 instances once we create them. This allows SSH traffic from the bastion host and HTTP traffic from the ALB. No other traffic is allowed in.

This is a pretty secure setup. To tighten things up, we may want to limit the allowed sources in the Inbound rules Source to IPs on our corporate VPN, or maybe IPs of developers that may be working on things and need access. For now, though, we're going to leave things the way they are.

Creating a NAT Gateway

We should still have a web browser tab open to the VPC Dashboard. Over there, let's click Nat Gateways, and then click Create Nat Gateway.

In the Subnet box, we need to select the second public one we created earlier. In our case, that's going to be the public2 subnet. Click on it in the dropdown, and then move along to the next box.

We need to select an Elastic IP Allocation ID from the dropdown. Whoops, we don't have one. We can create one easily enough, though, by clicking Create new EIP. An EIP will then show up in the dropdown, and now we can click Create a Nat Gateway.

We need to edit the gateway and make sure it will route traffic to everything properly, but first let's click Close.

We're going to head back in to Route Tables in the left-hand menu, and edit the Routes tab for our RT-Private route table. Don't forget to first make sure that only the checkbox for the desired route table is checked. We left this alone before, but now we've got to add a route to the open internet. Click on the Edit routes button (in the lower part of the window) and then click Add route.

As a Target (and make sure you're looking in the Target column, not the Destination column), we'll set the new gateway (nat-xxxxx) and set the Destination to What we've done here is associated our route table with the NAT gateway, so the two EC2 instances will be able to get out onto the internet, like if they need to grab a software update. But nothing can come directly in from the internet, which is how we want it.

Clicking Save routes will get us saved and out of here. Then, we can click Close to get right out of the routes screen entirely.

Create Our EC2 Instances

So far, we've created a lot of our application's infrastructure. We've got VPCs, subnets, route tables, security groups, and the NAT gateway. We've yet to make any EC2 instances, though, and we need three. One will be the bastion host, and two will be serving out our application.

Let's get out of the VPC Dashboard and into the EC2 Dashboard.

The First Private EC2 Instance

In the Instances option, in the left-hand menu, we're going to click Launch Instance. This will be one of the EC2 instances serving out our application, and it will sit in our private3 subnet. We'll click the Amazon Linux AMI image, and then select the t2.micro Type on the next screen (using the checkbox on the left). It's the row that says (in green) "Free tier eligible" in the Type column. Next, we'll click Next: Configure Instance Details.

There's a form on this next screen. We're going to set the Network dropdown to our vpc-xxxxxxxx | Lab_VPC. As a Subnet, select the private3 one. The next dropdown, Auto-assign Public IP, needs to be set on either Use subnet setting (Disable) or just plain old Disable. This is in our private subnet, so we don't want it getting a public IP at all, and either of those options will work.

We don't need to do any more in this form, so let's click Next: Add Storage. We're not doing anything at all here, so let's breeze through. Click Next: Add Tags button, and then click Next: Configure Security Group.

There's some work to do here. In the Assign a security group spot, we want to Select an existing security group. Once we've selected that, we'll see a list containing all the security groups we created. Select the one we made for EC2 instances, called EC2-SG. This is going to allow HTTP traffic that's coming from the ALB, as well as SSH traffic coming from the bastion host. We can click Review and Launch now.

We're greeted with another window, where we have to click Launch. This brings up yet another window where we have to either create or select an existing key pair. We don't have one, so we've got to create one. We'll name it something simple, like "security", and then click Download Key Pair. Now we can fire this EC2 instance up by clicking Launch Instances. Then click View Instances to watch it fire up.

The Second Private EC2 Instance

We've got to make our second EC2 instance, and the procedure is the same, except for a few minor differences.

On the first Configure Instance Details page, we're going to set the Subnet to our private4 subnet. In the last screen, at the key pair window, use the one (security) we created for the EC2 instance we just launched.

We can click Launch Instances and move along to the next instance.

The Bastion Host

This will be a similar process as the other two, but with a few differences. We're going to leave this instance in the public1 subnet, because we want traffic from the internet hitting it. In the Auto-assign Public IP dropdown, we want to select Enable. This will give us a public IP we can use to access the bastion host.

We'll click Next: Add Storage and Next: Add Tags, making no changes in either of those, and then click Next: Configure Security Groups. Here, we're going to pick a different security group. Select the one we named Bastion-SG. This is what's going to allow SSH access in from the internet. Once that's done, we can click Review and Launch.

Again, we'll use the key pair we used for the other two EC2 instances.

Back at the Instances screen, we can click View Instances to watch as they spin up. Let's take a quick look around. We'll see one of our instances, the bastion host, has a public IP. The other two don't. But if we click on one of them, we can find the private IP down in the lower part of the screen.

While we're here, we can make life a little easier and give them meaningful names. The Name column is blank. If we hover over that column, in an EC2 instance's row, a pencil should show up. When we click that, we can edit the name. Let's name the bastion host "bastion", one of the other instances to "web1", and the other instance to "web2". This will just make it easier during some of the next steps to see at a glance which EC2 instance we're dealing with.

And also, while we're here, let's make note of where the IP addresses for these instances are found. We're going to need them all in a bit once we start logging in and installing software.

Once these are all launched completely and renamed, we can proceed.

Installing Software

You can find the IP in the Instances screen of the EC2 Dashboard by highlighting the instance in question and looking at the lower part of the screen. We'll need bastion's IP first, the public one. Let's open up a terminal and get in.

There should be a security.pem file sitting in our Downloads directory. Before we can use it, we have to modify permissions. Let's get into Downloads and run a quick chmod:

[user@$host]$ cd Downloads
[user@$host]$ chmod 400 security.pem

This means the file owner (us) has read privileges. Nobody else (group or others) has any permission at all.

Use the SSH Key

To use it (add the key to our SSH agent), we'll run:

[user@$host]$ ssh-add -k security.pem

Note that on a Mac, the appropriate command will be:

[user@$host]$ ssh-add -K security.pem

Get into the bastion host with:

[user@$host]$ ssh -A ec2-user@BASTION-HOST-PUBLIC-IP

Let's stop a minute and clear something up before there's any confusion. Back in the EC2 Dashboard, there is a Connect button up at the top of the screen. Clicking that (with the bastion EC2 instance row highlighted) will show you an ssh command you can use as well:

[user@$host]$ ssh -i security.pem ec2-user@BASTION-HOST-PUBLIC-IP

It will also get us in, but that's only going to let us get as far as the bastion host. With the other command (ssh -A...) we'll be able to leapfrog into the other two EC2 instances as well with the same security credentials.

Once we run the ssh command, we're asked if we're sure about connecting. We are, so type yes at the prompt, and we'll be dumped into a shell.

Now that we're in, we've got to keep going into one of the other EC2 instances that will be serving out our application.

Installing Software on web1

Back in the EC2 Dashboard, find the private IP of the instance we named web1. Once we've got that, let's get in with:

[user@$bastion]$ ssh ec2-user@PRIVATE-IP-OF-EC2-INSTANCE

Now we're sitting in one of our private EC2 instances. We're going to run several commands here to both update the OS on the instance and install a web server.

Update the EC2 instance (the -y switch will just answer yes automatically for everything):

[user@$web1]$ sudo yum update -y

Install the web server:

[user@$web1]$ sudo yum install -y httpd

Start the web server up:

[user@$web1]$ sudo service httpd start

Make sure it starts after a reboot:

[user@$web1]$ sudo chkconfig httpd on

Log out of the EC2 instance:

[user@$web1]$ exit

See how this instance is able to go out and get the software? This is because of the NAT gateway we set up earlier. If we hadn't done that, these private EC2 instances wouldn't be able to access the internet.

Installing Software on web2

We should still be sitting in the bastion host. Let's log in to our second private EC2 server. Grab the private IP from the EC2 dashboard (highlight the web2 row, and find the private IP in the lower part of the screen) and run:

[user@$bastion]$ ssh ec2-user@PRIVATE-IP-OF-EC2-INSTANCE

We need to run the exact same commands we did on web1. Here they are, minus the explanations:

[user@$web2]$ sudo yum update -y
[user@$web2]$ sudo yum install -y httpd
[user@$web2]$ sudo service httpd start
[user@$web2]$ sudo chkconfig httpd on
[user@$web2]$ exit

Now, we can get out of the bastion host as well, and land back at our terminal.

[user@$bastion]$ exit

Set Up the Application Load Balancer

We've arrived at the last piece in our project. This is what will take traffic from the internet and pass it along to our private EC2 instances that are serving web traffic.

Back in the EC2 Dashboard, find Load Balancing in the left menu and select Load Balancers. In the upper left of the next screen, we'll click Create Load Balancer. Then on (yet another) screen, we've got three options. There's an Application Load Balancer, a Network Load Balancer, and a Classic Load Balancer. We're going to click Create on the Application Load Balancer section. We've got some information to fill out on the next screen.

We'll give it a Name of "Lab-ALB", leave the Scheme set to internet-facing, and leave the IP address type set to ipv4.

The next section is fine (HTTP protocol on port 80), but we've got a bit of work to do down in the Availability Zones section. Two zones should be sitting there, and we need to select each in order to choose the subnets we want. In us-east-1a, choose our public1. In us-east-1b, we'll choose public2. This sort of gives our ALB a presence in each of those zones. Once those are set, we can click Next: Configure Security Settings. We're not changing anything here, so we can just click Next: Configure Security Groups.

> Note: Skipping over HTTPS isn't necessarily a good idea in the real world. Our focus here, though, is on securing the actual application and framework, not the transport layer. Setting up an app that uses HTTPS is definitely more desirable, but beyond the scope of what we're trying to do here.

Instead of leaving this ALB in the default security group, we're going to place it in ALB-SG. There are three things to watch for here. We need to make sure we've selected Select an existing security group in the Assign a security group section. We've also got to make sure we select the ALB-SG group, and uncheck the default security group. Once we're sure we've got all that set, we can click Next: Configure Routing.

On the next screen, we're greeted with another form. We're going to leave Target group set to New target group, give it a Name of ALB-TG (short for "ALB Target Group") and leave the rest alone. The last thing we'll change is something we don't see yet. Let's click Advanced health check settings to see some more options. We're going to change Healthy threshold from 5 to 2. This will just speed up the process of determining whether or not our ALB is healthy or not.

It's safe to move on, so we can click Next: Register Targets.

This screen is where we'll actually declare which instances we want to use for serving the traffic through the ALB. We're going to select the two EC2 instances that are in the EC2-SG security group. Those are the ones we just logged into and installed web servers on. Once they're selected, we can click Add to registered just above the list of EC2 instances. We should see those two instances show up in the top part of the screen, in the list of registered targets. We can keep moving and click Next: Review. Stay down in that vicinity, and click Create right after.

Now we can sit back, but only for a few seconds, and watch Lab-ALB get created. Once it's successful, we can close the window (using the blue Close button), and now we should be back in the Load Balancers screen to see that it's got a provisioning state. While we're waiting for it to become active, let's copy the DNS from the lower part of the screen, which should be something like We can paste that into another web browser tab or window. It won't work right off, so we'll give it a couple minutes before we try.

Once the status of Lab-ALB is active, we can refresh the web browser we opened up a few minutes ago. We should see the Amazon Linux AMI Test Page, which looks eerily similar to the default page on a fresh Apache web server. Seeing this page means we've successfully installed and set up everything we needed to in this lab environment.


Congratulations. This environment is ready for some web developers to get to work.