← Back to list

Building a Low-Cost Site-to-Site VPN in AWS Using Public IP Encryption Domains

The Problem

Don Spidell · 2026-08-03 15:11 · 0 claps · 8.0 min read
#aws-networking #site-to-site-vpn
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud 🔒 · Cybersecurity

Building a Low-Cost Site-to-Site VPN in AWS Using Public IP Encryption Domains

The Problem

I recently had to build a site-to-site VPN between AWS and a vendor with a requirement that doesn’t line up with how VPC networking is normally designed:

All traffic inside the tunnel must use public IP space. RFC1918 addresses are not allowed.

That requirement breaks the default assumption. Inside a VPC, everything is private by design. You control ingress and egress, but internally you’re always working with RFC1918 ranges. So the question becomes: how do you present public IP space inside a VPN tunnel without redesigning the entire environment?

The Expensive Option

There are commercial ways to solve this cleanly. You can deploy a third-party firewall appliance from the AWS Marketplace such as Palo Alto or FortiGate, terminate the VPN there, and use NAT and policy controls to make the traffic look exactly the way the vendor expects. That approach gives you a polished interface, mature operational tooling, and a lot of control over routing and security policy.

It also comes with the usual baggage: licensing, larger instance footprints, and more operational overhead than most small environments actually need. In a production environment with inspection, compliance, or HA requirements, that may be justified. In a development environment moving a small amount of traffic, it usually is not.

Why I Didn’t Use the Obvious Solutions

AWS’s managed Site-to-Site VPN was the first place to look, followed by the usual appliance-based pattern. Both are familiar. Both work well for standard site-to-site connectivity.

The problem is that AWS Site-to-Site VPN does not support this requirement in a straightforward way. It is designed around defining local and remote CIDR ranges as encryption domains, and in practice those are almost always private ranges on the AWS side. While you can technically include public IP ranges in the configuration, AWS does not provide a native mechanism to perform source NAT before traffic enters the tunnel. That means you cannot easily present all traffic as a single public IP identity without introducing an intermediate device.

In other words, the limitation is not that AWS cannot carry public IP traffic, but that it does not give you a place to transform the traffic into that form before encryption.

That effectively rules out using the managed service directly for this scenario.

But this was a dev environment moving a small amount of SFTP traffic, and I didn’t want to solve a narrow networking problem by introducing a full firewall stack with recurring licensing costs and more infrastructure than the workload warranted.

What I needed was something lighter: a way to satisfy the vendor’s public-IP requirement without turning a simple tunnel into a full security platform.

The Approach

The design came down to one simple idea: translate traffic before it enters the tunnel.

Instead of trying to make the VPC itself use public space, I left the VPC exactly as it was — private and isolated — and moved the transformation to the edge. Traffic leaves the VPC, reaches an EC2 instance running StrongSwan, gets source NAT’d to a public IP, and only then enters the IPsec tunnel.

From the vendor’s perspective, everything inside the tunnel is public IP space. Inside AWS, the workloads stay on private addresses.

Architecture

At a high level, the traffic flow looks like this:

VPC (10.0.0.0/16) ↓ StrongSwan EC2 instance ↓ (SNAT to public IP) IPsec Tunnel ↓ Vendor Network

The EC2 instance becomes both the VPN endpoint and the translation boundary.

Why StrongSwan

StrongSwan was a good fit because it gives you full control over IPsec behavior without forcing you into a heavyweight appliance model. It is flexible, well understood, and works well when you need to manipulate selectors, marks, routing, and NAT in ways that managed services do not make especially convenient.

For this use case, that flexibility mattered more than having a GUI.

EC2 Instance Configuration

The EC2 instance effectively acts as a lightweight VPN appliance, so its network configuration matters just as much as the IPsec configuration.

At a minimum, the instance needs an Elastic IP associated with its primary network interface to act as the VPN peer. A second Elastic IP is used as the NAT identity. This is typically implemented by assigning a secondary private IP address to the same ENI and associating an Elastic IP with it, although a secondary ENI can also be used if you prefer to separate roles more explicitly.

