--- title: Setting up WireGuard VPN at AWS with Terraform slug: aws-wireguard-vpn-terraform date: 2024-10-08 taxonomies: tags: ["aws", "terraform", "vpn"] extra: medium: https://medium.com/p/f3054cba21fa devto: https://dev.to/vladkens/setting-up-wireguard-vpn-at-aws-with-terraform-497n --- 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: 1. Bastion / Jump Host – EC2 instance with SSH access for each member 2. 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`: ```sh 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. ```tf 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: ```sh 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): ```sh ❯ 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` ```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). ```sh #!/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 ``` ```sh [Peer] # ${peer_name} PublicKey = ${peer_pubkey} AllowedIPs = ${peer_addr} ``` Next we need to prepare user data script in terraform file from this templates. ```tf # 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. ```tf # 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: ```tf 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](https://www.wireguard.com/install/) 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`: ```sh [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: ```tf # 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. ```sh ❯ 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: ```sh ❯ 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: ```yaml 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](https://github.com/vladkens/blog/tree/main/code/aws-wireguard-terraform/)._