Tommi's Scribbles

How to Create a Personal VPN on AWS with WireGuard

How to Create a Personal VPN on AWS with WireGuard
  • Published on 2022-01-15

There are multiple guides online for setting up a personal VPN on AWS with WireGuard. Why write another? My experience found out you can't find all the information you need in a single place, but instead need to go here and there to get a working setup. To remedy this, here are the steps to get a WireGuard VPN running on AWS. Modify it to your needs

Set up the cloud

First off, I am not going to tell you how to create an AWS account, IAM account there and so forth. If you can't sort it out on your own, then this guide is definitely not for you. I also won't hold your hand through the basic cloud things, like spinning up the instance.

The reason is simple: I don't know what kind of instance you want. If you want to go stay the free tier (mine expired), you will want a t2.micro. If you plan on having your whole family and friends use the VPN, you will probably want something more performant. Or maybe you want to have multiple locations and use an ALB and automatically spin up and down the instances. That stuff is for you to figure out.

Minimum requirements

That said, the minimum you need is an AWS account with access to VPC and EC2 resources. I highly recommend picking Debian as the OS. This is because in my testing, WireGuard was the easiest to install on an a-series EC2 (ARM) using Debian. The EC2 instance type I used was an a1.large spot instance. This might be overkill as WireGuard is not a massive resource hog. However, I wanted to make sure if I grant family or friends access too, that the performance won't tank.

Region choice

For the region, I picked a location that is central to the area I usually travel to have a good latency from everywhere. Nothing prevents from adding one instance per region and just creating multiple configs, kind of how all the VPN services have a country selection.

EBS

For the EBS, I picked a small 8GB drive since I don't expect to need more than that. You could possibly go even lower, but if you want to do logging or such it might be nice to have a bit of a buffer. I went with gp3 type, as IIRC it has the lowest cost. It's not like the VPN needs tons of IOPS.

VPCs

I created a new VPC, security group, subnets, and all for the VPN. But you could also use a pre-existing one as well. Do note, that if you use the default VPC subnet it has some different options by default compared to when you create a new VPC. But if you match the rules to the ones below, you should be all good even with the default one.

Internet Gateway and Elastic IP

The VPC needs an Internet Gateway and an Elastic IP as additional items. This is again the minimum for a working setup. Assign the elastic IP to the EC2.

Once you have the resources ready, configure the VPC resources in the following way:

Route Table Configuration:

Destination Target
x.x.x.x/24	local
0.0.0.0/0	  the resource id of your Internet Gateway
::/0	      the resource id of your Internet Gateway

Notes:

Security Group Configuration

Inbound rules:

IP version  Type    Protocol  Port range      Source
IPv4	      HTTP	  TCP	      80	            0.0.0.0/0
IPv4	      Custom  TCP	      TCP $$$$$       0.0.0.0/0
IPv6	      HTTPS	  TCP	      443	            ::/0
IPv6	      HTTP	  TCP	      80	            ::/0
IPv4	      SSH	    TCP	      22	            0.0.0.0/0
IPv4	      Custom  UDP	      UDP	$$$$$	      0.0.0.0/0
IPv4	      HTTPS	  TCP	      443	            0.0.0.0/0

Notes:

Outbound rules:

IP version  Type        Protocol    Port range    Destination
IPv4	      All traffic	All	        All	          0.0.0.0/0

Notes:

Network ACL Configuration

Inbound rules:

Rule number Type        Protocol  Port range    Source      Allow/Deny
100	        SSH (22)	  TCP (6)	  22	          0.0.0.0/0	  Allow
101	        HTTP (80)	  TCP (6)	  80	          0.0.0.0/0	  Allow
102 	      HTTPS (443)	TCP (6)	  443	          0.0.0.0/0	  Allow
103	        Custom TCP	TCP (6)	  1024 - 65535	0.0.0.0/0	  Allow
104	        Custom UDP	UDP (17)	$$$$$	        0.0.0.0/0	  Allow
All traffic	All	      All	          0.0.0.0/0	  Deny

Notes

Outbound rules:

Rule number Type        Protocol  Port range    Source      Allow/Deny
100	        All traffic	All	      All	          0.0.0.0/0	  Allow
101	        HTTP (80)	  TCP (6)	  80	          0.0.0.0/0	  Allow
102	        HTTPS (443)	TCP (6)	  443	          0.0.0.0/0	  Allow
All traffic	All	      All	          0.0.0.0/0	  Deny

Notes:

Configure WireGuard

With the infrastructure set up, you can log in to the EC2 instance and start setting up WireGuard. All the command examples below are for Debian 10.

Update the OS

Start by updating and upgrading the OS.

sudo apt-get update
sudo apt-get upgrade
sudo apt-get dist-upgrade

Once up-to-date, I like to install ufw to control the firewall and make sure basic security is in place. So:

Setup firewall

sudo apt-get install ufw

The following is the set of commands to configure ufw:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw allow $$$$$/udp
sudo ufw allow $$$$$/tcp
sudo ufw allow dns
sudo ufw allow from x.x.x.x/24

For x.x.x.x you want to come up with an IP address range you will use. I used part of the CIDR range assigned to the VPC in the infrastructure configuration step, so for example, with CIDR 10.0.0.0 something like 10.10.10.0 will work.

The rules we create:

Replace $$$$$ with the port you used above when configuring the VPC. Also, as mentioned in the VPC section, TCP for the port isn't possibly required. And as mentioned there, you could limit ssh to just your workstation/network IP.

Next, we will edit the ufw before rules. For that and also for WireGuard configuration, we need the network interface:

ip -o -4 route show to default | awk '{print $5}'

