*Configuring an AWS Site-to-Site VPN
Summary
In this lab I set up an AWS Site-to-Site VPN between an AWS VPC and an on-premises data center. I didn't have a physical data center, so I simulated one with a second VPC in a different region. On the AWS side, I created a VPC with a private subnet and an instance, EC2-A, plus a Virtual Private Gateway. On the data center side, I created a VPC with a public subnet holding a VPN-Server instance and a private subnet holding an application server, EC2-B. I created a Customer Gateway pointing to VPN-Server's public IP and a Site-to-Site VPN connection using static routing. Then I installed LibreSwan on VPN-Server, configured it from the AWS VPN configuration file, and started the IPsec tunnel. After updating the route tables on both sides and turning off VPN-Server's source/destination check so it could act as a router, EC2-B could ping EC2-A across the VPN using only private IP addresses.
Architecture
| AWS side | Simulated data center | |
|---|---|---|
| Region | Canada (Central) ca-central-1 | US East (N. Virginia) us-east-1 |
| VPC | AWS-VPC, 10.0.0.0/16 | DC-VPC, 192.168.0.0/16 |
| Subnets | Private subnet 10.0.0.0/24, ca-central-1a | Public subnet 192.168.0.0/24, private subnet 192.168.100.0/24 |
| Instances | EC2-A (private, no public IP) | VPN-Server (public), EC2-B (private, no public IP) |
| Internet access | None | Internet Gateway for the public subnet |
| VPN endpoint | Virtual Private Gateway | VPN-Server running LibreSwan |
| VPN resource | Details |
|---|---|
| Virtual Private Gateway (VGW) | Attached to AWS-VPC |
| Customer Gateway (CGW) | IP address = VPN-Server's public IP |
| Site-to-Site VPN connection | VGW ↔ CGW, static routing, static prefix 192.168.0.0/16 |
Traffic path from EC2-B to EC2-A:
EC2-B (192.168.100.x)
→ DC private route table: 10.0.0.0/16 → VPN-Server
→ VPN-Server (LibreSwan encrypts the packet)
→ IPsec tunnel over the internet
→ Virtual Private Gateway
→ EC2-A (10.0.0.x)
Part 1: Building the AWS Network
1. Created AWS-VPC
I switched the console to Canada (Central), opened the VPC console, and created a VPC named AWS-VPC with the CIDR block 10.0.0.0/16. This VPC represented the cloud side of the connection.
2. Created the private subnet
I created a subnet in AWS-VPC with the CIDR block 10.0.0.0/24 in ca-central-1a. It represented internal AWS resources, so it didn't need an Internet Gateway or any public access.
3. Created a route table for the subnet
I created a route table in AWS-VPC, then opened its Subnet associations tab, clicked Edit subnet associations, selected the private subnet, and saved. I used a dedicated route table so I could add a route to the VPN later without touching the VPC's main route table.
4. Launched EC2-A
I launched an instance named EC2-A with these settings:
| Setting | Value |
|---|---|
| AMI | Amazon Linux |
| VPC / Subnet | AWS-VPC / the private subnet |
| Auto-assign public IP | Disable |
| Security group inbound | All ICMP - IPv4 from 192.168.0.0/16 |
The ICMP rule only allowed ping from the data center's address range, so it was the only traffic EC2-A would accept. I noted its private IPv4 address for testing later.
Part 2: Building the Simulated Data Center
1. Created DC-VPC
I switched the console to US East (N. Virginia) and created a VPC named DC-VPC with the CIDR block 192.168.0.0/16. This VPC played the role of the company's on-premises network. Its range didn't overlap with AWS-VPC's 10.0.0.0/16, which was required so each side could route to the other.
2. Created and attached an Internet Gateway
I created an Internet Gateway and attached it to DC-VPC. The VPN server needed internet access, because the IPsec tunnel to AWS travels over the public internet.
3. Created two subnets
| Subnet | CIDR | Purpose |
|---|---|---|
| Public subnet | 192.168.0.0/24 | Holds VPN-Server, the data center's VPN router |
| Private subnet | 192.168.100.0/24 | Holds EC2-B, an internal application server |
4. Created route tables
| Route table | Associated with | Routes |
|---|---|---|
| Public route table | Public subnet | 192.168.0.0/16 → local, 0.0.0.0/0 → Internet Gateway |
| Private route table | Private subnet | 192.168.0.0/16 → local |
5. Launched VPN-Server
| Setting | Value |
|---|---|
| AMI | Amazon Linux |
| VPC / Subnet | DC-VPC / public subnet |
| Auto-assign public IP | Enable |
| Security group inbound | SSH and All ICMP - IPv4 from 0.0.0.0/0 |
I noted its public IP, since AWS would need it to know where the data center's end of the VPN was.
6. Launched EC2-B
| Setting | Value |
|---|---|
| AMI | Amazon Linux |
| VPC / Subnet | DC-VPC / private subnet |
| Auto-assign public IP | Disable |
| Security group inbound | SSH from 192.168.0.0/16, All ICMP - IPv4 from 10.0.0.0/16 |
The SSH rule let me reach EC2-B from VPN-Server, and the ICMP rule allowed ping from AWS-VPC.
Part 3: Configuring the AWS VPN Components
1. Created the Virtual Private Gateway
Back in Canada (Central), I went to Virtual private gateways in the VPC console, created one, and then clicked Actions → Attach to VPC and selected AWS-VPC.
A Virtual Private Gateway (VGW) is the AWS end of a VPN connection. It's attached to a VPC much like an Internet Gateway, but it only carries VPN traffic.
2. Created the Customer Gateway
Under Customer gateways, I created one and set its IP address to VPN-Server's public IP.
A Customer Gateway (CGW) is AWS's record of the device on the customer's side of the VPN. In a real company, it would be a physical router or firewall. Here, it was VPN-Server.
3. Created the Site-to-Site VPN connection
Under Site-to-Site VPN connections, I clicked Create VPN connection and set:
| Setting | Value |
|---|---|
| Target gateway type | Virtual private gateway (the one attached to AWS-VPC) |
| Customer gateway | Existing (the one pointing to VPN-Server) |
| Routing options | Static |
| Static IP prefixes | 192.168.0.0/16 |
Static routing meant I told AWS directly which address range was on the data center side. The alternative, dynamic routing, uses the BGP protocol to exchange routes automatically, and needs a router that supports BGP.
AWS created the connection with two tunnels, each with its own AWS public IP, so the VPN can stay up if one tunnel fails.
4. Downloaded the configuration file
Once the VPN connection's state was Available, I clicked Download configuration and chose the Openswan format. LibreSwan is based on Openswan, so it uses the same configuration style. The file contained the tunnel IP addresses, the pre-shared keys, and step-by-step settings for the VPN server.
Part 4: Configuring LibreSwan on VPN-Server
1. Installed LibreSwan
I connected to VPN-Server over SSH and installed LibreSwan, the software that creates the IPsec tunnel:
sudo dnf update -y
sudo dnf install libreswan -y2. Turned on packet forwarding
I opened the system settings file:
sudo nano /etc/sysctl.confand added these lines at the end:
net.ipv4.ip_forward = 1
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.default.accept_source_route = 0
ip_forward = 1let the server pass packets between networks, so it could act as a router instead of only handling its own traffic.rp_filter = 0turned off a check that would drop packets arriving on an unexpected interface, which can happen with tunneled traffic.accept_source_route = 0kept a safety setting that rejects packets trying to choose their own route.
I applied the changes without restarting:
sudo sysctl -pThe command printed the three settings back, confirming they were active.
3. Enabled LibreSwan's extra configuration folder
I opened /etc/ipsec.conf and made sure this line near the bottom wasn't commented out:
include /etc/ipsec.d/*.conf
This told LibreSwan to load any connection files placed in /etc/ipsec.d/, which is where I put the AWS settings.
4. Created the tunnel configuration
I created the connection file:
sudo nano /etc/ipsec.d/aws.confand pasted the Tunnel 1 section from the downloaded configuration file, with a few changes:
conn Tunnel1
authby=secret
auto=start
left=%defaultroute
leftid=VPN_SERVER_PUBLIC_IP
right=AWS_TUNNEL1_PUBLIC_IP
type=tunnel
ikelifetime=8h
keylife=1h
phase2alg=aes128-sha256
ike=aes128-sha256;modp2048
keyingtries=%forever
keyexchange=ike
leftsubnet=192.168.0.0/16
rightsubnet=10.0.0.0/16
dpddelay=10
dpdtimeout=30
dpdaction=restart_by_peer
| Setting | What it means |
|---|---|
left / leftid | The data center side: VPN-Server and its public IP |
right | The AWS side: the public IP of Tunnel 1 |
leftsubnet | The data center's network, DC-VPC's CIDR |
rightsubnet | The AWS network, AWS-VPC's CIDR |
phase2alg / ike | Updated to the stronger encryption settings AWS recommends |
auto=start | Bring the tunnel up automatically when IPsec starts |
dpd... settings | Dead peer detection: restart the tunnel if AWS stops responding |
I also removed the auth=esp line from the downloaded file, since newer LibreSwan versions don't accept it.
5. Added the pre-shared key
I created the secrets file:
sudo nano /etc/ipsec.d/aws.secretsand pasted the line from step 5 of the configuration file, in this format:
VPN_SERVER_PUBLIC_IP AWS_TUNNEL1_PUBLIC_IP: PSK "PRE_SHARED_KEY"
The pre-shared key is a password both ends of the tunnel use to prove who they are.
6. Started IPsec
sudo systemctl start ipsec.service
sudo systemctl status ipsec.serviceThe status showed active (running). A running service doesn't guarantee the tunnel works, though, so I tested connectivity next.
Part 5: Routing Traffic Through the VPN
1. First ping test
From VPN-Server, I pinged EC2-A's private IP:
ping EC2_A_PRIVATE_IPIt got no replies. The tunnel could carry traffic to AWS, but EC2-A's route table had no route back to 192.168.0.0/16, so its replies had nowhere to go.
2. Added a route on the AWS side
In Canada (Central), I opened the private subnet's route table in AWS-VPC, clicked Edit routes, and added:
| Destination | Target |
|---|---|
192.168.0.0/16 | Virtual Private Gateway |
I ran the ping again, and this time EC2-A replied. In the VPN connection's Tunnel details tab, Tunnel 1 now showed Up. Tunnel 2 stayed Down, because I had only configured Tunnel 1 on VPN-Server.
3. Turned off Source/Destination Check on VPN-Server
At this point, only VPN-Server itself could reach EC2-A. For EC2-B to use the VPN, VPN-Server had to forward other instances' traffic.
By default, EC2 drops any traffic passing through an instance that isn't addressed to or from that instance. That's the Source/Destination Check. In US East (N. Virginia), I selected VPN-Server, clicked Actions → Networking → Change source/destination check, checked Stop, and saved.
4. Added a route on the data center side
I opened DC-VPC's private route table, clicked Edit routes, and added:
| Destination | Target |
|---|---|
10.0.0.0/16 | Instance → VPN-Server |
Now any traffic from the private subnet headed for AWS-VPC would go to VPN-Server, which would send it through the tunnel.
Part 6: Verifying End-to-End Connectivity
1. Connected to EC2-B through VPN-Server
EC2-B had no public IP, so I reached it through VPN-Server. On VPN-Server, I created a key file:
nano key.pempasted in the contents of EC2-B's private key, saved it, and locked down its permissions:
chmod 400 key.pemThen I connected to EC2-B using its private IP:
ssh -i key.pem ec2-user@EC2_B_PRIVATE_IP2. Pinged EC2-A from EC2-B
From EC2-B, I pinged EC2-A's private IP:
ping EC2_A_PRIVATE_IPEC2-A replied. Traffic from a private server in the simulated data center was traveling through VPN-Server, across the encrypted IPsec tunnel, through the Virtual Private Gateway, and into a private subnet in a different region, without either private instance having a public IP.
What I Learned
- An AWS Site-to-Site VPN connects a VPC to an on-premises network through an encrypted IPsec tunnel over the public internet.
- The AWS end is a Virtual Private Gateway, attached to the VPC. The customer end is described to AWS as a Customer Gateway, which points to the public IP of the customer's VPN device.
- Every VPN connection has two tunnels to different AWS endpoints for redundancy. In production, both would be configured so the connection survives if one goes down.
- Static routing means listing the remote network's CIDR by hand. Dynamic routing uses BGP to share routes automatically.
- The two networks need non-overlapping CIDR blocks (
10.0.0.0/16and192.168.0.0/16), or routing between them wouldn't work. - A working tunnel isn't enough. Both sides need routes: AWS-side traffic to
192.168.0.0/16must go to the VGW, and data-center-side traffic to10.0.0.0/16must go to the VPN device. - An EC2 instance acting as a router needs IP forwarding turned on in Linux and its Source/Destination Check turned off in EC2. Otherwise it drops traffic that isn't addressed to itself.
- A route table entry can target an EC2 instance, which is how the private subnet's traffic was sent to
VPN-Server. - Compared with VPC peering, a Site-to-Site VPN is for connecting to networks outside AWS, like an office or data center. Peering connects two VPCs over AWS's own network without encryption overhead.
Notes
VPN-Serverstarted the tunnel itself (auto=start), so its security group didn't need inbound rules for IPsec. If AWS had to start the connection, the security group would need to allow UDP 500 and UDP 4500 (IKE and IPsec NAT traversal) from the tunnel IPs.- Copying
EC2-B's private key ontoVPN-Serverwas fine for a lab, but it's risky in practice. SSH's jump host feature (ssh -J) would avoid putting the key on the server. - The downloaded configuration file and
aws.secretscontain the pre-shared keys. They shouldn't be shared, screenshotted into documents, or committed to a repository.
Cleanup
This lab created resources in two regions, and several of them are billed by the hour: the Site-to-Site VPN connection, all three EC2 instances, and VPN-Server's public IPv4 address. I deleted everything in this order:
1. Deleted the VPN connection (Canada Central)
Under Site-to-Site VPN connections, I selected the connection, clicked Actions → Delete VPN connection, and confirmed. This is the most important step, since the VPN connection is billed every hour it exists, whether or not traffic is flowing.
2. Deleted the Customer Gateway (Canada Central)
Once the VPN connection showed Deleted, I went to Customer gateways, selected the gateway, and clicked Actions → Delete customer gateway.
3. Detached and deleted the Virtual Private Gateway (Canada Central)
Under Virtual private gateways, I selected the gateway, clicked Actions → Detach from VPC, waited for it to show Detached, then clicked Actions → Delete virtual private gateway.
4. Terminated the instances
In Canada (Central), I terminated EC2-A. In US East (N. Virginia), I terminated VPN-Server and EC2-B. I waited until all three showed Terminated.
5. Deleted the security groups
In each region, I deleted the security groups created for the instances.
6. Deleted the data center network (N. Virginia)
- Detached the Internet Gateway from
DC-VPCand deleted it. - Deleted the public and private subnets.
- Deleted the two custom route tables.
- Deleted
DC-VPC.
7. Deleted the AWS network (Canada Central)
I deleted the private subnet, its custom route table, and AWS-VPC.
8. Confirmed nothing was left
In both regions, I checked that Site-to-Site VPN connections, Virtual private gateways, Customer gateways, Instances, and Your VPCs didn't show anything from this lab. I made sure not to delete the default VPC in either region.