Creating a Static NAT in AWS

In general, it is not a good idea to have all of your infrastructure sitting on the public internet. AWS solves this issue by creating VPCs (Virtual Private Clouds) which have private subnets to sequester servers that should not be directly exposed to the internet. While this allows for greater security, it also creates a few problems when you want to access specific services from the internet that reside in the private subnet. One example would be an application where the web interface should be public facing, however all of the other services and ports opened on the instance should not be publicly accessable.

To get around the accessability issue, there are a few solutions to consider. The first of which would be having the instance in question stradle the public and private subnets allowing it to have an IP in each and set up security groups to lock down access to specific ports. While this approach works, it also requires an Elastic-IP which by default are limited to 5.

Another solution may be to use an Elastic Load Balancer (ELB) or Application Load Balancer (ALB). This solution is a good approach for production envrionments and allows for proper loadbalancing between VMs, terminating TLS at the load balancer, easily configuring ACLs, and more. The downsinde is the additional cost, expecially for a small personal dev environment where where costs are a large concern.

The other solution to consider is using a static NAT. This approach leverages the fact that if you have a private subnet, a NAT instance or NAT gateway is required for internet access. By adding a few rules to iptables, the NAT can also be configured to allow traffic in to an instance in the private subnet. For example, it can be configured to allow http and https traffic through to the web interface of concourse while keeping the rest of the instance sequestered in the private network. This approach also has the advantage of not requiring an additional Elastic-IP.

Static Nat Terraform Template

Using Terraform, two files are required for provisioning a NAT instance with custom rules. The first of which is the terraform file for creating the instance itself. As shown below, terraform will provision a t2.medium for the instance using the static-nat key for ssh. It is placed in the public subnet (sometimes referred to as a DMZ) and added to a security group that has access to the private subnet as well as allows access from the internet to the NAT instance.

# aws-nat.tf
# Terraform file for creating a NAT instance with custom nat rules
variable "aws_nat_ami" {
    default = {
        us-east-1 = "ami-01623d7b"
        us-east-2 = "ami-021e3167"
    }
}
resource "aws_instance" "nat" {
	ami = "${lookup(var.aws_nat_ami, var.aws_region)}"
	instance_type = "t2.medium"
	key_name = "static-nat"
	security_groups = ["${aws_security_group.nat.id}"]
	subnet_id = "${aws_subnet.public.id}"
	associate_public_ip_address = true
	source_dest_check = false
	user_data = "${file("nat-user-data.yml")}"
	tags {
		Name = "nat"
	}
}
resource "aws_eip" "nat" {
	instance = "${aws_instance.nat.id}"
	vpc = true
}
resource "aws_security_group" "nat" {
	name = "nat"
	description = "Allow services from the private subnet through NAT"
	vpc_id = "${aws_vpc.default.id}"
	ingress {
		from_port = 80
		to_port = 80
		protocol = "tcp"
		cidr_blocks = ["0.0.0.0/0"]
	}
	ingress {
		from_port = 443
		to_port = 443
		protocol = "tcp"
		cidr_blocks = ["0.0.0.0/0"]
	}
	ingress {
		from_port = 0
		to_port = 65535
		protocol = "tcp"
		security_groups = ["${aws_security_group.private.id}"]
	}
	ingress {
		from_port = 0
		to_port = 65535
		protocol = "udp"
		security_groups = ["${aws_security_group.private.id}"]
	}
	egress {
		from_port = 0
		to_port = 0
		protocol = "-1"
		cidr_blocks = ["0.0.0.0/0"]
	}
	tags {
		Name = "${var.aws_vpc_name}-nat"
	}
}

When the NAT instance is provisioned, it leverages user_data to run commands on the instance when it is launched. In this case, the rules added are to allow http and https traffic to our Concourse instance located at 10.10.1.10. As part of the rule it is important to exclude the private subnet otherwise traffic will be routed via the nat even if it could directly contact the internal instance.

#!/bin/bash
# Script for adding static nat rules to the instance provisioned
# by the terraform script
sudo iptables --table nat --append PREROUTING ! -s 10.10.0.0/16 \
    --protocol tcp --dport 80 --jump DNAT --to-destination 10.10.1.10:80
sudo iptables --table nat --append PREROUTING ! -s 10.10.0.0/16 \
    --protocol tcp --dport 443 --jump DNAT --to-destination 10.10.1.10:443

Caveats

While a static NAT works well to solve this issue, it also has some caveats.

If two instances behind the NAT are running on the same port, one will have to be contacted via a non-standard port. For example, for two webservers, one could be contacted on port 80, the other on port 8080.

This solution is not recommended for production systems – the use of an ALB or ELB is better suited for a HA workload.

Spread the word

twitter icon facebook icon linkedin icon