With the interface value at hand, time to edit the rules:

sudo nano /etc/ufw/before.rules

Add this after the first comment batch before the filter part.

*nat
:PREROUTING ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s x.x.x.x/24 -o ZZZ -j MASQUERADE
COMMIT

Replace ZZZ with the network interface we got earlier. Replace x.x.x.x with what you get from the following command:

sudo route -n | grep ^0.0.0.0

Finally, enable ufw:

sudo ufw enable

Since the ssh connections were allowed, you should stay signed in with the firewall in a good shape.

Install WireGuard

Installing the WireGuard is easy as it is available as a kernel extension in the default Debian repository:

sudo apt-get install wireguard

After the installation is complete, restart the instance.

sudo reboot

Your connection terminates, but you can reconnect after a brief moment. I like to move around the OS to the working directories instead of issuing them as commands, but feel free to replace commands with direct ones if you don't like that. I also love nano over other editors, but feel free to use your favorite for the edits.

Generate keys

cd /var
sudo mkdir keys
umask 077
sudo wg genkey > privatekey
sudo wg pubkey < privatekey > publickey
sudo mv privatekey /var/keys/wg-privatekey
sudo mv publickey /var/keys/wg-publickey

These commands create the WireGuard keys that are needed in the configuration. You can take note of the keys now, use echo and cat for some fancy config making, or just view the keys later if needed with

sudo cat /var/keys/wg-privatekey

for the private key and

sudo cat /var/keys/wg-publickey

for the public key.

Create the configuration

Now we can start with the configuration.

sudo nano /etc/wireguard/wg0.conf

This is the full configuration for the server.

[Interface]
Address = x.x.x.x/24
SaveConfig = false
ListenPort = $$$$$
PrivateKey = THE_SERVER_PRIVATE_KEY_FROM_ABOVE
PostUp = iptables -A FORWARD -i <>i -j ACCEPT; iptables -A FORWARD -o <>i -j ACCEPT; iptables -t nat -A POSTROUTING -o ZZZ -j MASQUERADE
PostUp = ip route add default via y.y.y.y dev ZZZ table ssh
PostUp = ip rule add fwmark 0x2 table ssh
PostUp = /sbin/iptables -A OUTPUT -t mangle -o wg0 -p tcp --sport 22 -j MARK --set-mark 2
PreDown = /sbin/iptables -D OUTPUT -t mangle -o wg0 -p tcp --sport 22 -j MARK --set-mark 2
PreDown = ip rule del fwmark 0x2 table ssh
PreDown = ip route del default via y.y.y.y dev ZZZ table ssh
PostDown = iptables -D FORWARD -i <>i -j ACCEPT; iptables -D FORWARD -o <>i -j ACCEPT; iptables -t nat -D POSTROUTING -o ZZZ -j MASQUERADE

Replace ZZZ with the network interface you identified above. Replace x.x.x.x with a local IP address. I used the same range as the CIDR you configured in the VPC infrastructure, so with CIDR 10.0.0.0 something like 10.10.10.1 works perfectly. Replace $$$$$ with the port you have used in the earlier steps. Replace y.y.y.y with the IP address you get from this command:

sudo route -n | grep ^0.0.0.0

The address is probably close to the x.x.x.x address and in the CIDR range. Finally, adjust the permissions of the file so normal users can't see your keys:

sudo chmod 600 /etc/wireguard/wg0.conf

Additional configuration

We need to do some additional configuration to allow SSH connections to work when WireGuard is up, and to allow the routing to work. Issue the following command and add 2 ssh at the end of the file.

sudo nano /etc/iproute2/rt_tables

Continue with editing the next file:

sudo nano /etc/sysctl.conf

Below are the lines we are interested in. Uncomment them from your configuration.

net.ipv4.conf.all.rp_filter=2
net.ipv4.ip_forward=1

Also uncomment this if you use IPv6:

net.ipv6.conf.all.forwarding=1

Now you are ready for another restart.

Add client configs

Once the device has restarted, it is time to start adding clients. For this, you need to generate keys for the client. Easiest way is to use the official WireGuard app. It is available at least in the macOS, iOS, and iPadOS App Stores for free. As another option, you can use the command you used to generate the server keys to generate the client keys. Just be sure to save them in a different file / place to not confuse them! Also note, you need to give the client private key to the client as well as the public server key.

Once you have the keys, you edit the main configuration again.

sudo nano /etc/wireguard/wg0.conf

With the file open, add the following to the end. You can add as many as you need, one per connecting client. Note that you can use the same peer for connecting from different devices, so think of it a bit more like "user". Though if you connect from multiple devices at the same time it probs won't allow it.

[Peer] #USER
PublicKey = USERPUBLICKEY
AllowedIPs = x.x.x.x/32

The #USER after the peer is not needed, but it is for bookkeeping purposes: by replacing USER with whomever you assing the config to, you can easily remember which config is which.

As before, you replace the USERPUBLICKEY with the public key generated either by the WireGuard app, or with the public key you generated on the client. For the x.x.x.x, you continue from the server's address. If you gave the server 10.10.10.1, you give the first Peer 10.10.10.2, next one 10.10.10.3 and so on.

Save the file and you are ready to launch!

Launching WireGuard

Launching WireGuard is a fairly easy and rather unexciting. You just apply this command:

sudo wg-quick up wg0

If you need to stop the server, you just replace up with down. If you want WireGuard to start automatically at restart, you do something like:

sudo systemctl enable wg-quick@wg0

Now you can start connecting with your clients.

Future improvements

You now have a simple personal VPN of your own running. If you would like to take the system to the next level, you could look into: