journalaws / vpc

*Configuring an AWS Site-to-Site VPN

May 26, 2026·12 min read

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 sideSimulated data center
RegionCanada (Central) ca-central-1US East (N. Virginia) us-east-1
VPCAWS-VPC, 10.0.0.0/16DC-VPC, 192.168.0.0/16
SubnetsPrivate subnet 10.0.0.0/24, ca-central-1aPublic subnet 192.168.0.0/24, private subnet 192.168.100.0/24
InstancesEC2-A (private, no public IP)VPN-Server (public), EC2-B (private, no public IP)
Internet accessNoneInternet Gateway for the public subnet
VPN endpointVirtual Private GatewayVPN-Server running LibreSwan
VPN resourceDetails
Virtual Private Gateway (VGW)Attached to AWS-VPC
Customer Gateway (CGW)IP address = VPN-Server's public IP
Site-to-Site VPN connectionVGW ↔ 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:

SettingValue
AMIAmazon Linux
VPC / SubnetAWS-VPC / the private subnet
Auto-assign public IPDisable
Security group inboundAll 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

SubnetCIDRPurpose
Public subnet192.168.0.0/24Holds VPN-Server, the data center's VPN router
Private subnet192.168.100.0/24Holds EC2-B, an internal application server

4. Created route tables

Route tableAssociated withRoutes
Public route tablePublic subnet192.168.0.0/16 → local, 0.0.0.0/0 → Internet Gateway
Private route tablePrivate subnet192.168.0.0/16 → local

5. Launched VPN-Server

SettingValue
AMIAmazon Linux
VPC / SubnetDC-VPC / public subnet
Auto-assign public IPEnable
Security group inboundSSH 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

SettingValue
AMIAmazon Linux
VPC / SubnetDC-VPC / private subnet
Auto-assign public IPDisable
Security group inboundSSH 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:

SettingValue
Target gateway typeVirtual private gateway (the one attached to AWS-VPC)
Customer gatewayExisting (the one pointing to VPN-Server)
Routing optionsStatic
Static IP prefixes192.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 -y

2. Turned on packet forwarding

I opened the system settings file:

sudo nano /etc/sysctl.conf

and 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 = 1 let the server pass packets between networks, so it could act as a router instead of only handling its own traffic.
  • rp_filter = 0 turned off a check that would drop packets arriving on an unexpected interface, which can happen with tunneled traffic.
  • accept_source_route = 0 kept a safety setting that rejects packets trying to choose their own route.

I applied the changes without restarting:

sudo sysctl -p

The 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.conf

and 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
SettingWhat it means
left / leftidThe data center side: VPN-Server and its public IP
rightThe AWS side: the public IP of Tunnel 1
leftsubnetThe data center's network, DC-VPC's CIDR
rightsubnetThe AWS network, AWS-VPC's CIDR
phase2alg / ikeUpdated to the stronger encryption settings AWS recommends
auto=startBring the tunnel up automatically when IPsec starts
dpd... settingsDead 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.secrets

and 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.service

The 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_IP

It 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:

DestinationTarget
192.168.0.0/16Virtual 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:

DestinationTarget
10.0.0.0/16Instance → 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.pem

pasted in the contents of EC2-B's private key, saved it, and locked down its permissions:

chmod 400 key.pem

Then I connected to EC2-B using its private IP:

ssh -i key.pem ec2-user@EC2_B_PRIVATE_IP

2. Pinged EC2-A from EC2-B

From EC2-B, I pinged EC2-A's private IP:

ping EC2_A_PRIVATE_IP

EC2-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/16 and 192.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/16 must go to the VGW, and data-center-side traffic to 10.0.0.0/16 must 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-Server started 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 onto VPN-Server was 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.secrets contain 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)

  1. Detached the Internet Gateway from DC-VPC and deleted it.
  2. Deleted the public and private subnets.
  3. Deleted the two custom route tables.
  4. 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.