Point to Cloud WireGuard With an Azure Hub VNet

This article will walk through how to set up WireGuard to enable remote client access to an internal web application, where the internal application is in an isolated Microsoft Azure Virtual Network (VNet). This is a common design, embodied in the Hub-Spoke Network Topology in Azure reference architecture.

In this article, we’ll first build an example hub VNet and an isolated spoke VNet; then we’ll launch a VM (Virtual Machine) for a bastion WireGuard server into the hub VNet, and a VM for an example web app into the spoke VNet. Through the WireGuard server, we’ll connect from our local workstation to the example app. The following diagram illustrates this scenario:

WireGuard Point-to-Cloud Diagram
Figure 1. WireGuard Point-to-Cloud with Azure Virtual Networks

The WireGuard server in this scenario, located in the wg-bastion subnet of the demo-hub VNet in the above diagram, allows your local workstation to access otherwise isolated subnets in spoke VNets — like the example app located in the internal-apps subnet of the demo-spoke VNet. The WireGuard server acts as a bastion host (also known as a relay server or jump box) for spoke VNets, providing similar functionality as the Azure Bastion service (albeit not as a managed service).

The advantage of using a WireGuard server instead of Azure Bastion, however, is that you’re not just limited to SSH and RDP — you can easily connect to any type of network application running in your internal networks. You can still use Azure Network Security Groups (NSGs) to limit access from the bastion host to those specific network applications, and you can use the internal firewall on the WireGuard server itself to customize access per user.

We’ll set up the WireGuard network in this article with a Point to Site Topology, where the WireGuard server (with an IP address of 10.0.0.2 in the above diagram) provides NAT (Network Address Translation) to your spoke VNets for the other WireGuard peers in its network (like your workstation, with an IP address of 10.0.0.1 in the above diagram). This is the easiest way to enable access to internal networks in Azure, requiring no changes to your existing Azure route tables or other network configuration. Once you complete the initial WireGuard server setup, you can grant more clients access to the same VPN (Virtual Private Network) simply by adding additional peers to the WireGuard server’s configuration file.

If you already have a hub VNet set up with several spoke VNets and internal applications, you don’t have to create new ones to try out this scenario. You can skip the first four sections of this article, and just create a subnet in your existing hub VNet for the WireGuard bastion server (in the article, I named this subnet wg-bastion and gave it the 10.10.0.0/24 address space). You can also skip sections 6 and 11, and just update the existing NSGs that apply to your internal applications to Allow Inbound HTTP Access Through WireGuard (or whatever protocol is appropriate for your applications, if not HTTP).

Otherwise, follow these steps:

Register SSH Key

If you already have an SSH key registered with Azure with which you can connect to the VMs we’ll launch in later steps, you can skip this first step. Otherwise, we’ll register one now.

Log into the Azure Portal, and navigate to the SSH keys service (type ssh keys into the Azure search bar and click the “SSH keys” result under “Services”). On the main service page, click the Create button:

SSH keys

Select the Subscription and Resource group under which you’ll launch your VMs, then enter a Key pair name. I’ll name mine justin-demo for this example. Select Upload existing public key for SSH public key source, and paste in the content of the id_rsa.pub file containing the public key of the SSH key pair that you’ll use to connect to your VMs (this content should begin with ssh-rsa, followed by a long string of base64-encoded text). Then click the Review + create button:

Create an SSH key: Basics

Then click the Create button:

Create an SSH key: Review

Create Hub VNet

Now navigate to the Virtual networks service (type virtual networks into the Azure search bar and click the “Virtual networks” result under “Services”).

Click the Create button:

Virtual networks

Select the Subscription and Resource group in which to create the VNet, then enter its Name. I’ll name the VNet demo-hub for this example. Then click the Next: IP Addresses button:

Create virtual network: Basics

Enter an IPv4 address space for the hub VNet. I’ll use 10.10.0.0/16 for this example. Then click the Add subnet button:

Create virtual network: IP Addresses

This subnet will be exclusively for WireGuard bastion servers, so for Subnet name, enter something like wg-bastion; and specify a Subnet address range that is a subnet of the IPv4 address space you selected for the VNnet — I’ll use 10.10.0.0/24 for this example (we’ll use this address range later in the Create Internal Application NSG step). Click the Add button:

Create virtual network: Add Subnet

After that, click the Review + create button:

Create virtual network: IP Addresses part 2

And then click the Create button:

