Skip to content

Instantly share code, notes, and snippets.

@MurylloEx
Last active May 17, 2026 02:11
Show Gist options
  • Select an option

  • Save MurylloEx/45ae52d5f427c3891663f1f3e4684986 to your computer and use it in GitHub Desktop.

Select an option

Save MurylloEx/45ae52d5f427c3891663f1f3e4684986 to your computer and use it in GitHub Desktop.
Wireguard - [Public Bastion] <-- VPN Tunnel --> [NAT Server]

WireGuard Bastion Forwarding Setup

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.

Network Topology

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

Goal

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.

Why This Works

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.

EC2 WireGuard Configuration

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/32

The EC2 only needs to route the Ubuntu WireGuard address:

AllowedIPs = 172.30.30.2/32

EC2 PostUp Script

Example /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 MASQUERADE

Important: 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.1

EC2 PostDown Script

Example /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 || true

Ubuntu WireGuard Configuration

Example /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/0

Important Ubuntu Options

Table = off

This 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.

AllowedIPs = 0.0.0.0/0

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.

Policy Routing

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 51820

They 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.

Normal Residential Internet Still Works

Connections started by the Ubuntu server itself continue to use the default residential route.

For example:

curl https://ifconfig.me

should show the residential public IP, not the EC2 public IP.

Testing

Start WireGuard on both sides:

sudo wg-quick up wg0

Check the tunnel:

sudo wg

Check policy routing on Ubuntu:

ip rule
ip route show table 51820

Expected route table:

default dev wg0 scope link

Check that normal outbound traffic still uses the residential internet:

curl https://ifconfig.me

Check incoming traffic on the Ubuntu server:

sudo tcpdump -ni wg0

Or inspect service logs such as Nginx access logs.

The source IP should be the real client IP, not 172.30.30.1.

Security Notes

Do not commit real WireGuard private keys or preshared keys to a repository.

Use placeholders such as:

PrivateKey = UBUNTU_PRIVATE_KEY
PresharedKey = PRESHARED_KEY

If private keys were exposed, rotate them immediately.

Summary

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.

#!/bin/bash
set -e
# Remove TCP DNAT rules
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
# Remove UDP DNAT rules
iptables -t nat -D PREROUTING -i eth0 -p udp --dport 8443 -j DNAT --to-destination 172.30.30.2 2>/dev/null || true
# Remove FORWARD rules
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
# Remove outbound MASQUERADE rule
iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE 2>/dev/null || true
#!/bin/bash
set -e
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
# DNAT TCP traffic to the Ubuntu host behind WireGuard
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
# DNAT UDP traffic
iptables -t nat -A PREROUTING -i eth0 -p udp --dport 8443 -j DNAT --to-destination 172.30.30.2
# Allow traffic from EC2 to WireGuard
iptables -A FORWARD -i eth0 -o wg0 -d 172.30.30.2 -j ACCEPT
# Allow return traffic from WireGuard to the internet
iptables -A FORWARD -i wg0 -o eth0 -s 172.30.30.2 -m state --state RELATED,ESTABLISHED -j ACCEPT
# Outbound NAT for traffic leaving through the EC2 internet interface
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
[Interface]
PrivateKey = <YOUR-NAT-SERVER-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 = <YOUR-BASTION-PUBLIC-KEY>
PresharedKey = <YOUR-PRESHARED-KEY>
Endpoint = <YOUR-BASTION-PUBLIC-IP>:51820
PersistentKeepalive = 25
AllowedIPs = 0.0.0.0/0
[Interface]
Address = 172.30.30.1/32
ListenPort = 51820
PrivateKey = <YOUR-BASTION-PRIVATE-KEY>
SaveConfig = false
PostUp = /etc/wireguard/public-bastion-postup.sh
PostDown = /etc/wireguard/public-bastion-postdown.sh
[Peer]
PublicKey = <YOUR-NAT-SERVER-PUBLIC-KEY>
PresharedKey = <YOUR-PRESHARED-KEY>
AllowedIPs = 172.30.30.2/32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment