---
title: "Connect a VPC to an External Network with WireGuard"
description: "Connect an Exoscale VPC to an external network over WireGuard: a gateway instance routes traffic both ways with source addresses preserved."
url: https://community.exoscale.com/product/networking/vpc/recipes/wireguard-gateway/
product: "VPC"
section: product
last_updated: 2026-10-26
---
> For AI agents: the documentation index is at https://community.exoscale.com/llms.txt. Every page is available as markdown at `<page URL>index.md` or with `Accept: text/markdown`.

# Connect a VPC to an External Network with WireGuard

This guide is provided for informational purposes. Because it involves third party tooling we cannot guarantee things still work exactly as described here.

In this recipe, an instance of your VPC acts as a router between its Subnet and an external network.
It connects to a host of the external network over a [WireGuard](https://www.wireguard.com/)
tunnel, which gives a routed link over the public internet. Traffic flows in both directions and
keeps its original source addresses: the external network can open connections to the instances of
the VPC, and sees their VPC addresses.

![](wireguard-gateway.drawio.png "WireGuard gateway setup")

The setup uses three instances:

- `internal-vm` is attached to the VPC Subnet.
- `gateway-vm` is attached to the VPC Subnet and routes traffic between the
  VPC and the tunnel.
- `external-vm` is the other end of the tunnel and stands for the external network. It's not
  attached to the VPC. It can also be a machine outside of Exoscale, for example a router in your
  office, as long as it can reach the public IP of `gateway-vm`.

The VPC side is configured with the `exo` CLI. The tunnel and the forwarding are configured inside
the instances.

> [!TIP]
> Of course this setup is also possible without a wireguard tunnel. The point is to connect two
> networks together and have each be able to reach the other.

## Before You Start

You need:

- The [Exoscale CLI](https://github.com/exoscale/cli/releases/latest), configured with `exo config`.
- An SSH key registered in your Exoscale account.

Set the following variables in your shell. The commands below refer to them:

```console
export ZONE=ch-dk-2
export SSH_KEY=your-ssh-key-in-exoscale
export SG=allow-ssh-from-any
export VPC_CIDR=10.1.0.0/24

export WG_CIDR=192.168.0.0/24
export WG_PORT=51820
export WG_GATEWAY_IP=192.168.0.1
export WG_EXTERNAL_IP=192.168.0.2
```

The tunnel addresses are arbitrary, but `$WG_CIDR` must not overlap with `$VPC_CIDR`.

## Step 1: Create the VPC and Subnet

```console
exo networking vpc create my-vpc -z $ZONE
exo networking vpc subnet create my-vpc my-subnet -z $ZONE \
    --ipv4-block $VPC_CIDR --address-family inet4 --address-space private
```

## Step 2: Create the Instances

Create a Security Group that allows SSH, then create `internal-vm` and `external-vm`:

```console
exo compute security-group create $SG
exo compute security-group rule add $SG \
    --flow ingress --protocol tcp --port 22 --network 0.0.0.0/0

exo compute instance create internal-vm \
    --ssh-key $SSH_KEY --security-group $SG -z $ZONE
exo compute instance create external-vm \
    --ssh-key $SSH_KEY --security-group $SG -z $ZONE
```

Create `gateway-vm` with VPC IP forwarding enabled. The gateway forwards packets from other hosts
with their original source address, and without this setting the platform drops them. It can only
be chosen when the instance is created:

```console
exo compute instance create gateway-vm --ip-forwarding \
    --ssh-key $SSH_KEY --security-group $SG -z $ZONE
```

## Step 3: Attach the Instances to the Subnet

Attach `gateway-vm` and `internal-vm` to the Subnet. `external-vm` stays outside of the VPC and only
talks to the gateway through the tunnel.

```console
exo compute instance attach-to-subnet gateway-vm my-vpc my-subnet -z $ZONE
exo compute instance attach-to-subnet internal-vm my-vpc my-subnet -z $ZONE
```

## Step 4: Allow the Traffic in Security Groups

**Traffic between `gateway-vm` and `internal-vm` inside the VPC.** Create a Security Group that
allows all traffic between its members:

```console
exo compute security-group create internal-gateway-sg
exo compute security-group rule add internal-gateway-sg \
    --flow ingress --protocol all --security-group internal-gateway-sg
exo compute instance security-group add gateway-vm internal-gateway-sg -z $ZONE
exo compute instance security-group add internal-vm internal-gateway-sg -z $ZONE
```

**The tunnel itself**, UDP port `$WG_PORT` between the public IPs of `gateway-vm` and
`external-vm`:

```console
exo compute security-group create wg-tunnel-sg
exo compute security-group rule add wg-tunnel-sg \
    --flow ingress --protocol udp --port $WG_PORT --security-group wg-tunnel-sg
exo compute instance security-group add gateway-vm wg-tunnel-sg -z $ZONE
exo compute instance security-group add external-vm wg-tunnel-sg -z $ZONE
```

If `external-vm` is outside of Exoscale, it can't join the Security Group. Allow its public IP
with `--network <external-public-ip>/32` instead, and open the port in its own firewall.

**Traffic arriving through the tunnel.** Packets from `external-vm` reach `internal-vm` with a
source address in `$WG_CIDR`, not from a member of `internal-gateway-sg`, so the rules above don't
match them:

```console
exo compute security-group rule add internal-gateway-sg \
    --flow ingress --protocol all --network $WG_CIDR
```

In production, narrow these rules down to the protocols and ports your workload needs.

## Step 5: Note the Addresses

Look up the public IP and the VPC address of each instance:

```console
exo compute instance show internal-vm -z $ZONE
exo compute instance show gateway-vm -z $ZONE
exo compute instance show external-vm -z $ZONE
```

Save them as variables:

```console
export GATEWAY_VPC_IP=<gateway-vm-vpc-ip>     # 10.1.0.20 in the diagram
export INTERNAL_VPC_IP=<internal-vm-vpc-ip>   # 10.1.0.10 in the diagram
export INT_PUB=<internal-vm-public-ip>
export GW_PUB=<gateway-vm-public-ip>
export EXT_PUB=<external-vm-public-ip>
```

> [!TIP]
> If you exported all the variables mentioned above you can connect to the VMs with
> `ssh ubuntu@$INT_PUB`, `ssh ubuntu@$GW_PUB` and `ssh ubuntu@$EXT_PUB` respectively.

## Step 6: Configure the VPC Network Interfaces

The new network interface (created in step 3) of each instance gets its address through DHCP.
Some operating systems may not configure this automatically. In that case, connect to `gateway-vm` and
`internal-vm` over SSH and start the DHCP client on the VPC interface yourself, for example on
Ubuntu 22.04:

```console
sudo dhclient eth1
```

## Step 7: Install WireGuard and Generate Keys

On both `gateway-vm` and `external-vm`, install WireGuard and generate a key pair:

```console
sudo apt-get update && sudo apt-get install -y wireguard
wg genkey | sudo tee /etc/wireguard/private.key | wg pubkey | sudo tee /etc/wireguard/public.key
sudo chmod 600 /etc/wireguard/private.key
```

Print the public key of each side and note it down. Each side's configuration needs the public key
of the other:

```console
sudo cat /etc/wireguard/public.key
```

## Step 8: Configure the Gateway Side

On `gateway-vm`, enable forwarding in the operating system and keep the setting across reboots:

```console
# on gateway-vm
sudo sysctl -w net.ipv4.ip_forward=1
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-forwarding.conf
```

Create `/etc/wireguard/wg0.conf`. The addresses and the port match `$WG_GATEWAY_IP`,
`$WG_EXTERNAL_IP` and `$WG_PORT`:

```ini
[Interface]
Address = 192.168.0.1/24
ListenPort = 51820
PrivateKey = <gateway-vm-private-key>

[Peer]
# external-vm
PublicKey = <external-vm-public-key>
Endpoint = <external-vm-public-ip>:51820
AllowedIPs = 192.168.0.2/32
```

`AllowedIPs` acts as the routing table of WireGuard. It adds a route to `192.168.0.2/32` through
`wg0`, and only accepts packets from the peer whose source address is in this list. If more
networks sit behind `external-vm`, add their ranges here.

The gateway must forward packets between the VPC interface (`eth1`) and the tunnel:

```console
# on gateway-vm
sudo iptables -A FORWARD -i eth1 -o wg0 -j ACCEPT
sudo iptables -A FORWARD -i wg0 -o eth1 -j ACCEPT
```

These rules are lost when the instance reboots. Persist them with the mechanism of your operating
system, for example the `iptables-persistent` package on Ubuntu.

## Step 9: Configure the External Side

On `external-vm`, create `/etc/wireguard/wg0.conf`:

```ini
[Interface]
Address = 192.168.0.2/24
ListenPort = 51820
PrivateKey = <external-vm-private-key>

[Peer]
# gateway-vm, and the VPC Subnet behind it
PublicKey = <gateway-vm-public-key>
Endpoint = <gateway-vm-public-ip>:51820
AllowedIPs = 192.168.0.1/32, 10.1.0.0/24
```

The `10.1.0.0/24` entry (`$VPC_CIDR`) makes `internal-vm` reachable from `external-vm`. It adds a
route to the VPC Subnet through `wg0`, and accepts packets with a source address in the Subnet.

## Step 10: Bring the Tunnel Up

On both `gateway-vm` and `external-vm`, start the tunnel and enable it at boot:

```console
sudo systemctl enable --now wg-quick@wg0
```

Check that a handshake happened and that the transfer counters aren't zero, then check the routes:

```console
sudo wg show
ip route show dev wg0
```

From `gateway-vm`, `ping $WG_EXTERNAL_IP` now gets replies.

## Step 11: Route the Tunnel Network Through the Gateway

Add a Subnet route that sends the traffic for the tunnel network to `gateway-vm`:

```console
exo networking vpc route create my-vpc -z $ZONE \
    --subnet my-subnet \
    --destination $WG_CIDR \
    --target ip=$GATEWAY_VPC_IP \
    --description "tunnel network via gateway-vm"
```

The platform doesn't push this route to the instances of the Subnet. The operating system of
`internal-vm` still sends this traffic through its public interface, so add a matching route through
its VPC interface, with the same destination. The next hop is the Subnet gateway, the second address
of the Subnet (`10.1.0.1` here). The platform then applies the Subnet route and delivers the packets
to `gateway-vm`:

```console
# on internal-vm
sudo ip route add $WG_CIDR via 10.1.0.1 dev eth1
```

This route is lost when the instance reboots. Persist it in the network configuration of the
instance, for example with netplan on Ubuntu.

Both directions need this route: `internal-vm` uses it to send traffic to `external-vm`, and to
reply to connections opened by `external-vm`.

## Step 12: Test Both Directions

From `internal-vm` you can reach `external-vm`:

```console
ubuntu@internal-vm:~$ ping $WG_EXTERNAL_IP
```

From `external-vm` you can reach `internal-vm`:

```console
ubuntu@external-vm:~$ ping $INTERNAL_VPC_IP
```

If the traffic doesn't get through, check that:

- `gateway-vm` was created with `--ip-forwarding` and `net.ipv4.ip_forward` is `1`.
- `sudo wg show` reports a recent handshake on both sides.
- `ip route get $WG_EXTERNAL_IP` on `internal-vm` shows `eth1` as the interface.
- The `AllowedIPs` of each side list every address the other side sends from.