Create virtual network: Review

Create Spoke VNet

Now we’ll repeat the process for a spoke VNet. Navigate back to the main page of to the Virtual networks service, and click the Create button:

Virtual networks

Select the Subscription and Resource group in which to create the spoke VNet, then enter its Name. I’ll name it demo-spoke for this example. Then click the Next: IP Addresses button:

Create virtual network: Basics

Enter an IPv4 address space. I’ll use 10.20.0.0/16 for this example. Then click the Add subnet button:

Create virtual network: IP Addresses

Enter a Subnet name and specify a Subnet address range that is a subnet of the IPv4 address space you selected for the VNet. I’ll name this subnet internal-apps, and use 10.20.0.0/24 for its range (we’ll use this range later in the Create WireGuard Server NSG section). Then click the Add button:

Create virtual network: Add Subnet

After that, click the Review + create button:

Create virtual network: IP Addresses part 2

And then click the Create button:

Create virtual network: Review

Peer the VNets

Now we need to peer the hub VNet (demo-hub) to the spoke VNet (demo-spoke) so that traffic can be exchanged between the two. Navigate to one of the subnets, and click the Peerings link in the leftnav (in this case I’ll start with the demo-hub VNet):

Virtual network overview

Click the Add button:

Virtual network Peerings

The resulting “Add peering” form has two parts, one for this VNet (demo-hub) and one for the VNet with which to peer (demo-spoke, the remote VNet from the perspective of demo-hub). In the first part of the form, enter a Peering link name for the demo-hub side of the connection. For the Traffic to remote virtual network setting, select Allow to allow traffic from demo-hub to be routed to demo-spoke. For the Traffic forwarded from remote virtual network setting, select Block to prevent traffic from the demo-spoke VNet from being forwarded into demo-hub. For the Virtual network gateway or Route Server setting, just select None:

Virtual network Add peering

Then scroll down to fill out the fields for the demo-spoke side of the connection. Enter a Peering link name for the peering link from the demo-spoke VNet’s perspective; use Resource manager for the Virtual network deployment model; select the Subscription of the demo-spoke VNet; and then select demo-spoke for the Virtual network. Next, select Allow for Traffic to remote virtual network; select Allow for Traffic forwarded from remote virtual network (this will allow our WireGuard server in the demo-hub VNet to forward traffic into demo-spoke); and select None for Virtual network gateway or Route Server. Then click the Add button:

Virtual network Add peering part 2

Note that we selected Allow for the Traffic forwarded from remote virtual network setting on the demo-spoke side of the peering link, but Block for the same setting on the demo-hub side. When using a hub-spoke network topology, the hub’s whole purpose is to forward traffic from other networks into the spoke networks (so on the demo-spoke side, we allow the hub to send us forwarded traffic); whereas the spokes should never need to forward traffic that originated elsewhere into the hub (so on the demo-hub side, we block the spoke from sending us forwarded traffic).

Create WireGuard Server NSG

Next, we’ll create an NSG (Network Security Group) to control network access to the WireGuard server that we’ll launch in the Launch WireGuard Server VM step. We’ll set up this NSG to allow inbound access through WireGuard (and also SSH, for administration) to our WireGuard server; and we’ll attach it to our dedicated WireGuard hub subnet.

Navigate to the Network security groups service (type network security groups into the Azure search bar and click the “Network security groups” result under “Services”). Click the Create button:

Network security groups

Select the Subscription and Resource group of the hub VNet, then enter a Name for the NSG. I’ll name the NSG demo-hub-wg-bastion for this example. Then click the Review + create button:

Create a network security group: Basics

Then click the Create button:

Create a network security group: Review

Once deployment has completed, click the Go to resource button:

Network security group: Deployment

Then click the Inbound security rules link in the leftnav:

Network security group: Overview

Allow Inbound WireGuard Access

The first inbound rule we’ll add will allow access to the WireGuard server over the WireGuard protocol (which uses UDP port 51820 by default, although you can select a different UDP port if you like). In this example, I’ll allow access to this WireGuard port from anywhere on the Internet.

But rather than allow Internet access from anywhere, it would be better, if you’re able, to limit access to just a few select IP addresses or blocks (like the IP addresses of the offices from which your users will need to access your internal apps). That way, if an attacker steals the private key of one of the WireGuard peers authorized to access your WireGuard server, the attacker will only be able to access the server through a computer physically located at one of your offices.

To add this rule, click the Add button:

