How to set up a Site-to-Site VPN connection
What is a VPN?
A Virtual Private Network (VPN) allows to route network traffic over a public network (typically the Internet) in a private and secure way. In fact, a VPN uses a private tunnel connection enabling traffic flow between your local network and another network.
There are three main use cases for using a VPN:
- Corporate environment: a VPN is used to access a corporate network from a branch office, at a lower cost than a dedicated line.
- Privacy and anonymity: a VPN lets you mask your current IP address and location, using encryption to keep the connection confidential.
- Connecting data centres: a VPN is used to connect two different data centres or cloud regions.
In this tutorial we are going to focus on the first use case. In particular, I’m going to explain how to set up a VPN connection between a local network and resources deployed in a VPC within the AWS network. In AWS jargon this is referred to as an AWS Site-to-Site VPN. The diagram below shows what we are going to build.
A typical scenario for this setup is a CI/CD build runner executing in a local network with a requirement to encrypt traffic between the office network and the AWS cloud.
The components involved in a Site-to-Site VPN connection to an AWS VPC are:
- Create a Customer Gateway
- Create a Virtual Private Gateway
- Create a VPN tunnel
- Test the network configuration
Note that this tutorial uses a computer running Linux as the customer gateway.
Create a Customer Gateway
Access to the internet in an office network is typically provided through a business contract with an Internet Service Provider (ISP). The ISP allocates one or more public IP addresses to your office network and you need to grab the public IPv4 address to follow this tutorial. You can find out your current public IPv4 address via online services like whatismyip or via the command line with curl checkip.amazonaws.com
.
Note that you can practice this tutorial even if you are connecting from home. However, bear in mind that in general ISPs dynamically allocate IP addresses to consumer contracts. This means that your home router gets an IP address which can change over time (restart your router and you will usually be allocated a different IP address). Some ISPs offer a static IP address allowing to have a dedicated and stable IP address for your home network, although this usually comes with an extra fee. For the sake of this tutorial, I just assume you have a public IP address but be aware of this caveat in case you practice this at home and your VPN connection doesn’t work the following day.
Armed with your public IP address head to the AWS console and navigate to VPC → Virtual Private Network (VPN) → Customer Gateways and click on Create Customer Gateway.
Set a name for your customer gateway, choose Static under Routing, paste your IP under IP address (here I’m using 79.22.223.65
), and click on Create Customer Gateway.
This screenshot shows an example of my settings:
Create a Virtual Private Gateway
The next step is to create a virtual private gateway. Navigate to VPC → Virtual Private Network (VPN) → Virtual Private Gateways, click on Create Virtual Private Gateway, give it a name tag and under ASN select Amazon default ASN. Finally, click on Create Virtual Private Gateway.
Initially the virtual private gateway has a detached status and we need to attach it to a VPC. Select the newly created virtual private gateway, click the Actions button and select Attach to VPC. Finally, click on Yes, Attach and wait until the status changes to attached.
Create a VPN
We now need to create a VPN tunnel linking the customer gateway with the virtual private gateway. Navigate to VPC → Virtual Private Network (VPN) → Site-to-Site VPN Connections and click on Create a VPN connection. Give it a name tag, choose Virtual Private Gateway under Target Gateway Type, and under Virtual Private Gateway select from the drop-down menu the virtual private gateway we created in the previous step.
In the customer gateway settings under Customer Gateway select Existing and under Customer Gateway ID select from the drop-down menu the customer gateway we created in the first step. On the Routing Options select Static and under Static IP Prefixes select the IP address of your local network (in my case 79.22.223.65
). Note that this is the same IP address we used in the first step. We keep the default tunnel options, so that two separate VPN tunnels are automatically created for redundancy.
This screenshot shows an example of my settings:
Finally, click on Create VPN Connection and wait several minutes until the VPN connection is created and displays the state as available.
If you click on the tab Tunnel Details, you notice that the VPN connection is using two tunnels but their status is currently down. We are going to bring them up by connecting our local network with AWS. To do that click on the button Download Configuration and select Generic under Vendor which automatically populates the other options. Finally, click on Download.
Keep this file at hand as we will need it later. Also, take care to keep its contents confidential.
Test the Network Configuration
Install libreswan
We are now going to set up the local network side of the VPN tunnel using libreswan. This is an open source tool that implements IPSec, a secure network protocol used in VPNs. You can install libreswan on Linux systems with:
# on rpm based systems (RedHat, CentOS, AmazonLinux, etc.)
sudo yum install libreswan
# on Debian based systems (Ubuntu, Debian, etc.)
sudo apt install libreswan
If you are on MacOS or Windows have a look at StrongSwan although the configuration steps below may be different.
A default installation of libreswan comes with two configuration files under /etc/ipsec.conf
and /etc/ipsec.secrets
. If you inspect these files you can notice they import any custom configuration and secret included with the directory /etc/ipsec.d
. We’re now going to add a custom configuration in this directory but first we need to grab some data to create these files.
IPSec Configuration
The first piece of data is the CIDR subnet of the local network and we call this the leftsubnet
. First, find the local router’s internal IP address as follows:
$ ip r | grep default
default via 192.168.1.1 dev enx00133ba9ee0d proto dhcp metric 100
So if your router’s internal IP address is 192.168.1.1
, you can set the leftsubnet
value to 192.168.0.0/16
in order to pick up a large number of IP addresses within your local network. Take note of this as we will use it in the next steps.
The second piece of data is the IP address of the VPN tunnel; we are going to call this value right
. Go back to the AWS console, navigate to VPC → Virtual Private Network (VPN) → Site-to-Site VPN Connections, and select the VPN connection created in the previous steps.
Click on the tab Tunnel Details as shown below:
I’m going to pick up the first tunnel which has an outside IP address of 3.228.164.221
and this is the value of right
.
The third piece of data is the CIDR subnet of the VPC where we created the virtual private gateway and we call this the rightsubnet
. Go back to the AWS console, navigate to VPC → Virtual Private Network (VPN) → Virtual Private Gateways and select the virtual private gateway created in the previous steps. Under the field VPC you can see in which VPC the virtual private gateway has been created. Click on the VPC ID and you can see the VPC settings, including the IPv4 CIDR:
This is the CIDR subnet of the VPC (in my case 172.31.0.0/16
and this is the value for rightsubnet
.
So to recap we have:
leftsubnet=192.168.0.0/16
right=3.228.164.221
rightsubnet=172.31.0.0/16
Armed with these values, we can create a new custom configuration file /etc/ipsec.d/myconnection.conf
. You can use your preferred editor to create this file but make sure to edit it as root
or with a tool like sudoedit
as this directory is owned by root
and you won’t be able to add a file in there without superuser permissions. This is how my /etc/ipsec.d/myconnection.conf
looks like:
conn myconnection
left=%defaultroute
leftsubnet=192.168.0.0/16
right=3.228.164.221
rightsubnet=172.31.0.0/16
authby=secret
auto=start
ikev2=no
Save the file and let’s move to the creation of the secret.
IPSec Secrets Configuration
For creating the secret for our connection we need one piece of data from the VPN configuration. You remember we downloaded this configuration when we created the VPN connection in AWS. The beginning of that downloaded file should look similar to this (I omitted the comments):
Amazon Web Services
Virtual Private CloudVPN Connection Configuration
[...]
IPSec Tunnel #1
[..]
- IKE version : IKEv1
- Authentication Method : Pre-Shared Key
- Pre-Shared Key : sJGhGU3KxyzbpZXb7JTn5.jUU4t6iyEl
- Authentication Algorithm : sha1
- Encryption Algorithm : aes-128-cbc
- Lifetime : 28800 seconds
- Phase 1 Negotiation Mode : main
- Diffie-Hellman : Group 2
Open your VPN configuration file and look under IPSec Tunnel #1
for the value of Pre-Shared Key
. In my case it is sJGhGU3KxyzbpZXb7JTn5.jUU4t6iyEl
. Note also the IKE version (IKEv1
) and this is the reason why we had to set ikev2=no
in myconnection.conf
. Recent versions of libreswan use IKEv2 by default so make sure you check this value.
Armed with your pre-shared key and with the right
value containing the IP address of your first VPN tunnel (look back to myconnection.conf
if you forgot it) we can now create /etc/ipsec.d/myconnection.secrets
. As usual, create it with sudo
and add the following line where 3.228.164.221
is the value of right
and sJGhGU3KxyzbpZXb7JTn5.jUU4t6iyEl
is the value of the pre-shared key.
3.228.164.221 0.0.0.0 %any: PSK "sJGhGU3KxyzbpZXb7JTn5.jUU4t6iyEl"
For my case, I didn’t need to make any changes to packet filtering rules. However, if you have a firewall or other configuration, you’ll need to make sure it allows both the IKE datagrams and the IPSec-protected data. The way you set this up depends a lot on the equipment and / or software you’re using, so I won’t cover it further here.
Establishing the VPN Connection
We can now test our connection. First, make sure to verify the syntax of your connection files and other networking settings in libreswan by running:
$ sudo ipsec verify
Version check and ipsec on-path [OK]
Libreswan 3.29 (netkey) on 5.4.0-48-generic
Checking for IPsec support in kernel [OK]
NETKEY: Testing XFRM related proc values
ICMP default/send_redirects [OK]
ICMP default/accept_redirects [OK]
XFRM larval drop [OK]
Pluto ipsec.conf syntax [OK]
Checking rp_filter [OK]
Checking that pluto is running [OK]
Pluto listening for IKE on udp 500 [OK]
Pluto listening for IKE/NAT-T on udp 4500 [OK]
Pluto ipsec.secret syntax [OK]
Checking 'ip' command [OK]
Checking 'iptables' command [OK]
Checking 'prelink' command does not interfere with FIPS [OK]
Checking for obsolete ipsec.conf options [OK]
If you get an error related to pluto not running, make sure you start libreswan with:
sudo systemctl start ipsec
If you get errors related to send_redirects
and accept_redirects
being disabled you can easily fix them by enabling the redirects with:
echo 0 | sudo tee /proc/sys/net/ipv4/conf/default/send_redirects
echo 0 | sudo tee /proc/sys/net/ipv4/conf/default/accept_redirects
Once the verify command returns all good, you can add your custom connection and restart libreswan:
sudo ipsec auto --add myconnection
sudo systemctl restart ipsec
You can check the status of your connection with:
sudo ipsec status
This should return an output ending with something similar to this (I omitted a few lines):
000 Connection list:
000
000 "myconnection": 192.168.0.0/16===192.168.1.9---192.168.1.1...3.228.164.221<3.228.164.221>===172.31.0.0/16; erouted; eroute owner: #2
000 "myconnection": oriented; my_ip=unset; their_ip=unset; my_updown=ipsec _updown;
[...]
000 "myconnection": IKEv1 algorithm newest: AES_CBC_128-HMAC_SHA1-MODP2048
000 "myconnection": ESP algorithm newest: AES_CBC_128-HMAC_SHA1_96; pfsgroup=<Phase1>
000
000 Total IPsec connections: loaded 1, active 1
000
000 State Information: DDoS cookies not required, Accepting new IKE connections
000 IKE SAs: total(1), half-open(0), open(0), authenticated(1), anonymous(0)
000 IPsec SAs: total(1), authenticated(1), anonymous(0)
000
000 #1: "myconnection":4500 STATE_MAIN_I4 (ISAKMP SA established); EVENT_SA_REPLACE in 454s; newest ISAKMP; lastdpd=3s(seq in:0 out:0); idle;
000 #2: "myconnection":4500 STATE_QUICK_I2 (sent QI2, IPsec SA established); EVENT_SA_REPLACE in 25895s; newest IPSEC; eroute owner; isakmp#1; idle;
000 #2: "myconnection" esp.cd325459@3.228.164.221 esp.a755d864@192.168.1.9 tun.0@3.228.164.221 tun.0@192.168.1.9 ref=0 refhim=0 Traffic: ESPin=0B ESPout=0B! ESPmax=4194303B
Next to Total IPsec connections you should get loaded 1, active 1 which shows that the connection has been established. If you don’t get an active connection, the last lines of the output tell you what has gone wrong so you can debug it, modify your configuration, and restart libreswan.
You can verify that the VPN tunnel is up by heading to the AWS console under VPC → Virtual Private Network (VPN) → Site-to-Site VPN Connections, selecting your VPN connection and checking the tab Tunnel Details:
Note that this is not immediate and you may need to wait up to a minute to see the status of the tunnel switching to up after a successful connection.
Test the Connection
We are now going to test the connection by pinging from our local network the private IP address of an EC2 instance deployed in the AWS network. But first we need to add some configuration in the VPN routing table so that the traffic can flow between our local network and the AWS network.
Head to VPC → Virtual Private Network (VPN) → Site-to-Site VPN Connections, select your VPN connection and click on the tab Static Routes. Click on the Edit button, add a rule by adding the CIDR 192.168.0.0/16
under IP Prefix, and click on Save. This allows your local network CIDR subnet.
Afterwards go to VPC → Your VPCs and select the VPC where your private gateway is located. Scroll on the right to find the main route table and click on its id. This lands you in the settings for Route Table. Go to tab Routes and click on Edit routes. Add a new route with Add route, put your local network CIDR subnet under Destination (192.168.0.0/16
), select Virtual Private Gateway under Target and choose the virtual private gateway set up in the previous steps.
Click on Save Routes and you should end up with something similar to this:
We can now start an EC2 instance in the VPC where we deployed the VPN connection and ping it from our local network to see if it’s reachable. When you start the EC2 instance make sure to add an inbound rule to the security group allowing the protocol ICMP from your local network CIDR subnet (in my case 192.168.0.0/16
). This allows to use the ping
command to reach the EC2 instance.
Now grab the private IP address of the EC2 instance running in the VPC (here I use 172.31.20.47
) and ping it from your local network with:
$ ping 172.31.20.47
PING 172.31.20.47 (172.31.20.47) 56(84) bytes of data.
64 bytes from 172.31.20.47: icmp_seq=1 ttl=254 time=95.8 ms
64 bytes from 172.31.20.47: icmp_seq=2 ttl=254 time=96.1 ms
64 bytes from 172.31.20.47: icmp_seq=3 ttl=254 time=95.5 ms
And voilà! You can now reach the AWS network from your local network via a VPN.
Wrap up
I hope you managed to get to the end of this tutorial and set up your VPN connection correctly. For production deployments, we’d always recommend that you configure the second VPN tunnel for redundancy purposes and that you use infrastructure as code for setting up VPN connections. If you want to dig deeper and discover the different networking options you can use in a VPN, read the AWS documentation on Site-to-Site VPN.
If you’re interested in another approach to connect your network with the AWS network check out the AWS Transit Gateway or look at solutions like OpenVPN and WireGuard.
I’m a consultant at The Scale Factory where we empower technology teams to deliver more on the AWS cloud, through consultancy, engineering, support, and training. If you’d like to find out how we can support you and your team to set up VPNs and other networking options, get in touch.