Source and destination checks must be disabled on the instance. By default, EC2 instances only accept traffic destined for themselves. In this case, the instance is forwarding traffic between the VPC and the VPN tunnel, so that behavior needs to be turned off.

Security groups should allow the required IPsec traffic, typically UDP 500 and 4500 for IKE and NAT-T, along with any management access you need. Network ACLs should also allow this traffic if they are in use.

Routing inside the VPC is what actually directs traffic to the instance. In the route tables associated with your subnets, you need a route for the remote destination (for example, a /32 for the vendor host or a larger CIDR if applicable) that points to the ENI of the StrongSwan instance. This ensures that traffic destined for the far side of the tunnel is sent to the instance instead of the default gateway or a NAT Gateway.

Once that route is in place, traffic flows from the workload to the instance, gets translated and encrypted, and then exits through the tunnel.

Without these pieces in place, the StrongSwan configuration can be perfectly valid and the tunnel can still fail to carry any traffic.

Two Public IPs, Not One

One of the first details that matters is public addressing. You need two Elastic IPs to keep the design clean.

The first is the VPN peer IP. That is the address used to establish the tunnel with the vendor. The second is the NAT identity IP. That is the address your internal traffic is translated to before it enters the tunnel.

Separating those roles makes troubleshooting much easier. One public address is responsible for tunnel establishment. The other represents the source identity seen across the VPN.

Route-Based VPN with VTI

The vendor required a route-based VPN, so I used a Virtual Tunnel Interface. That turned out to be helpful rather than restrictive. With a VTI, the tunnel behaves much more like a normal interface, which means you can reason about traffic using standard routing behavior instead of burying everything inside policy-based IPsec decisions.

That becomes especially important when NAT is involved. You want to be able to see where the packet is going, how it is being rewritten, and at what point it is expected to match the IPsec policy.

The Public Encryption Domain

This is the part that makes the design work.

In a normal site-to-site VPN, the local encryption domain would be a private subnet or CIDR range from the VPC. In this case, that would immediately violate the vendor requirement. So instead of using the VPC CIDR, the local encryption domain becomes a public /32 representing the NAT identity.

All traffic leaving the VPC is rewritten to that address before entering the tunnel:

iptables -t nat -A POSTROUTING -o vti0 -j SNAT — to-source <public-nat-ip>

That single change is what allows the private AWS environment to interoperate with a peer that refuses RFC1918 space inside the tunnel.

StrongSwan Configuration

The StrongSwan configuration itself is not complicated. The important thing is that the local subnet presented to IPsec is the public NAT address, not the internal VPC range.

config setup charondebug=”ike 2, knl 2, cfg 2"

conn vpn keyexchange=ikev2 auto=start

left=%defaultroute leftid=<your-vpn-peer-ip> leftauth=psk leftsubnet=<your-public-nat-ip>/32

right=<vendor-peer-ip> rightid=<vendor-peer-ip> rightauth=psk rightsubnet=<vendor-host>/32

ike=aes256gcm16-prfsha384-ecp384! esp=aes256gcm16-ecp384!

ikelifetime=86400s lifetime=3600s

mark=42

And the shared secret is defined as usual:

<your-vpn-peer-ip> <vendor-peer-ip> : PSK “<your-psk>”

Routing the Traffic into the Tunnel

Once the tunnel is negotiated, traffic still has to be steered into it correctly. That means building the VTI interface and making sure the routing matches the marks that StrongSwan is applying.

ip link add vti0 type vti local <your-vpn-peer-ip> remote <vendor-peer-ip> key 42 ip addr add 169.254.100.1/30 dev vti0 ip link set vti0 up

ip route add <vendor-host>/32 dev vti0

The key 42 on the VTI side needs to align with mark=42 in the StrongSwan connection. If those values do not match, the kernel will not associate the traffic with the correct tunnel and packets will never be encrypted the way you expect.

Where It Usually Breaks