Network security group: Inbound security rules

For Source, select Any if you want to allow access to your WireGuard bastion server from anywhere. To limit access to just a few specific addresses (like the public IP addresses used by your office locations), for Source select IP Addresses instead, and enter those IP addresses. Either way, for Source port ranges, enter *.

For Destination, select Any; for Service, select Custom; for Destination port ranges, enter 51280 (or the custom port on which you’ll set your WireGuard server to listen, if not 51820); and for Protocol, select UDP. For Action, select Allow:

Add inbound security rule

Scroll down and set the Priority to 100 (or some other number lower than any of deny-access rules that you may set up); set the Name to something descriptive, like wg-from-anywhere (or wg-from-office if you configured the Source property to use your office locations); and use the Description for additional details. Then click the Add button:

Add inbound security rule part 2

Allow Inbound SSH Access

Next we’ll allow SSH access to the WireGuard server, so that we can administer it. Like for the WireGuard service itself, with this example I’ll allow access to the SSH service from anywhere on the Internet; but just like with the WireGuard service, with SSH if you’re able to limit access to just a few select IP addresses instead of the entire Internet, do that instead.

Click the Add button again to this next inbound rule:

Network security group: Inbound security rules

For Source, select Any if you want to allow SSH access to your WireGuard bastion server from anywhere. To limit access to just a few specific addresses (like the public IP addresses used by your office locations), for Source select IP Addresses instead, and enter those IP addresses. Either way, for Source port ranges, enter *.

For Destination, select Any; for Service, select SSH. For Action, select Allow:

Add inbound security rule

Scroll down and set the Priority to 110 (or some other number lower than any of deny-access rules that you may set up); set the Name to something descriptive, like ssh-from-anywhere (or ssh-from-office if you configured the Source property to use your office locations); and use the Description for any additional details. Then click the Add button:

Add inbound security rule part 2

Deny Everything Else Inbound

Now to deny all other access to our WireGuard bastion (including from other internal VNets), click the Add button again to add one more inbound rule:

Network security group: Inbound security rules

For Source, select Any; for Source port ranges, enter *. For Destination, select Any; for Service, select Custom; for Destination port ranges, enter *; and for Protocol, select Any. For Action, select Deny:

Add inbound security rule

Scroll down and set the Priority to 4000 (or some other high number that will allow room for any future rules below it that you may want to add); set the Name to something descriptive, like deny-all; and set the Description to something appropriate. Then click the Add button:

Add inbound security rule part 2

Attach to WireGuard Subnet

Now click the Subnets link in the leftnav to attach this NSG to the dedicated WireGuard subnet we added in the Create Hub VNet step above:

Network security group: Inbound security rules

Click the Associate button:

Network security group: Subnets

For Virtual network, select the demo-hub VNet we created in the Create Hub VNet step, and for Subnet, select the wg-bastion subnet we created at the same time. Then click the OK button:

Associate subnet

Create Internal Application NSG

Next, we’ll create an NSG (Network Security Group) to control network access to the example internal web app that we’ll launch in the Launch Internal Application VM step. We’ll configure this NSG to allow inbound access from our WireGuard subnet (10.10.0.0/24 in this example); and we’ll attach it to the subnet we created for our demo internal app.

If you already some internal apps set up, you don’t need to create a new NSG for them just to use WireGuard — you can simply update the existing NSGs you already have for those apps to include similar rules as shown below to allow inbound access from the WireGuard subnet.

Start by navigating to the Network security groups service. Click the Create button:

Network security groups

Select the Subscription and Resource group of the spoke VNet, then enter a Name for the NSG. I’ll name the NSG demo-spoke-internal-apps for this example. Then click the Review + create button:

Create a network security group: Basics

Then click the Create button:

Create a network security group: Review

Once deployment has completed, click the Go to resource button:

Network security group: Deployment

Then click the Inbound security rules link in the leftnav:

Network security group: Overview

Allow Inbound HTTP Access Through WireGuard

This is where we’ll grant access to connect to our internal apps through the WireGuard bastion. In this example, we’ll grant access to connect to TCP port 80 and 443 of our demo internal app — but you can customize this rule (and add others like it) for other ports as necessary.

Click the Add button:

Network security group: Inbound security rules

For Source, select IP Addresses; for Source IP addresses/CIDR ranges, enter the CIDR of the WireGuard subnet from your hub VNet — in our example, it’s 10.10.0.0/24 (which we set up in the Create Hub VNet step). For Source port ranges, enter *.

