Setting up WireGuard VPN at AWS with Terraform
All resources in AWS work inside private VPC. Sometimes you may need to access these resources from local computer (e.g. to interact with database). Some resources, like RDS, have the option to enable public access to them – but this is unsecure. Of course you can configure Security Group to allow access to public resource only from allowed IPs to make this setup a bit better, but still in this case all your colleagues must to have static IPs, which is not always true.
Objective
We need to provide a quick and easy solution to give access to internal AWS VPC resources for our team. In general, there are two ways to do this:
- Bastion / Jump Host – EC2 instance with SSH access for each member
- Private VPN – EC2 instance with VPN config for each member
In both cases we need to to setup a custom EC2 instance, so second variant seems more convenient to me.
Preparation
In addition to the traditional Terraform client, we will need WireGuard CLI tools to generate a key pair. You can install WireGuard tools on macOS via brew
:
brew install wireguard-tools
Initial Terraform
First, as usual, we need to configure Terraform Provider and get VPC and Public Subnet data where we want to host our VPN server. On this step you can export VPC / Public Subnet from another layer or pass them via variables. I will access my VPC and Public Subnet by name.
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.70" }
}
}
provider "aws" {
profile = "default" # Change this to your AWS profile
region = "us-east-1" # Change this to your AWS region
}
# MARK: Getting VPC and Subnets
locals {
vpc_name = "my_vpc" # Change this to yor VPC name
}
data "aws_vpc" "vpc" {
filter {
name = "tag:Name"
values = [local.vpc_name]
}
}
data "aws_subnets" "public" {
filter {
name = "vpc-id"
values = [data.aws_vpc.vpc.id]
}
tags = { Tier = "Public" } # This is custom tag
}
WireGuard Server
To setup WireGuard server we need to know AMI ID of OS will be running on EC2 instance, prepare key pairs for the server and for client (peers), create a security group to control network access, create and assing Elastic IP to be sure that the server IP will be same.
Generate key pairs
First we need to create key pairs which can be generated with this command:
bash -c 'priv=$(wg genkey); pub=$(echo $priv | wg pubkey); printf "$priv\n$pub\n"'
In output first line is privkey, second is pubkey.
I will generate it twice, one for server, second for first client (peer). Then for each new peer you will need to generate new key pairs. I will same generated keys to secrets.yaml
file, but you can use different ways to store secrets.
Generate secrets for server and first peer (do not use this keys in your setup):
❯ bash -c 'priv=$(wg genkey); pub=$(echo $priv | wg pubkey); printf "$priv\n$pub\n"'
0McsAe5Y/oupwJeju+94ZFl1bmp2KIX2bBQGk0cSWXQ=
KeV+BlZMfCgRhdfaHqpdPp23Nu/otir+6VR02DER7lU=
❯ bash -c 'priv=$(wg genkey); pub=$(echo $priv | wg pubkey); printf "$priv\n$pub\n"'
SK71fHXpTo2cGKvlLLTlu+0AqpwaNWG30rzGXFIryk0=
wf3j1JLhXw/u6iDZf9Qhq0lU34exF8mrHdGzz/u+xFU=
Then save generated keys to secrets.yaml
wg_server:
privkey: 0McsAe5Y/oupwJeju+94ZFl1bmp2KIX2bBQGk0cSWXQ=
pubkey: KeV+BlZMfCgRhdfaHqpdPp23Nu/otir+6VR02DER7lU=
wg_peers:
- name: user1
addr: 172.16.16.1
privkey: SK71fHXpTo2cGKvlLLTlu+0AqpwaNWG30rzGXFIryk0=
pubkey: wf3j1JLhXw/u6iDZf9Qhq0lU34exF8mrHdGzz/u+xFU=
Setup script
Then we need create WireGuard setup script which will be runned during EC2 creation. User data will be generated from this script and populated from secrets.yaml
file. So create templates
directory with two files in it: wg-init.tpl
(general setup script) and wg-peer.tpl
(peer info).
#!/bin/bash
sudo apt update && apt -y install net-tools wireguard
# https://github.com/vainkop/terraform-aws-wireguard/blob/master/templates/user-data.txt
sudo mkdir -p /etc/wireguard
sudo cat > /etc/wireguard/wg0.conf <<- EOF
[Interface]
PrivateKey = ${wg_privkey}
ListenPort = ${wg_port}
Address = ${wg_cidr}
PostUp = sysctl -w -q net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o ENI -j MASQUERADE
PostDown = sysctl -w -q net.ipv4.ip_forward=0
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o ENI -j MASQUERADE
${wg_peers}
EOF
# Make sure we replace the "ENI" placeholder with the actual network interface name
export ENI=$(ip route get 8.8.8.8 | grep 8.8.8.8 | awk '{print $5}')
sudo sed -i "s/ENI/$ENI/g" /etc/wireguard/wg0.conf
sudo wg-quick up wg0
sudo systemctl enable wg-quick@wg0
[Peer]
# ${peer_name}
PublicKey = ${peer_pubkey}
AllowedIPs = ${peer_addr}
Next we need to prepare user data script in terraform file from this templates.
# MARK: Config
locals {
wg_cidr = "172.16.16.0/20" # CIDR of WireGuard Server
wg_port = 51820 # Port of WireGuard Server
secrets = yamldecode(file("secrets.yaml"))
userdata = templatefile("templates/wg-init.tpl", {
wg_cidr = local.wg_cidr
wg_port = local.wg_port
wg_privkey = local.secrets.wg_server.privkey
wg_peers = join("\n", [
for p in local.secrets.wg_peers :
templatefile("templates/wg-peer.tpl", {
peer_name = p.name
peer_pubkey = p.pubkey
peer_addr = p.addr
})
])
})
}
Server setup
Then we need to select Ubuntu AMI, create Security Group and Elastic IP.
# MARK: Server dependencies
data "aws_ami" "ubuntu" {
owners = ["099720109477"] # Canonical
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-arm64-server-*"]
}
}
resource "aws_eip" "wireguard" {
tags = { Name = "wireguard" }
}
resource "aws_security_group" "wireguard" {
name = "${local.vpc_name}-wireguard"
description = "SG for WireGuard VPN Server"
vpc_id = data.aws_vpc.vpc.id
ingress {
from_port = local.wg_port
to_port = local.wg_port
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow WireGuard Traffic"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
Next we can setup a server and assing Elastic IP to it:
resource "aws_instance" "wireguard" {
count = 1
ami = data.aws_ami.ubuntu.id
instance_type = "t4g.nano"
subnet_id = data.aws_subnets.public.ids[0]
vpc_security_group_ids = [aws_security_group.wireguard.id]
user_data = local.userdata
user_data_replace_on_change = true
tags = { Name = "wireguard" }
}
resource "aws_eip_association" "wireguard" {
instance_id = aws_instance.wireguard[0].id
allocation_id = aws_eip.wireguard.id
}
Client configuration
To access to WireGuard server we need to have WireGuard client app and valid client config with .conf
extension.
We can generated this configs with Terraform Templates as well. So first, lets add new template file called client-conf.tpl
:
[Interface]
Address = ${client_addr}/32
PrivateKey = ${client_privkey}
DNS = ${client_dns}
[Peer]
Endpoint = ${server_addr}
PublicKey = ${server_pubkey}
AllowedIPs = ${client_routes}
PersistentKeepalive = 25
And add this code to generate client config to out terraform file:
# MARK: Export client configuration
locals {
# https://docs.aws.amazon.com/vpc/latest/userguide/AmazonDNS-concepts.html#AmazonDNS
client_dns = ["169.254.169.253", "1.1.1.1", "1.0.0.1"]
client_routes = [
data.aws_vpc.vpc.cidr_block, # All IPs in the VPC
"169.254.169.253/32", # AWS DNS
# "0.0.0.0/32", # Route all traffic through VPN (uncomment if you want this)
]
}
resource "local_file" "peer_conf" {
for_each = { for index, p in local.secrets.wg_peers : p.name => p }
filename = "generated/${each.value.name}.conf"
content = templatefile("templates/client-conf.tpl", {
server_addr = "${aws_eip.wireguard.public_ip}:${local.wg_port}",
server_pubkey = local.secrets.wg_server.pubkey,
client_addr = each.value.addr,
client_privkey = each.value.privkey,
client_dns = join(",", local.client_dns),
client_routes = join(",", local.client_routes),
})
}
Execute
Finally we can run terraform init
& terraform apply
. In less then a minute VPN server will be created and user client will be exported to generated/user1.conf
. Then we can share this file with our teammates.
To check VPN connection working fine you can export .conf
into WireGuard client. Then for example I check my RDS instance before VPN enabled and after.
❯ dig +short my-rds.4oexsiqydp8q.us-east-1.rds.amazonaws.com
ec2-34-226-xx-xx.compute-1.amazonaws.com.
34.226.xx.xx
❯ dig +short my-rds.4oexsiqydp8q.us-east-1.rds.amazonaws.com
ec2-34-226-xx-xx.compute-1.amazonaws.com.
10.10.20.80
Adding clients
Adding new client configuration is quite simple. We need to generate new key pair:
❯ bash -c 'priv=$(wg genkey); pub=$(echo $priv | wg pubkey); printf "$priv\n$pub\n"'
ALqgaUiWd0rtcLza2K143x5RwS0Y5zwh3YwHx/L7nV0=
u1yQmh/G7n+S8aCp0PRuRuAuacmqYNzHLCPOcl9aS28=
Then update secrets.yaml
file:
wg_peers:
# ...
- name: user2
addr: 172.16.16.2
privkey: ALqgaUiWd0rtcLza2K143x5RwS0Y5zwh3YwHx/L7nV0=
pubkey: u1yQmh/G7n+S8aCp0PRuRuAuacmqYNzHLCPOcl9aS28=
And run terraform apply
again. Server will be re-created (!) with new configuration. Note that existing clients will be disconnected during deploy.
Summary
WireGuard provides easy solution for creating VPN server. We created cheap and fast setup to access to internal AWS VPC with ability to create different clients configurations, which can be shared with out team members.
PS. Source files of this article can be found here.