Most of the time, bringing the tunnel up is the easy part. The harder part is proving that the traffic path and the encryption path are actually the same thing.

One failure mode is that the tunnel establishes successfully, but no traffic flows. That usually means the traffic selectors or encryption domain are wrong. IKE succeeds, but the data plane never matches the policy.

Another failure mode is that packets reach the VTI interface but are never encrypted. When that happens, the usual culprit is a mismatch between the VTI key and the mark used by StrongSwan, or traffic not being associated with the correct policy due to missing or incorrect marks.

The most frustrating case is when everything looks fine locally, but the vendor sees nothing. In practice, that usually means the SNAT rule was wrong or incomplete, so packets never took on the public identity required to match the tunnel policy.

The Details That Actually Matter

A few implementation details turned out to be critical.

The mark in the StrongSwan configuration has to match the VTI key. Policy installation needs to stay enabled so StrongSwan can bind traffic correctly. The instance must also have IP forwarding enabled at the OS level, since it is acting as a transit device. And the SNAT target has to be the same public /32 that the remote side expects as the local encryption domain.

Those are the kinds of details that can leave you with a healthy-looking tunnel and zero working traffic.

Bootstrapping with User Data

This can be built cleanly with EC2 user data. Installing StrongSwan, writing the configuration, and applying the networking rules is enough to have the instance come up ready to negotiate the tunnel.

cloud-config

package_update: true packages: — strongswan — strongswan-starter — iptables-persistent

write_files: — path: /etc/ipsec.conf permissions: ‘0600’ content: | config setup charondebug=”ike 2, knl 2, cfg 2"

conn vpn keyexchange=ikev2 auto=start

left=%defaultroute leftid=<your-vpn-peer-ip> leftauth=psk leftsubnet=<your-public-nat-ip>/32

right=<vendor-peer-ip> rightid=<vendor-peer-ip> rightauth=psk rightsubnet=<vendor-host>/32

ike=aes256gcm16-prfsha384-ecp384! esp=aes256gcm16-ecp384!

ikelifetime=86400s lifetime=3600s

mark=42

— path: /etc/ipsec.secrets permissions: ‘0600’ content: | <your-vpn-peer-ip> <vendor-peer-ip> : PSK “<your-psk>”

From there, the rest is normal Linux networking: iptables, routes, interface state, and persistent startup behavior.

Cost and Tradeoffs

That is really the appeal of this design. It runs on a small EC2 instance with no firewall licensing and very little infrastructure around it. For a low-throughput development workload, it is dramatically cheaper than deploying a commercial virtual appliance just to solve a narrow compatibility issue.

The tradeoff is that this is not a replacement for a real firewall platform. If you need HA, centralized policy management, advanced inspection, or compliance-heavy controls, this approach starts to lose its appeal quickly. It is a targeted solution, not a universal one.

Closing Thoughts

What made this interesting was not the tunnel itself. The tunnel was the easy part. The hard part was getting NAT, routing, and IPsec to agree on what the traffic was supposed to look like at each stage.

Once that alignment was in place, the design became simple and predictable. The VPC stayed private. The vendor saw public IP space inside the tunnel. And the whole thing ran on a small instance without the cost and complexity of a marketplace firewall appliance.

That is what made this approach worth using. It solved the actual requirement without dragging in a larger platform just because that is what the standard architecture diagrams usually show.


메타데이터
post_id
6c15ea8a4b2d
slug
building-a-low-cost-site-to-site-vpn-in-aws-using-public-ip-encryption-domains-6c15ea8a4b2d
url
https://medium.com/@donnyspi/building-a-low-cost-site-to-site-vpn-in-aws-using-public-ip-encryption-domains-6c15ea8a4b2d
canonical_url
https://medium.com/@donnyspi/building-a-low-cost-site-to-site-vpn-in-aws-using-public-ip-encryption-domains-6c15ea8a4b2d
author_url
https://medium.com/@donnyspi
status
ok
fetched_at
2026-08-09 07:18:25