For Destination, select Any; for Service, select Custom; for Destination port ranges, enter 80,443; and for Protocol, select TCP. If you need access to different ports for your internal app (like you have a web app that listens on port 8080 instead of 80 and 443), specify those ports instead. (If you need a mix of TCP and UDP ports, or also want to allow ICMP messages, create another rule similar to this for the UDP ports or for ICMP.) For Action, select Allow:

Add inbound security rule

Scroll down and set the Priority to 100 (or some other number below any deny rules that you may create); set the Name to something descriptive, like http-from-wg-bastion; and use the Description for additional details. Then click the Add button:

Add inbound security rule part 2

Allow Inbound SSH Access Through WireGuard

We’ll also allow SSH access from our WireGuard bastion, so that we can use our WireGuard connection to administer our example app VM. You can omit this rule if you don’t need to SSH through your WireGuard connection to administer your internal apps (like because you can SSH into them through a different route, or because they’re baked into a VM or container or other service and you don’t need to SSH into them at all).

Click the Add button again to add another inbound rule:

Network security group: Inbound security rules

For Source, select IP Addresses; for Source IP addresses/CIDR ranges, enter the CIDR of the WireGuard subnet from your hub VNet — in our example, it’s 10.10.0.0/24. For Source port ranges, enter *.

For Destination, select Any; and for Service, select SSH. For Action, select Allow:

Add inbound security rule

Scroll down and set the Priority to 110 (or some other number below any deny rules that you may create); set the Name to something descriptive, like ssh-from-wg-bastion; and use the Description for additional details. Then click the Add button:

Add inbound security rule part 2

Deny Everything Else Inbound

Now we’ll make sure that nothing else can access our internal apps (including by accessing them through other internal VNets) with one more inbound rule. Click the Add button again:

Network security group: Inbound security rules

For Source, select Any; for Source port ranges, enter *. For Destination, select Any; for Service, select Custom; for Destination port ranges, enter *; and for Protocol, select Any. For Action, select Deny:

Add inbound security rule

Scroll down and set the Priority to 4000 (or some other high number that will allow room for any future rules below it that you may want to add); set the Name to something descriptive, like deny-all; and set the Description to something appropriate. Then click the Add button:

Add inbound security rule part 2

Attach to Internal Applications Subnet

Now click the Subnets link in the leftnav to attach this NSG to the internal applications subnet we added in the Create Spoke VNet step above:

Network security group: Inbound security rules

Click the Associate button:

Network security group: Subnets

For Virtual network, select the demo-spoke VNet we created in the Create Spoke VNet step, and for Subnet, select the internal-apps subnet we created at the same time. Then click the OK button:

Associate subnet

Create WireGuard Public IP

Now we’ll create a public IP address for the bastion WireGuard server that we’ll launch in the next step. When we launch that server, we’ll assign it this IP address.

Navigate to the Public IP addresses service (type public ip addresses into the Azure search bar and click the “Public IP addresses” result under “Services”). Then click the Create button:

Public IP addresses

For IP Version, select IPv4; for SKU, select Standard; and for Tier, select Regional. Enter a descriptive Name for the IP address, like wg-bastion-1:

Create public IP address

Then scroll down and select the Subscription and Resource group of the hub VNet; and select No Zone for the Availability zone option. Then click the Create button:

Create public IP address part 2

Launch WireGuard Server VM

Now we’re finally ready to launch or WireGuard server VM (Virtual Machine).

Navigate to the Virtual machines service (type virtual machines into the Azure search bar and click the “Virtual Machines” result under “Services”). Click the Add button, and select Virtual machine:

Virtual machines

Select the Subscription and Resource group of the hub VNet, then enter a Virtual machine name. I’ll name the VM wg-bastion-1 for this example. For Availability options select No infrastructure redundancy required; and for Image select Ubuntu Server 20.04 LTS. For Size, click the See all sizes link to select the cheapest size available:

Create a virtual machine: Basics

WireGuard doesn’t require much processing power, so unless you need to support dozens of concurrent users, the smallest Azure VM will work just fine. Sort this list by cheapest cost/month, and click the cheapest option (in this case B1ls). Then click the Select button:

Select a VM size

Now scroll down, and for Authentication type select SSH public key; for Username, enter azureuser; for SSH public key source, select Use existing key stored in Azure; and for Stored Keys, select a key that you can use to SSH into the VM (I selected the justin-demo key I created in the Register SSH Key section above).

