Effective security requires close control over your data and resources. Bastion hosts, NAT instances, and VPC peering can help you secure your AWS infrastructure.

Welcome to part four of my AWS Security overview. Last week we looked at network security at the subnet level. I want to stay on the topic of network security, but this time look at using a bastion host to tighten access to your resources, and NAT instances and VPC peering to avoid unnecessarily exposing your data to the Internet.

What is a bastion host and do I need one?

Bastion hosts are instances that sit within your public subnet and are typically accessed using SSH or RDP. Once remote connectivity has been established with the bastion host, it then acts as a ‘jump’ server,  allowing you to use SSH or RDP to login to other instances (within private subnets) deeper within your network. When properly configured through the use of security groups and Network ACLs, the bastion essentially acts as a bridge to your private instances via the Internet.

You may ask yourself, “Do I need one of those in my environment?” If you require remote connectivity with your private instances over the public Internet, then I would say yes!

This diagram shows connectivity flowing from an end user to resources on a private subnet through an bastion host:

AWS bastion host - connection

When designing the bastion host for your AWS infrastructure, you shouldn’t use it for any other purpose, as that could open unnecessary security holes. You need to keep it locked down as much as possible. I would suggest you look into hardening your chosen operating system for even tighter security.

Here are the basic steps for creating a bastion host for your AWS infrastructure:

  • Launch an EC2 instance as you normally would for any other instance.
  • Apply your OS hardening as required.
  • Set up the appropriate security groups (SG).
  • Implement either SSH-Agent Forwarding (Linux connectivity) or Remote Desktop Gateway (Windows connectivity).
  • Deploy an AWS bastion host in each of the Availability Zones you’re using.

Security groups are essential for maintaining tight security and play a big part in making this solution work (you can read more about AWS security groups here). First, create a SG that will be used to allow bastion connectivity for your existing private instances. This SG should only accept SSH or RDP inbound requests from your bastion hosts across your Availability Zones. Apply this group to all your private instances that require connectivity.

Next, create a security group to be applied to your bastion host. Inbound and outbound traffic must be restricted at the protocol level as much as possible. The inbound rule base should accept SSH or RDP connections only from the specific IP addresses (usually those of your administrators’ work computers). You definitely want to avoid allowing universal access (0.0.0.0/0). Your outbound connection should again be restricted to SSH or RDP access to the private instances of your AWS infrastructure. An easy way to do this is to populate the ‘Destination’ field with the ID of the security group you’re using for your private instances.

SSH and RDP connections require private and public key access to authenticate. This does not pose a problem when you are trying to connect to your bastion host from a local machine, as you can easily store the private key locally. However, once you have connected to your bastion host, logging in to your private instances from the bastion would require having their private keys on the bastion. As you will probably already know (and if not, then take careful note now), storing private keys on remote instances is not a good security practice.

As a result, I suggest that you implement either Remote Desktop Gateway (for connecting to Windows instances) or SSH-agent forwarding (for Linux instances). Both of these solutions eliminate the need for storing private keys on the bastion host. AWS provides great documentation on how to implement Windows Remote Desktop Gateway and SSH-agent forwarding.

As with all cloud deployments, you should always consider the resiliency and high availability of your services. With this in mind, I recommend deploying a bastion within each Availability Zone that you are using. Remember: if the AZ hosting your only AWS bastion host goes down, you will lose connectivity to your private instances in other AZs.

NAT instances for AWS infrastructures

A NAT (Network Address Translation) instance is, like an bastion host, an EC2 instance that lives in your public subnet. A NAT instance, however, allows your private instances outgoing connectivity to the Internet, while at the same time blocking inbound traffic from the Internet. Many people configure their NAT instances to allow private instances to access the Internet for important operating system updates. As I’ve discussed previously, patching your OS is an important part of maintaining instance level security.

Launching a NAT instance inside an AWS VPC

To create and launch a NAT:

  • Create a Security Group which will be applied to your NAT.
  • Select a pre-defined AMI and configure it as you normally would any other EC2 instance.
  • Set up correct routing.

Once your NAT is launched, it’s important to disable source/destination checks. To do this, right click on your NAT Instance within the AWS Console and select ‘Networking > Change Source/Dest. Check > Yes, Disable’.

When creating a security group for your NAT, make sure that you allow inbound traffic from your private instances through the HTTP (80) and HTTPS (443) ports to allow for OS and software updates. Your outbound rule set should have an open destination of 0.0.0.0/0 for port 80 and 443 as well. If your instances will require any other ports opened, this is where to do it.

