DEV Community

Charles Uneze
Charles Uneze

Posted on • Updated on

Securing your VPC using Public & Private Subnets using Terraform

In this lab, two instances are created, one is launched in a first subnet which is public, while the other is launched the second subnet which is private.

The private instance must access the internet using a Network Address Translation (NAT) gateway deployed into the first subnet.

No default security group, NACL, or route table is used in this demonstration. This means any non-default being used must be explicitly associated with its resource. For example, a non-default route table must be explicitly associated with a subnet, etc.

Also, in this lab, we will explore the stateful nature of a Security Group, at Algorithm no 12.

Image description

Algorithm of the Infrastructure

  1. Create a VPC
  2. Create an Internet Gateway
  3. Create a first subnet
  4. Create a first NACL for the first subnet
  5. Specify the first route table
    • Add a route destined to all IP addresses, and a target to the Internet Gateway.
  6. Create a security group in the VPC for the first subnet
    • Add an ingress rule for SSH
    • Add an egress rule for SSH
  7. Create an instance for the first subnet
  8. Create an elastic IP
  9. Create a NAT Gateway in the first subnet and attach the elastic IP to it
  10. Create a second subnet
  11. Create a second NACL for the second subnet
  12. Create a second security group
    • Add an ingress rule for SSH
    • Add an egress rule for all ICMP
  13. Create a second route table
    • Add a route destined for the NAT Gateway
  14. Create a second instance

Create a VPC

CIDR = 10.0.0.0/16

resource "aws_vpc" "VPC_A" {
  cidr_block       = "10.0.0.0/16"
  instance_tenancy = "default"

  tags = {
    Name = "VPC_A"
  }
}
Enter fullscreen mode Exit fullscreen mode

Create an Internet Gateway

resource "aws_internet_gateway" "VPC_A_IGW" {
  vpc_id = aws_vpc.VPC_A.id

  tags = {
    Name = "VPC_A_IGW"
  }
}
Enter fullscreen mode Exit fullscreen mode

Create a first subnet

resource "aws_subnet" "VPC_A_Public_Subnet_A" {
  vpc_id                  = aws_vpc.VPC_A.id
  cidr_block              = "10.0.20.0/24"
  availability_zone       = "us-west-2a"
  map_public_ip_on_launch = "true"

  tags = {
    Name = "VPC_A_Public_Subnet_A"
  }
}
Enter fullscreen mode Exit fullscreen mode

Create a first NACL for the first subnet