For Public inbound ports, select None. Then click the Next: Disks button:

Create a virtual machine: Basics part 2

For OS disk type, select Standard HDD (you won’t need any high-performance disk settings for WireGuard unless you’re planning on doing something like logging all packets to disk). Then click the Next: Networking button:

Create a virtual machine: Disks

For Virtual network, select the demo-hub network we created in the Create Hub VNet section above; and for Subnet, select the wg-bastion subnet we created at the same time. For Public IP, select the wg-bastion-1 IP we created in the Create WireGuard Public IP section above.

For NIC network security group, select None (since we’re launching this VM into the wg-bastion subnet, the demo-hub-wg-bastion NSG we set up in the Create WireGuard Server NSG section above will apply to it).

If you want to, you can proceed to the Management, Advanced, and Tags sections of the VM launch process — but these are all the settings we need for our example server. So you can go ahead and click the Review + create button now:

Create a virtual machine: Networking

Scroll down to review all your settings, then click the Create button:

Create a virtual machine: Review + create

When the VM deployment finishes, click the Go to Deployment button:

Create a virtual machine: deployment

Navigate to the main page for the VM, and click on its Networking link in the leftnav:

Virtual machine

Now click the link to its Network interface (named wg-bastion-1190 in this example):

Virtual machine: Networking

Now click on the network interface’s IP configurations link in the leftnav:

Network interface

Select Enabled for the network interface’s IP forwarding option; then click the Save button:

Network interface: IP configurations

Configure WireGuard Server

At this point, we’ve just launched a generic Azure VM instance running Ubuntu 20.04. Copy the VM’s Public IP address (13.66.244.73 in this example) from the Azure Portal, and open up a terminal on your local workstation.

SSH into the VM from your local workstation (add the -i flag to the ssh command if you need to specify the path to the SSH key pair with which you launched the VM):

justin@jws:~$ ssh azureuser@13.66.244.73
The authenticity of host '13.66.244.73 (13.66.244.73)' can't be established.
ED25519 key fingerprint is SHA256:iAcUyXn0OInuoYHk/dQNBTQsZOhC7Nz5a3XVFPiFCIE.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '13.66.244.73' (ED25519) to the list of known hosts.
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-1047-azure x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon May 31 21:07:39 UTC 2021

  System load:  0.0               Processes:             119
  Usage of /:   4.4% of 28.90GB   Users logged in:       0
  Memory usage: 53%               IPv4 address for eth0: 10.10.0.4
  Swap usage:   0%

1 update can be applied immediately.
To see these additional updates run: apt list --upgradable


The list of available updates is more than a week old.
To check for new updates run: sudo apt update


The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

azureuser@wg-bastion-1:~$

Now install WireGuard on the server. Since the VM is running Ubuntu 20.04, execute the following commands on it:

$ sudo apt update
$ sudo apt install wireguard

Next, generate a WireGuard key on the VM:

$ wg genkey > server.key
$ wg pubkey < server.key > server.pub
$ cat server.key
ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
$ cat server.pub
fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=

And also generate a WireGuard key on your local workstation (if you haven’t yet installed WireGuard on your local workstation, do so now):

justin@jws:~$ wg genkey > my-workstation.key
justin@jws:~$ wg pubkey < my-workstation.key > my-workstation.pub
justin@jws:~$ cat my-workstation.key
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
justin@jws:~$ cat my-workstation.pub
/TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=

SCP the public key from your local workstation to the VM, and the VM’s public key to your workstation:

justin@jws:~$ scp my-workstation.pub azureuser@13.66.244.73:.
justin@jws:~$ scp azureuser@13.66.244.73:server.pub .

Create a new file on the server at /etc/wireguard/wg0.conf, and put the following content in it:

# /etc/wireguard/wg0.conf

# local settings for WireGuard Server
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/32
ListenPort = 51820

# IP forwarding
PreUp = sysctl -w net.ipv4.ip_forward=1
# IP masquerading
PreUp = iptables -t mangle -A PREROUTING -i wg0 -j MARK --set-mark 0x30
PreUp = iptables -t nat -A POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE
PostDown = iptables -t mangle -D PREROUTING -i wg0 -j MARK --set-mark 0x30
PostDown = iptables -t nat -D POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE

