Securing Inter-Server Communication with WireGuard and Reverse Proxy Afdrukken

  • 1

If you run services across multiple VPS servers, those services probably talk to each other over the public internet. Database connections, reverse proxy traffic, API calls - all crossing networks you don't control. This guide fixes that.

We'll set up a private WireGuard network between your servers and route all inter-server traffic through it. Public-facing requests hit your reverse proxy over HTTPS. Everything behind it travels through an encrypted tunnel. Services never touch the public internet.

Architecture

User (HTTPS) → proxy-server (nginx) → WireGuard tunnel → backend-server (service)

Three components:

  • Proxy server - runs nginx, handles SSL termination, connects to backends over WireGuard
  • Backend servers - run your actual services (apps, databases, dashboards), only reachable via tunnel
  • WireGuard tunnel - encrypted private network connecting all servers

The proxy server is a hub. Backend servers connect to it. Services bind to the WireGuard interface only - no public ports exposed.

Requirements

  • Two or more Linux VPS (KVM) running Debian 12+ or Ubuntu 22.04+
  • Root access on all servers
  • One server designated as the proxy/hub
  • WireGuard kernel support (available on all KVM VPS at ArkHost)

Step 1: Install WireGuard

Run on all servers:

apt update
apt install -y wireguard

Step 2: Generate Keys

On each server, generate a key pair:

wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key
chmod 600 /etc/wireguard/private.key

Note down each server's public key. You'll need them for the configuration.

Step 3: Configure the Proxy Server (Hub)

This is your central node. All other servers connect to it.

Create /etc/wireguard/wg0.conf:

[Interface]
Address = 10.50.0.1/24
PrivateKey = <proxy-server-private-key>
ListenPort = 51820

[Peer]
# backend-1
PublicKey = <backend-1-public-key>
AllowedIPs = 10.50.0.2/32

[Peer]
# backend-2
PublicKey = <backend-2-public-key>
AllowedIPs = 10.50.0.3/32

Add a peer block for each backend server. Assign sequential IPs: 10.50.0.2, 10.50.0.3, etc.

Step 4: Configure Backend Servers

Each backend server gets a client configuration pointing to the proxy server.

Create /etc/wireguard/wg0.conf on backend-1:

[Interface]
Address = 10.50.0.2/24
PrivateKey = <backend-1-private-key>

[Peer]
PublicKey = <proxy-server-public-key>
AllowedIPs = 10.50.0.1/32
Endpoint = <proxy-server-public-ip>:51820
PersistentKeepalive = 25

Same for backend-2, using Address = 10.50.0.3/24 and its own private key.

Step 5: Firewall

On the proxy server, allow WireGuard only from known backend IPs:

ufw allow from <backend-1-public-ip> to any port 51820 proto udp
ufw allow from <backend-2-public-ip> to any port 51820 proto udp

Do not open 51820 to the world. Restrict it to your server IPs.

Step 6: Enable and Start

On all servers:

systemctl enable --now wg-quick@wg0

Verify the tunnel:

wg show

You should see a "latest handshake" timestamp for each peer. If it's empty, the connection didn't establish - check firewall rules and keys.

Test connectivity:

# From proxy server
ping 10.50.0.2

# From backend-1
ping 10.50.0.1

Step 7: Bind Services to the Tunnel

This is the critical part. Your services should only listen on the WireGuard IP, not on all interfaces.

For Docker containers, change the port binding in your docker-compose.yml:

# Before (exposed publicly)
ports:
  - "8080:8080"

# After (only reachable via tunnel)
ports:
  - "10.50.0.2:8080:8080"

For native services, bind to the WireGuard IP in their configuration. For example, a Node.js app:

app.listen(3000, '10.50.0.2');

After this change, the service is invisible from the public internet. It only responds to requests coming through the tunnel.

Step 8: Configure nginx Reverse Proxy

On the proxy server, create a virtual host for each service. The key line is proxy_pass pointing to the backend's WireGuard IP.

server {
    listen 443 ssl http2;
    server_name app.example.com;

    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

    location / {
        proxy_pass http://10.50.0.2:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

The Upgrade and Connection headers are needed for WebSocket support. Include them even if your app doesn't use WebSockets - it won't cause issues and saves debugging later.

Step 9: Restrict Access (Optional)

For admin panels and internal tools, add IP restrictions in nginx:

location / {
    allow 10.66.66.0/24;    # Your VPN network
    allow 203.0.113.50;     # Your static IP
    deny all;

    proxy_pass http://10.50.0.2:8080;
    # ... proxy headers
}

Anyone not on the allow list gets a 403.

Adding More Servers

To add a new backend:

  1. Install WireGuard on the new server
  2. Generate keys
  3. Add a [Peer] block to the proxy server's wg0.conf
  4. Create wg0.conf on the new server pointing to the proxy
  5. Add a firewall rule on the proxy for the new server's public IP
  6. Restart the tunnel on the proxy: wg-quick down wg0 && wg-quick up wg0
  7. Start the tunnel on the new server: systemctl enable --now wg-quick@wg0

No changes needed on existing backend servers.

What This Gets You

  • No public ports - Services don't listen on public interfaces. Port scans find nothing.
  • Encrypted inter-server traffic - WireGuard uses ChaCha20 encryption. No plaintext between servers.
  • Centralized SSL - One server handles all certificates. Backends don't need SSL configuration.
  • Simple scaling - Adding a server is one peer block and a firewall rule.
  • Minimal overhead - WireGuard runs in kernel space. The performance impact is negligible.

Troubleshooting

No handshake after setup

Check that the proxy server's firewall allows UDP 51820 from the backend's public IP. Verify the public keys match on both ends.

Tunnel is up but can't reach the service

Make sure the service is bound to the WireGuard IP, not 0.0.0.0 or 127.0.0.1. Check with ss -tlnp | grep <port>.

nginx returns 502 Bad Gateway

The backend service isn't running or isn't listening on the expected port. Check with curl http://10.50.0.2:8080 from the proxy server.

Tunnel drops after logout

If running WireGuard as a non-root user with Podman, enable session persistence: loginctl enable-linger username. With systemd and root, this doesn't apply.

Summary

Point your DNS to the proxy server. Let nginx handle SSL and routing. Services sit behind an encrypted tunnel with no public exposure. It takes 15 minutes to set up and scales to as many servers as you need.


Was dit antwoord nuttig?

« Terug

WHOIS Information

×
Loading WHOIS information...