resource "aws_network_acl" "VPC_A_Public_NACL_A" {
vpc_id = aws_vpc.VPC_A.id
subnet_ids = [aws_subnet.VPC_A_Public_Subnet_A.id]

  ingress {
    protocol   = -1
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  egress {
    protocol   = -1
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  tags = {
    "name" = "VPC_A_Public_NACL_A"
  }
}

resource "aws_network_acl_association" "VPC_A_Public_NACL_A_Association" {
  network_acl_id = aws_network_acl.VPC_A_Public_NACL_A.id
  subnet_id      = aws_subnet.VPC_A_Public_Subnet_A.id
}
Enter fullscreen mode Exit fullscreen mode

Specify the first route table

resource "aws_route_table" "VPC_A_Public_RT_A" {
  vpc_id = aws_vpc.VPC_A.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.VPC_A_IGW.id
  }

  tags = {
    Name = "VPC_A_Public_RT_A"
  }
}

resource "aws_route_table_association" "VPC_A_Public_RT_A_Association_A" {
  subnet_id      = aws_subnet.VPC_A_Public_Subnet_A.id
  route_table_id = aws_route_table.VPC_A_Public_RT_A.id
}
Enter fullscreen mode Exit fullscreen mode

Create a security group in the VPC for the first subnet

resource "aws_security_group" "SG_bastion" {
  vpc_id      = aws_vpc.VPC_A.id
  description = " SG for bastion host. SSH access only"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Create an instance for the first subnet

resource "aws_instance" "Bastion" {
  ami               = "ami-0b029b1931b347543"
  instance_type     = "t2.micro"
  availability_zone = "us-west-2a"
  key_name          = ""
  tenancy           = "default"
  subnet_id         = aws_subnet.VPC_A_Public_Subnet_A.id
  security_groups   = ["${aws_security_group.SG_bastion.id}"]
}
Enter fullscreen mode Exit fullscreen mode

Create an elastic IP

resource "aws_eip" "EIP_for_NAT_GW" {
  vpc = true
}
Enter fullscreen mode Exit fullscreen mode

Create a NAT Gateway in the second subnet and attach the elastic IP to it

resource "aws_nat_gateway" "NAT_GW" {
  subnet_id         = aws_subnet.VPC_A_Public_Subnet_A.id
  connectivity_type = "public"
  allocation_id     = aws_eip.EIP_for_NAT_GW.id

  tags = {
    "Name" = "NAT_GW"
  }
}
Enter fullscreen mode Exit fullscreen mode

Create a second subnet

resource "aws_subnet" "VPC_A_Private_Subnet_A" {
  vpc_id                  = aws_vpc.VPC_A.id
  cidr_block              = "10.0.10.0/24"
  availability_zone       = "us-west-2a"
  map_public_ip_on_launch = "false"

  tags = {
    Name = "VPC_A_Private_Subnet_A"
  }
}
Enter fullscreen mode Exit fullscreen mode

Create a second NACL for the second subnet

resource "aws_network_acl" "VPC_A_Private_NACL_A" {
vpc_id = aws_vpc.VPC_A.id
subnet_ids = [aws_subnet.VPC_A_Private_Subnet_A.id]

  ingress {
    protocol   = -1
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  egress {
    protocol   = -1
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  tags = {
    "name" = "VPC_A_Private_NACL_A"
  }
}

resource "aws_network_acl_association" "VPC_A_Private_NACL_A_Association" {
  network_acl_id = aws_network_acl.VPC_A_Private_NACL_A.id
  subnet_id      = aws_subnet.VPC_A_Private_Subnet_A.id
}
Enter fullscreen mode Exit fullscreen mode

Create a second security group

Here we will explore the stateful nature of a security group

resource "aws_security_group" "SG_Private" {
  name        = "SG_Private"
  description = "Security group for private subnet instances."
  vpc_id      = aws_vpc.VPC_A.id

  ingress {
    description     = "Incoming & Outgoing SSH"
    from_port       = 22
    to_port         = 22
    protocol        = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    description = "Outgoing & Incoming ICMP"
    from_port   = -1
    to_port     = -1
    protocol    = "icmp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    "name" = "SG_Private"
  }
}
Enter fullscreen mode Exit fullscreen mode

We can notice that the ingress rule is for an SSH protocol, while the egress rule is for the ICMP protocol.

On AWS, when a packet ingresses or is directed towards an instance, it can leave the instance even if the egress rule doesn't match. That is why I described it as “incoming & outgoing SSH”
The Bastion instance can SSH into the private instance. Since the Bastion security group has an egress SSH rule set. Also, since the Private security grphp has an ingress SSH rule set.

Also, the private instance can ping any address on the Internet.
ICMP uses an "echo request" and an "echo reply.”
Since the first outgoing “request” traffic was approved, its incoming “reply” traffic will be approved too. That is why I described it as an “outgoing & incoming ICMP”

Create a second route table

resource "aws_route_table" "VPC_A_Private_RT_A" {
  vpc_id = aws_vpc.VPC_A.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_nat_gateway.NAT_GW.id
  }

  tags = {
    Name = "VPC_A_Private_RT_A"
  }
}

resource "aws_route_table_association" "VPC_A_Private_RT_A_Association" {
  subnet_id      = aws_subnet.VPC_A_Private_Subnet_A.id
  route_table_id = aws_route_table.VPC_A_Private_RT_A.id
}
Enter fullscreen mode Exit fullscreen mode

Create a second instance

resource "aws_instance" "VPC_A_Private_Subnet_A_Instance_A" {
  ami               = "ami-0b029b1931b347543"
  instance_type     = "t2.micro"
  availability_zone = "us-west-2a"
  key_name          = ""
  tenancy           = "default"
  subnet_id         = aws_subnet.VPC_A_Private_Subnet_A.id
  security_groups   = ["${aws_security_group.SG_Private.id}"]

  tags = {
    "name" = "VPC_A_Private_Subnet_A_Instance_A"
  }
}
Enter fullscreen mode Exit fullscreen mode

After deploying the configurations, configure SSH Agent forwarding on Putty, then SSH into your first instance, and then into the second instance. Lastly do a ping to any public URL from the second instance and make sure its successful.

Follow my AWS Advanced Networking Journey on Github

AWS Advanced Networking
https://github.com/Its-All-About-the-Journey/AWS-Advanced-Networking

Top comments (0)