# firewall local host from wg peers
PreUp = iptables -A INPUT -i wg0 -m state --state ESTABLISHED,RELATED -j ACCEPT
PreUp = iptables -A INPUT -i wg0 -j REJECT
PostDown = iptables -D INPUT -i wg0 -m state --state ESTABLISHED,RELATED -j ACCEPT
PostDown = iptables -D INPUT -i wg0 -j REJECT
# firewall wg peers from other hosts
PreUp = iptables -A FORWARD -o wg0 -m state --state ESTABLISHED,RELATED -j ACCEPT
PreUp = iptables -A FORWARD -o wg0 -j REJECT
PostDown = iptables -D FORWARD -o wg0 -m state --state ESTABLISHED,RELATED -j ACCEPT
PostDown = iptables -D FORWARD -o wg0 -j REJECT

# remote settings for Justin's Workstation
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
AllowedIPs = 10.0.0.1/32

Replace the PrivateKey value with the private key you generated on the server (server.key), and the PublicKey value with the public key you generated on your local workstation (my-workstation.pub).

For a detailed explanation of all the settings in this file, see the Point to Site Configuration article (“Host β” in that article is the equivalent of the WireGuard bastion server in this article). The firewall commands in this file will prevent clients from using the WireGuard tunnel to connect directly to other services on the server, or to other WireGuard clients that you add later (omit these firewall commands if you do want to allow such access). Since we’re using Azure NSGs to control access from the WireGuard server to other services in our cloud, we don’t need to add any additional firewall settings on the server itself.

Bring up the wg0 WireGuard interface we just configured, by running the following commands on the server:

$ sudo systemctl enable wg-quick@wg0.service
$ sudo systemctl start wg-quick@wg0.service

Now with the interface up, when you run sudo wg on the server, you should see the following output:

$ sudo wg
interface: wg0
  public key: fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
  private key: (hidden)
  listening port: 51820

peer: /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
  allowed ips: 10.0.0.1/32

Configure WireGuard Client

Configure your local workstation to connect to the server over WireGuard. Create a new file on your local workstation at /etc/wireguard/wg-bastion-1.conf, and put the following content in it:

# /etc/wireguard/wg-bastion-1.conf

# local settings for Justin's Workstation
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32

# remote settings for WireGuard Server
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
Endpoint = 13.66.244.73:51820
AllowedIPs = 10.20.0.0/16

Replace the PrivateKey value with the private key you generated on your local workstation (my-workstation.key), and the PublicKey value with the public key you generated on the server (server.pub). Also replace the Endpoint value with the actual public IP address of the VM (the IP address we created with the Create WireGuard Public IP step); and replace the AllowedIPs value with the actual IPv4 address space of your spoke VNets (separate multiple address blocks with commas, or add a separate AllowedIPs line for each block).

Start up the wg-bastion-1 WireGuard interface we just configured on your local workstation:

justin@jws:~$ sudo systemctl enable wg-quick@wg-bastion-1.service
justin@jws:~$ sudo systemctl start wg-quick@wg-bastion-1.service

Now if you run sudo wg on your local workstation, it should include the following output:

justin@jws:~$ sudo wg
interface: my-vpc-01
  public key: /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
  private key: (hidden)
  listening port: 56789

peer: fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
  endpoint: 13.66.244.73:51820
  allowed ips: 10.20.0.0/16

Launch Internal Application VM

If you already have some internal web apps running in your spoke VNets, you can skip this step. Otherwise, we’ll set up a simple app for to test out our WireGuard connection.

Return to the Virtual machines service in the Azure Portal; click the Add button, and select Virtual machine:

Virtual machines

Select the Subscription and Resource group of the spoke VNet, then enter a Virtual machine name. I’ll name the VM demo-internal-app for this example. For Availability options select No infrastructure redundancy required; and for Image select Ubuntu Server 20.04 LTS. For Size, select the cheapest possible instance (B1ls):

Create a virtual machine: Basics

Scroll down and for Authentication type select SSH public key; for Username, enter azureuser; for SSH public key source, select Use existing key stored in Azure; and for Stored Keys, select a key that you can use to SSH into the VM (I selected the justin-demo key I created in the Register SSH Key section above).

For Public inbound ports, select None. Then click the Next: Disks button:

Create a virtual machine: Basics part 2

For OS disk type, select Standard HDD. Then click the Next: Networking button:

Create a virtual machine: Disks

