Secure Reverse Proxying Behind NAT Using WireGuard and Caddy
I’m migrating my existing reverse proxy and TLS termination setup from Pangolin to a simpler and more flexible architecture using Caddy and WireGuard.
Table of Contents
Prerequisite
- Server with public IP
- Local Server (NAT or CGNAT)
- Domain from Cloudflare
VPS (Public IP)
Install Wireguard.
1sudo apt update
2sudo apt install wireguard resolvconf -y
Create private key, public key and remove unnecessary permissions from the keys.
1sudo wg genkey | tee /etc/wireguard/server_private_key | sudo wg pubkey > /etc/wireguard/server_public_key
2
3sudo chmod go= /etc/wireguard/server_private_key
Create Wireguard server configuration.
1EXT_IF=$(ip route get 1.1.1.1 | awk '{for(i=1;i<=NF;i++) if ($i=="dev") print $(i+1)}')
2
3sudo cat > /etc/wireguard/wg0.conf <<EOF
4# Server configuration
5[Interface]
6PrivateKey = $(cat /etc/wireguard/server_private_key)
7Address = 10.0.0.1/24
8
9# PostUP - Commands to run after starting WireGuard
10PostUp = iptables -A FORWARD -i wg0 -o wg0 -j ACCEPT
11PostUp = iptables -t nat -I POSTROUTING 1 -s 10.0.0.0/24 -o ${EXT_IF} -j MASQUERADE
12PostUp = iptables -I INPUT 1 -i wg0 -j ACCEPT
13PostUp = iptables -I FORWARD 1 -i ${EXT_IF} -o wg0 -j ACCEPT
14PostUp = iptables -I FORWARD 1 -i wg0 -o ${EXT_IF} -j ACCEPT
15PostUp = iptables -A FORWARD -i ${EXT_IF} -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
16
17# Accept connections to WireGuard and HTTP/HTTPS ports
18PostUp = iptables -I INPUT 1 -i ${EXT_IF} -p udp --dport 51820 -j ACCEPT
19PostUp = iptables -I INPUT 1 -i ${EXT_IF} -p tcp --dport 80 -j ACCEPT
20PostUp = iptables -I INPUT 1 -i ${EXT_IF} -p tcp --dport 443 -j ACCEPT
21
22# PostDown - Commands to run after stopping WireGuard
23PostDown = iptables -D FORWARD -i wg0 -o wg0 -j ACCEPT
24PostDown = iptables -t nat -D POSTROUTING -s 10.0.0.0/24 -o ${EXT_IF} -j MASQUERADE
25PostDown = iptables -D INPUT -i wg0 -j ACCEPT
26PostDown = iptables -D FORWARD -i ${EXT_IF} -o wg0 -j ACCEPT
27PostDown = iptables -D FORWARD -i wg0 -o ${EXT_IF} -j ACCEPT
28PostDown = iptables -D FORWARD -i e${EXT_IF} -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
29
30PostDown = iptables -D INPUT -i ${EXT_IF} -p udp --dport 51820 -j ACCEPT
31PostDown = iptables -D INPUT -i ${EXT_IF} -p tcp --dport 80 -j ACCEPT
32PostDown = iptables -D INPUT -i ${EXT_IF} -p tcp --dport 443 -j ACCEPT
33
34# WireGuard port
35ListenPort = 51820
36
37# Client Configuration
38[Peer]
39PublicKey = $(cat /etc/wireguard/client_public_key)
40AllowedIPs = 10.0.0.2/32
41EOF
Enable IP forwarding and WireGuard service.
1# Enable IP forwarding
2sudo sed -i 's/#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/' /etc/sysctl.conf
3sudo sysctl -p
4# Enable WireGuard service
5sudo systemctl enable wg-quick@wg0.service
6sudo systemctl start wg-quick@wg0.service
Check Wireguard status.
1sudo wg show
Create peer config, later copy this to your local node.
1mkdir /opt/wireguard/peer001
2
3sudo wg genkey | tee /opt/wireguard/peer001/client_private_key | sudo wg pubkey > /opt/wireguard/peer001/client_public_key
4
5sudo cat > /opt/wireguard/peer001/peer_wg0.conf <<EOF
6[Interface]
7PrivateKey = $(cat /opt/wireguard/peer001/client_private_key)
8Address = 10.0.0.2/32
9
10[Peer]
11PublicKey = $(cat /etc/wireguard/server_public_key)
12Endpoint = $(curl ifconfig.me):51820
13AllowedIPs = 10.0.0.0/8
14PersistentKeepalive = 25
15EOF
Optional, create QR for the configurations.
1qrencode -o /opt/wireguard/peer001/wireguard_qr.png < /opt/wireguard/peer001/peer_wg0.conf
Repeat this process if you want to create and add more peer. Make sure to create dir per config and change the peer IP address.
Also add the private key and IP address of new peer.
/etc/wireguard/wg0.conf
1PostDown = iptables -D INPUT -i ${EXT_IF} -p udp --dport 51820 -j ACCEPT
2PostDown = iptables -D INPUT -i ${EXT_IF} -p tcp --dport 80 -j ACCEPT
3PostDown = iptables -D INPUT -i ${EXT_IF} -p tcp --dport 443 -j ACCEPT
4
5ListenPort = 51820
6
7[Peer]
8PublicKey = $(cat /etc/wireguard/client_public_key)
9AllowedIPs = 10.0.0.2/32
10
11# ADD HERE
12[Peer]
13PublicKey = NEW_CLIENT_PUBLIC_KEY
14AllowedIPs = 10.0.0.3/32
Wireguard Peer (Local Server)
Install wireguard.
1sudo apt update
2sudo apt install wireguard resolvconf -y
Enable IP forwarding and WireGuard service.
1sudo sed -i 's/#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/' /etc/sysctl.conf
2sudo sysctl -p
Copy Wireguard peer config. /etc/wireguard/wg0.conf
1[Interface]
2PrivateKey = 8DW1ba8GHfA9hKAXrm17ssWX0aXQ4Za2ozSsHN6c1Z0c=
3Address = 10.0.0.2/32
4
5[Peer]
6PublicKey = 6F8h1/L2xwYLXe32ffBA+97pjVDsPJ7/uFkAT/OMChM=
7Endpoint = <YOUR_PUBLIC_IP>:51820
8AllowedIPs = 10.0.0.0/24
9PersistentKeepalive = 25
Add this config, application you want to reverse proxy to vps server. A nginx server is running in 192.168.254.101:80
Final config.
1[Interface]
2PrivateKey = 8DW1ba8GHfA9hKAXrm17ssWX0aXQ4Za2ozSsHN6c1Z0c=
3Address = 10.0.0.2/32
4
5# add this config, application you want to reverse proxy to vps server
6# a nginx server is running in 192.168.254.101:80
7
8# PostUp
9PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
10# Nexcloud port-forward
11PostUP = iptables -t nat -A PREROUTING -p tcp -i wg0 --dport 8080 -j DNAT --to 192.168.254.101:80
12# PostDown
13PostDown = iptables -t nat -D PREROUTING -s tcp -i wg0 --dport 8080 -j DNAT --to 192.168.254.101:80
14PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
15
16[Peer]
17PublicKey = 6F8h1/L2xwYLXe32ffBA+97pjVDsPJ7/uFkAT/OMChM=
18Endpoint = <YOUR_PUBLIC_IP>:51820
19AllowedIPs = 10.0.0.0/24
20PersistentKeepalive = 25
Enable and start WireGuard service.
1sudo systemctl enable wg-quick@wg0.service
2sudo systemctl start wg-quick@wg0.service
Caddy (VPS)
My domain is hosted in Cloudflare so we’ll be using ghcr.io/caddybuilds/caddy-cloudflare:latest image.
compose.yaml
1services:
2 caddy:
3 image: ghcr.io/caddybuilds/caddy-cloudflare:latest
4 container_name: caddy
5 restart: unless-stopped
6 cap_add:
7 - NET_ADMIN
8 ports:
9 - 80:80
10 - 443:443
11 - 443:443/udp
12 volumes:
13 - /srv/volume/caddy/Caddyfile:/etc/caddy/Caddyfile
14 - /srv/volume/caddy/srv:/srv
15 - /srv/volume/caddy/data:/data
16 - /srv/volume/caddy/config:/config
17 environment:
18 - CLOUDFLARE_API_TOKEN=jn4cKIBcdYT6UqUwtQeMZPyif1IC8ATPGlv3Pwlt
19 networks:
20 - caddy
21 labels:
22 - homepage.group=Utilities
23 - homepage.name=Caddy
24 - homepage.icon=caddy
25 - homepage.description=Open source web server with automatic HTTPS
26 - homepage.widget.type=caddy
27 - homepage.widget.url=http://caddy:2019
28 healthcheck:
29
30 test:
31 - CMD-SHELL
32 - nc -z 127.0.0.1 80 || exit 1
33 interval: 30s
34 timeout: 5s
35 retries: 3
36 start_period: 10s
37networks:
38 caddy:
39 name: caddy
40 external: true
Make sure to create mount dir, for my setup it is in /srv/volume/caddy
1mkdir /srv/volume/caddy
Create Caddyfile and add you Cloudflare API token, check this post if you haven’t.
1touch /srv/volume/caddy/Caddyfile
Caddyfile
1{
2 admin :2019
3}
4
5*.<YOUR_DOMAIN> {
6
7 tls {
8 dns cloudflare <CLOUDFLARE_API_TOKEN>
9 }
10
11 # Add this config to tunnel your application.
12
13 @test host test.<YOUR_DOMAIN>
14 handle @test {
15 reverse_proxy 10.0.0.2:8080
16 }
17}
Create docker network and start Caddy.
1docker network create caddy
2docker compose up -d
Verify.
1curl https://test.marktaguiad.dev
2<!DOCTYPE html>
3<html>
4<head>
5<title>Welcome to nginx!</title>
6<style>
7html { color-scheme: light dark; }
8body { width: 35em; margin: 0 auto;
9font-family: Tahoma, Verdana, Arial, sans-serif; }
10</style>
11</head>
12<body>
13<h1>Welcome to nginx!</h1>
14<p>If you see this page, the nginx web server is successfully installed and
15working. Further configuration is required.</p>
16
17<p>For online documentation and support please refer to
18<a href="http://nginx.org/">nginx.org</a>.<br/>
19Commercial support is available at
20<a href="http://nginx.com/">nginx.com</a>.</p>
21
22<p><em>Thank you for using nginx.</em></p>
23</body>
24</html>
Docker Network
This just add complexity in your setup, but if you are cautious about security-since ports will be expose in you Host IP then I recommend this setup.
Create docker network, let’s call this wg.
1docker network create \
2 --driver bridge \
3 --subnet 172.30.0.0/24 \
4 wg
Run docker application, let’s use nginx as example.
compose.yaml
1services:
2 app:
3 image: nginx
4 container_name: app
5
6 networks:
7 wg:
8 ipv4_address: 172.30.0.69
9
10networks:
11 wg:
12 external: true
In you wg0.conf add this. Note that you can use any dport (8069), just make sure it doesn’t conflict with other application you are reverse proxying.
1[Interface]
2PrivateKey = 8DW1ba8GHfA9hKAXrm17ssWX0aXQ4Za2ozSsHN6c1Z0c=
3Address = 10.0.0.2/32
4
5PostUp = iptables -t nat -A PREROUTING -i wg0 -p tcp --dport 8069 -j DNAT --to-destination 172.30.0.69:80
6PostUp = iptables -A FORWARD -p tcp -d 172.30.0.69 --dport 80 -j ACCEPT
7
8PostDown = iptables -t nat -D PREROUTING -i wg0 -p tcp --dport 8069 -j DNAT --to-destination 172.30.0.69:8080
9PostDown = iptables -D FORWARD -p tcp -d 172.30.0.69 --dport 8069 -j ACCEPT
10
11[Peer]
12PublicKey = 6F8h1/L2xwYLXe32ffBA+97pjVDsPJ7/uFkAT/OMChM=
13Endpoint = <YOUR_PUBLIC_IP>:51820
14AllowedIPs = 10.0.0.0/8
15PersistentKeepalive = 25
In your Caddyfile you add.
Caddyfile
1{
2 admin :2019
3}
4
5*.<YOUR_DOMAIN> {
6
7 tls {
8 dns cloudflare <CLOUDFLARE_API_TOKEN>
9 }
10
11 # Add this config to tunnel your application.
12
13 @web host web.<YOUR_DOMAIN>
14 handle @web {
15 reverse_proxy 10.0.0.2:8069
16 }
17}