Setup managed client VPN in AWS using Terraform.

What is AWS Client VPN?

Mutual authentication — generating certs

Setting this all up in Terraform

resource aws_security_group vpn_access {
name = "shared-vpn-access"
vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id
ingress {
from_port = 0
protocol = "-1"
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
protocol = "-1"
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
}
locals {
private_subnet_1 = "subnet-xxxxxxxx" // production-eu-west-2a
private_subnet_2 = "subnet-xxxxxxxx" // production-eu-west-2b
}
resource aws_ec2_client_vpn_endpoint vpn {
client_cidr_block = "10.10.0.0/21"
split_tunnel = false
server_certificate_arn = "arn:aws:acm:eu-west-2:XXXXXXXXXXX:certificate/xxxxxxxx-xxxx-xxxx"

authentication_options {
type = "certificate-authentication"
root_certificate_chain_arn = "arn:aws:acm:eu-west-2:XXXXXXXX:certificate/xxxxxxxx-xxxx-xxxx"
}

connection_log_options {
enabled = false
}
}
resource aws_ec2_client_vpn_network_association private {
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.vpn.id
subnet_id = local.private_subnet_1
}
resource aws_route53_resolver_endpoint vpn_dns {
name = "vpn-dns-access"
direction = "INBOUND"
security_group_ids = [aws_security_group.vpn_dns.id]
ip_address {
subnet_id = local.private_subnet_1
}
ip_address {
subnet_id = local.private_subnet_2
}
}
resource aws_security_group vpn_dns {
name = "vpn_dns"
vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id
ingress {
from_port = 0
protocol = "-1"
to_port = 0
security_groups = [aws_security_group.vpn_access.id]
}
egress {
from_port = 0
protocol = "-1"
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
}
dns_servers = [
aws_route53_resolver_endpoint.vpn_dns.ip_address.*.ip[0],
aws_route53_resolver_endpoint.vpn_dns.ip_address.*.ip[1]
]
resource aws_ec2_client_vpn_endpoint vpn {
client_cidr_block = "10.10.0.0/21"
split_tunnel = false
server_certificate_arn = "arn:aws:acm:eu-west-2:XXXXXXXXXXX:certificate/xxxxxxxx-xxxx-xxxx"
dns_servers = [
aws_route53_resolver_endpoint.vpn_dns.ip_address.*.ip[0],
aws_route53_resolver_endpoint.vpn_dns.ip_address.*.ip[1]
]
authentication_options {
type = "certificate-authentication"
root_certificate_chain_arn = "arn:aws:acm:eu-west-2:XXXXXXXX:certificate/xxxxxxxx-xxxx-xxxx"
}

connection_log_options {
enabled = false
}
}
resource null_resource client_vpn_ingress {
depends_on = [aws_ec2_client_vpn_endpoint.vpn]
provisioner "local-exec" {
when = create
command = "aws ec2 authorize-client-vpn-ingress --client-vpn-endpoint-id ${aws_ec2_client_vpn_endpoint.vpn.id} --target-network-cidr 0.0.0.0/0 --authorize-all-groups"
}
lifecycle {
create_before_destroy = true
}
}
resource null_resource client_vpn_route_table {
depends_on = [aws_ec2_client_vpn_endpoint.vpn]
provisioner "local-exec" {
when = create
command = "aws ec2 create-client-vpn-route --client-vpn-endpoint-id ${aws_ec2_client_vpn_endpoint.vpn.id} --destination-cidr-block 0.0.0.0/0 --target-vpc-subnet-id ${local.private_subnet_1}"
}
lifecycle {
create_before_destroy = true
}
}
resource null_resource client_vpn_security_group {
depends_on = [aws_ec2_client_vpn_endpoint.vpn]
provisioner "local-exec" {
when = create
command = "aws ec2 apply-security-groups-to-client-vpn-target-network --client-vpn-endpoint-id ${aws_ec2_client_vpn_endpoint.vpn.id} --vpc-id ${aws_security_group.vpn_access.vpc_id} --security-group-ids ${aws_security_group.vpn_access.id}"
}
lifecycle {
create_before_destroy = true
}
}

Full terraform code can be seen below:

resource aws_ec2_client_vpn_network_association private {
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.vpn.id
subnet_id = local.private_subnet_1
}
resource aws_route53_resolver_endpoint vpn_dns {
name = "vpn-dns-access"
direction = "INBOUND"
security_group_ids = [aws_security_group.vpn_dns.id]
ip_address {
subnet_id = local.private_subnet_1
}
ip_address {
subnet_id = local.private_subnet_2
}
}
locals {
private_subnet_1 = "subnet-xxxxxxxxxxxxxxxxx"
private_subnet_2 = "subnet-xxxxxxxxxxxxxxxxx"
}
provider aws {
region = "eu-west-2"
}
resource null_resource client_vpn_ingress {
depends_on = [aws_ec2_client_vpn_endpoint.vpn]
provisioner "local-exec" {
when = create
command = "aws ec2 authorize-client-vpn-ingress --client-vpn-endpoint-id ${aws_ec2_client_vpn_endpoint.vpn.id} --target-network-cidr 0.0.0.0/0 --authorize-all-groups"
}
lifecycle {
create_before_destroy = true
}
}

resource null_resource client_vpn_route_table {
depends_on = [aws_ec2_client_vpn_endpoint.vpn]
provisioner "local-exec" {
when = create
command = "aws ec2 create-client-vpn-route --client-vpn-endpoint-id ${aws_ec2_client_vpn_endpoint.vpn.id} --destination-cidr-block 0.0.0.0/0 --target-vpc-subnet-id ${local.private_subnet_1}"
}
lifecycle {
create_before_destroy = true
}
}

resource null_resource client_vpn_security_group {
depends_on = [aws_ec2_client_vpn_endpoint.vpn]
provisioner "local-exec" {
when = create
command = "aws ec2 apply-security-groups-to-client-vpn-target-network --client-vpn-endpoint-id ${aws_ec2_client_vpn_endpoint.vpn.id} --vpc-id ${aws_security_group.vpn_access.vpc_id} --security-group-ids ${aws_security_group.vpn_access.id}"
}
lifecycle {
create_before_destroy = true
}
}
resource aws_security_group vpn_access {
name = "shared-vpn-access"
vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id
ingress {
from_port = 0
protocol = "-1"
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
protocol = "-1"
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
}

resource aws_security_group vpn_dns {
name = "vpn_dns"
vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id
ingress {
from_port = 0
protocol = "-1"
to_port = 0
security_groups = [aws_security_group.vpn_access.id]
}
egress {
from_port = 0
protocol = "-1"
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
}
resource aws_ec2_client_vpn_endpoint vpn {
client_cidr_block = "10.10.0.0/21"
split_tunnel = false
server_certificate_arn = "arn:aws:acm:eu-west-2:xxxxxxxx:certificate/xxxxx"
dns_servers = [
aws_route53_resolver_endpoint.vpn_dns.ip_address.*.ip[0],
aws_route53_resolver_endpoint.vpn_dns.ip_address.*.ip[1]
]

authentication_options {
type = "certificate-authentication"
root_certificate_chain_arn = "arn:aws:acm:eu-west-2:xxxxxxxxx:certificate/xxxxx"
}

connection_log_options {
enabled = false
}
}

Technologist who enjoys writing and working with software and infra. I write up all the things I learn as I go along to share the knowledge! beardy.digital

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store