For Virtual network, select the demo-spoke network we created in the Create Spoke VNet section above; and for Subnet, select the internal-apps subnet we created at the same time. For Public IP, select None; and for NIC network security group, select None.

Then can click the Review + create button:

Create a virtual machine: Networking

Scroll down to review all you settings, then click the Create button:

Create a virtual machine: Review + create

When the VM deployment finishes, click the Go to Deployment button:

Create a virtual machine: deployment

On the main page for the VM, find the Private IP address of the VM, and copy it:

Virtual machine

Connect Through WireGuard

With the private IP address of the internal application VM we launched in the previous step (10.20.0.4), open up a terminal on your local workstation, and SSH into the VM (add the -i flag to the ssh command if you need to specify the path to the SSH key pair with which you launched the VM):

justin@jws:~$ ssh azureuser@10.20.0.4
The authenticity of host '10.20.0.4 (10.20.0.4)' can't be established.
ED25519 key fingerprint is SHA256:8NlhzD1R6MjP8k2M4fXJSA2jLEKtwjWbn6ON5nPhGbk.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.20.0.4' (ED25519) to the list of known hosts.
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-1047-azure x86_64)
...
azureuser@demo-internal-app:~$

This SSH session is routed through the WireGuard connection between my local workstation and the WireGuard server we set up in the Configure WireGuard Client section, and from that WireGuard server to the internal application VM we set up in the Launch Internal Application VM section. This routing happens because:

  1. In the wg-bastion-1 WireGuard interface I set up for my local workstation in the Configure WireGuard Client section, I set the AllowedIPs value for the WireGuard server to 10.20.0.0/16 (a block which contains the internal application’s IP address, 10.20.0.4).

  2. I started up that interface with wg-quick (via the sudo systemctl start wg-quick@wg-bastion-1.service command), which automatically sets up routes on my local workstation to match the AllowedIPs value for each peer of the interface.

  3. In the wg0 configuration file I set up for the WireGuard server in the Configure WireGuard Server section, I used several PreUp commands (executed by wg-quick) to configure the Linux networking subsystem on the server to allow network traffic to be forwarded, and to apply masquerading (aka NAT) to the WireGuard traffic forwarded to the spoke VNet.

Now that we’re SSH’d into the internal application server, run the following commands on it to start a demo web app (which serves the directory listings of a dummy directory via a Python webserver running on port 80):

$ mkdir -p dummydir && cd dummydir
$ sudo python3 -m http.server 80

With the demo web app booted up, you can now access the internal application from your local workstation. Run this command (using the private IP address of the internal application’s VM):

$ curl 10.20.0.4

If you see any HTML output from this, the WireGuard tunnel through the WireGuard bastion server to the internal application works! Just like the SSH session above, this HTTP request will be routed through the WireGuard tunnel between your local workstation and the WireGuard server, and from the WireGuard server through the spoke VNet to the internal application.

Troubleshoot WireGuard Tunnel

If you try to connect to the internal application like above, but the connection hangs or you get an error like Connection refused or No route to host, first run sudo wg on your local workstation:

justin@jws:~$ sudo wg
interface: wg-bastion-1
  public key: /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
  private key: (hidden)
  listening port: 56789

peer: fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
  endpoint: 13.66.244.73:51820
  allowed ips: 10.20.0.0/16
  latest handshake: 57 seconds ago
  transfer: 312.84 KiB received, 294.07 KiB sent

You should see the interface to wg-bastion-1 listed (if not, follow the instructions for Configure WireGuard Client above). You should also see some data both sent and received in the transfer field for the WireGuard bastion server peer.

If you don’t see a transfer field at all listed for the peer, you likely have a routing problem on your local workstation. First make sure that the IP of the internal app server to which you’re trying to connect (10.20.0.4 in this example) is within the listed IP blocks in the allowed ips field. If it isn’t, correct the AllowedIPs setting for the bastion server peer in your wg-bastion-1.conf file to include the IPv4 address space to which the internal app belongs; then restart the interface (via the sudo systemctl restart wg-quick@wg-bastion-1.service command).

If the allowed ips field is good, try using the ip route command to check the route to the internal application from your workstation. You should see something similar to this:

justin@jws:~$ ip route get 10.20.0.4
10.20.0.4 dev wg-bastion-1 src 10.0.0.1 uid 1000
    cache

If the result of the command indicates a device other than wg-bastion-1 will be used, you need to fiddle with the routing tables on your workstation.

