This setup uses an EC2 instance as a public bastion that forwards selected public ports to a private Ubuntu server behind a residential NAT.
The EC2 instance acts as the public entry point, while the Ubuntu server receives the real client traffic through a WireGuard tunnel.
Internet Client
|
v
Public EC2 Bastion
Public IP / Domain: bastion.example.com
WireGuard IP: 172.30.30.1
|
| WireGuard Tunnel
v
Private Ubuntu Server
WireGuard IP: 172.30.30.2
Residential NAT Network
This configuration provides the following behavior:
- Public traffic reaches the EC2 bastion.
- The EC2 forwards selected TCP/UDP ports to the Ubuntu server through WireGuard.
- The Ubuntu server sees the real client IP address, not only the WireGuard peer IP.
- The Ubuntu server continues to use its normal residential internet connection for outbound traffic.
- Only responses related to traffic received through WireGuard are returned through WireGuard.
The EC2 performs DNAT, forwarding incoming public traffic to the Ubuntu server:
Client IP -> EC2 Public IP -> 172.30.30.2
The EC2 does not SNAT traffic going into the WireGuard tunnel. Because of that, the Ubuntu server receives packets with the original client IP preserved.
To make the return path work correctly, the Ubuntu server uses policy routing:
Traffic sourced from 172.30.30.2 uses routing table 51820
That table sends replies back through wg0.
Normal outbound traffic from the Ubuntu server does not use source IP 172.30.30.2, so it continues to use the residential default gateway.
Example /etc/wireguard/wg0.conf on the EC2 bastion:
[Interface]
Address = 172.30.30.1/32
SaveConfig = false
PostUp = /etc/wireguard/postup.sh
PostDown = /etc/wireguard/postdown.sh
ListenPort = 51820
PrivateKey = EC2_PRIVATE_KEY
[Peer]
PublicKey = UBUNTU_PUBLIC_KEY
PresharedKey = PRESHARED_KEY
AllowedIPs = 172.30.30.2/32The EC2 only needs to route the Ubuntu WireGuard address:
AllowedIPs = 172.30.30.2/32Example /etc/wireguard/postup.sh:
#!/bin/bash
sysctl -w net.ipv4.ip_forward=1
iptables -F
iptables -t nat -F
iptables -X
iptables -t nat -X
iptables -P INPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -P OUTPUT ACCEPT
# Forward selected TCP ports to the Ubuntu server
iptables -t nat -A PREROUTING -i eth0 -p tcp -m multiport --dports 80,443,3306,2222,5432,6379,8443 -j DNAT --to-destination 172.30.30.2
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 30000:65535 -j DNAT --to-destination 172.30.30.2
# Forward selected UDP ports
iptables -t nat -A PREROUTING -i eth0 -p udp --dport 8443 -j DNAT --to-destination 172.30.30.2
# Allow forwarding from the public interface to WireGuard
iptables -A FORWARD -i eth0 -o wg0 -d 172.30.30.2 -j ACCEPT
# Allow return traffic from WireGuard to the public interface
iptables -A FORWARD -i wg0 -o eth0 -s 172.30.30.2 -m state --state RELATED,ESTABLISHED -j ACCEPT
# NAT outgoing traffic from the EC2 to the internet
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADEImportant: do not add SNAT/MASQUERADE on traffic going to wg0, otherwise the Ubuntu server will only see 172.30.30.1 as the source IP.
Avoid this:
iptables -t nat -A POSTROUTING -o wg0 -d 172.30.30.2 -j SNAT --to-source 172.30.30.1Example /etc/wireguard/postdown.sh:
#!/bin/bash
iptables -t nat -D PREROUTING -i eth0 -p tcp -m multiport --dports 80,443,3306,2222,5432,6379,8443 -j DNAT --to-destination 172.30.30.2 2>/dev/null || true
iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 30000:65535 -j DNAT --to-destination 172.30.30.2 2>/dev/null || true
iptables -t nat -D PREROUTING -i eth0 -p udp --dport 8443 -j DNAT --to-destination 172.30.30.2 2>/dev/null || true
iptables -D FORWARD -i eth0 -o wg0 -d 172.30.30.2 -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -i wg0 -o eth0 -s 172.30.30.2 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE 2>/dev/null || trueExample /etc/wireguard/wg0.conf on the private Ubuntu server:
[Interface]
PrivateKey = UBUNTU_PRIVATE_KEY
Address = 172.30.30.2/32
Table = off
PostUp = sysctl -w net.ipv4.conf.all.rp_filter=0
PostUp = sysctl -w net.ipv4.conf.default.rp_filter=0
PostUp = sysctl -w net.ipv4.conf.wg0.rp_filter=0
PostUp = ip route add default dev wg0 table 51820
PostUp = ip rule add from 172.30.30.2/32 table 51820
PostDown = ip rule del from 172.30.30.2/32 table 51820 2>/dev/null || true
PostDown = ip route flush table 51820 2>/dev/null || true
PostDown = sysctl -w net.ipv4.conf.all.rp_filter=2
PostDown = sysctl -w net.ipv4.conf.default.rp_filter=2
[Peer]
PublicKey = EC2_PUBLIC_KEY
PresharedKey = PRESHARED_KEY
Endpoint = bastion.example.com:51820
PersistentKeepalive = 25
AllowedIPs = 0.0.0.0/0This prevents wg-quick from automatically changing the main routing table.
Without this, AllowedIPs = 0.0.0.0/0 could make all Ubuntu traffic go through the WireGuard tunnel.
This allows the Ubuntu peer to accept packets from any public client IP through the tunnel.
This is required because the EC2 preserves the original source IP.
These lines create a separate routing table:
PostUp = ip route add default dev wg0 table 51820
PostUp = ip rule add from 172.30.30.2/32 table 51820They mean:
Any packet whose source IP is 172.30.30.2 must use routing table 51820.
That makes replies to forwarded public traffic return through WireGuard.
Connections started by the Ubuntu server itself continue to use the default residential route.
For example:
curl https://ifconfig.meshould show the residential public IP, not the EC2 public IP.
Start WireGuard on both sides:
sudo wg-quick up wg0Check the tunnel:
sudo wgCheck policy routing on Ubuntu:
ip rule
ip route show table 51820Expected route table:
default dev wg0 scope link
Check that normal outbound traffic still uses the residential internet:
curl https://ifconfig.meCheck incoming traffic on the Ubuntu server:
sudo tcpdump -ni wg0Or inspect service logs such as Nginx access logs.
The source IP should be the real client IP, not 172.30.30.1.
Do not commit real WireGuard private keys or preshared keys to a repository.
Use placeholders such as:
PrivateKey = UBUNTU_PRIVATE_KEY
PresharedKey = PRESHARED_KEYIf private keys were exposed, rotate them immediately.
This setup allows an EC2 instance to expose services running on a private Ubuntu server behind residential NAT, while preserving the real client IP address.
The EC2 performs DNAT only. The Ubuntu server uses policy routing to return WireGuard-originated traffic through the tunnel, while keeping its own outbound traffic on the residential internet connection.