AWS provides some Amazon Machine Images (AMIs) that are already pre-configured as NAT instances – I recommend you consider using one. NAT AMIs have names that include the string ‘amzn-ami-vpc-nat’ so they’re easy to find by searching from the Community AMI tab in Step One when launching an EC2 instance. These AMIs are a good idea, as they’re configured right out of the box for IPv4 forwarding and iptables IP masquerading. ICMP redirects are disabled.

You will now have to modify the route table used by your private subnets. Make sure you have a route pointing to the outside world (0.0.0.0/0) via your new NAT instance. Your NAT-instance must be launched within your public subnet and have a public IP address. The route table of your public subnet where your NAT resides must have a route to the outside world by way of your Internet Gateway. This will ensure that any request from your private instance will first go to the NAT, and the NAT will forward that traffic out via the IGW to the Internet.

Your NAT is now set up and your private instances should be able to communicate with the outside world for updates etc., using ports 80 and 443. However, it’s important to note that connections initiated from the Internet will not reach your private instances – as this configuration protects them.

Introduction to AWS VPC peering (Virtual Private Cloud)

AWS VPC Peering lets you connect two VPCs together as a single network. It allows you to share resources between AWS VPCs without routing data through the Internet or a VPN connection. AWS VPC peering provides a tight and secure shared environment while minimizing external exposure.

Peered VPCs communicate across their private IP blocks, therefore it’s important to ensure that the two VPCs do not have overlapping CIDR Address ranges.  It’s also important to note that you are unable to directly reference a security group from one VPC to the other. Instead, you’ll need to enter a CIDR Block or specific IP address in the Source/Destination section of your SG rules.

AWS VPC peering design

When setting up a peered connection, one VPC acts as the requester (the VPC initiating the connection) while the other acts as a peer. Before a connection can be established, the owner of the peer VPC has to acknowledge the request and accept the Peering connection. Once a connection has been established, routing between the CIDR blocks of each VPC needs to be added to a route table to enable resources within the networks to talk to each other via the private IP address range.

From a design perspective, you are not able to daisy chain VPCs together expecting them all to talk together across one large network. Each AWS VPC will only communicate with its ‘requester’ or ‘peer’. As an example, if you have a peering connection between VPC 1 and VPC 2, and another connection between VPC 2 and VPC 3 as below:

VPC peering

…Then VPC 1 and VPC 2 could communicate with each other directly, as can VPC 2 and VPC 3. However, VPC 1 and VPC 3 could not. You can’t route through one VPC to get to another.

To allow VPC 1 and VPC 3 to talk directly, you would have to implement a separate peering connection between VPC 1 and VPC 3 as shown below:

VPC peering

AWS VPC peering provides an excellent secure and trusted connection between VPCs for enhanced management and resource sharing. Depending on the way you’ve configured your VPCs, there may be many reasons why you would want to incorporate such an architecture into your environment. AWS offers some possible scenarios of their own that are definitely worth exploring.

Let’s summarise what we’ve covered this week:

  • Bastion host. How an AWS bastion host can provide a secure primary connection point as a ‘jump’ server for accessing your private instances via the Internet.
  • NAT instance. How a NAT instance can provide your private instances with access to the Internet for essential software updates while blocking incoming traffic from the outside world.
  • VPC Peering. How VPC peering can be used to create secure connectivity between two VPCs to enable resource sharing.

Next week I’ll be looking at AWS’s Identity Access Manager Service (IAM) and how to create and manage users, groups, and roles, as well as MFA (Multi Factor Authentication).

Thank you for taking the time to read my article. If you have any feedback please do leave a comment below.

  • Ralf Bergs

    It is not a good idea to use “SSH-agent forwarding,” because this can pose a security risk. Instead you should invoke ssh with “-W” to forward stdin/stdout to your internal destination host.

  • Jaroslav

    Ralf is correct. You should consider using ProxyCommand instead of agent forwarding. I describe it here: http://www.jrslv.com/ssh-bastion/

  • Prince Cassius

    Awesome article. Can you please add links to your article series or better yet, a way to receive email updates for articles?

    • Diego Tiziani

      We publish 3-4 new articles every week and at the moment it’s not possible to subscribe for getting updates on a new blog post. We might add this feature in the next blog version though. Thanks for your feedback!

  • Alaeddine Zaier

    Thank you this is a great article! But you should use ProxyCommand instead of agent forwarding!