Otherwise, if the transfer field for the peer lists some data sent, but none received, run sudo wg on the WireGuard server:

$ sudo wg
interface: wg0
  public key: fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
  private key: (hidden)
  listening port: 51820

peer: /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
  endpoint: 198.51.100.1:45678
  allowed ips: 10.0.0.1/32
  latest handshake: 1 minute, 53 seconds ago
  transfer: 302.04 KiB received, 364.87 KiB sent

You should see the interface to wg0 listed (if not, go through the instructions for Configure WireGuard Server above). You should also see some data both sent and received in the transfer field for your workstation peer.

If you don’t see a transfer field listed at all, you’ve either incorrectly configured the PrivateKey, PublicKey, or Endpoint settings on the client (review the instructions for Configure WireGuard Client above); incorrectly configured the PrivateKey, PublicKey, or ListenPort settings on the server (review the instructions for Configure WireGuard Server above); or there’s some NSG or other VNet setting that’s blocking traffic on UDP port 51820 from being sent from your local workstation to the WireGuard server (see the section below to troubleshoot).

If the transfer field shows data both sent and received, then the WireGuard tunnel itself is working correctly — but either the WireGuard server is not routing traffic internally to other VNets, or some Azure networking setting is blocking traffic between the WireGuard server and your internal application servers. Follow the Basic Troubleshooting steps from the Point to Site Configuration article to check if it’s the former (“Host β” in that article is the equivalent of the WireGuard bastion server in this article). Otherwise, see the section below.

Troubleshoot Azure Networking

Here are three areas to check in the Azure portal if it looks like your WireGuard traffic is being blocked by some Azure networking gear:

IP Forwarding

The network interface used by your WireGuard server bastion must have its IP Forwarding setting Enabled. This setting is Disabled by default to prevent the VM from forwarding the packets it receives from one host onto another host.

To view this setting for a network interface, navigate to its main page, and click the IP configurations link it its leftnav:

Network interface: IP configurations

Network Security Groups

The Inbound security rules for the NSG (Network Security Group) used by your WireGuard server bastion (demo-hub-wg-bastion in this example) must at least include access to UDP port 51820 (or whatever value you’ve configured the ListenPort setting to be on the WireGuard server) from your local workstation’s public IP address. For this example, I’ve configured the NSG to allow both UDP port 51820 and TCP port 22 from everywhere (via the wg-from-anywhere and ssh-from-anywhere rules):

Network security group: Inbound security rules

(The ssh-from-anywhere rule allows me to SSH directly into the WireGuard server; if you don’t need this, you can omit it.)

The Outbound security rules for the bastion NSG must at least include access to your internal application server on whatever port(s) the application uses (in this example, the application uses TCP port 80; and I also used TCP port 22 to SSH into the application server from the WireGuard server). Usually there’s no reason to customize Azure’s default outbound security rules, which allow outbound traffic to all other VNets (via the AllowVNetOutBound rule):

Network security group: Outbound security rules

The Inbound security rules for the NSG used by your internal application (demo-spoke-internal-apps in this example) must at least include access to the port used by the application (TCP 80 in this example) from the bastion subnet (10.10.0.0/24). For this example, I’ve configured the NSG to allow TCP ports 80 and 443 from the WireGuard bastion (via the http-from-wg-bastion rule), as well as TCP port 22 (via the ssh-from-wg-bastion rule):

Network security group: Inbound security rules

(The TCP 22 rule allows me to SSH into the application server through the WireGuard server; if you don’t need this, you can omit it.)

The Outbound security rules for the application NSG don’t matter (from a WireGuard perspective). For this example, I’ve kept the default rules, which allow all traffic to all destinations, VNet and Internet — but you can remove or override them without disrupting your access to the application through WireGuard (although note, depending on your application, if you remove these default rules you might need to replace them with other rules that allow your application to access the other network services it may need, like databases, fileshares, external APIs, etc):

Network security group: Outbound security rules

VNet Peerings

The hub-to-spoke peering (configured on the hub VNet) needs its Traffic to remote virtual network set to Allow. You should also set its Traffic forwarded from remote virtual network setting to Block:

Virtual network: Peering

The spoke-to-hub peering (configured on the spoke VNet) needs its Traffic to remote virtual network set to Allow — and it also needs its Traffic forwarded from remote virtual network setting to Allow:

Virtual network: Peering

This is required to allow the WireGuard server in the hub VNet to forward traffic into the spoke VNet.