How to Set Up a WireGuard Jumphost
Setting up a WireGuard jumphost is easy! It’s a great way to securely access services on a private internal network from a remote location (and often a better choice than using an SSH jumphost).
This article will show you how, using the following example scenario, where we will provide Alice, Bob, and Cindy remote access to select services in a private cloud site through a WireGuard jumphost:
The three services we’ll grant access to, a back-end server which needs to be administered via SSH, a MySQL database, and an internal web-app, are accessible only from within the cloud site, via private IP addresses (using the private 192.168.200.0/24
address space). We’ll set up a jumphost with a public IP address (198.51.100.10
in this example), and configure it so that Alice, Bob, and Cindy can connect to it through an encrypted WireGuard tunnel.
We’ll follow these steps:
Inventory the Systems
First, we need to determine which client systems need access to this jumphost, designate WireGuard IP addresses for them, and identify which services in the site each client should be able to access.
If you don’t have a fancy inventory management system for this, you can just draw up a simple spreadsheet like the following to keep track of each system, and the access granted to each:
WireGuard Client | WireGuard IP Address | Site Service | Site IP Address | Port | |
---|---|---|---|---|---|
Alice’s Workstation |
? |
Back-end server admin |
192.168.200.21 |
TCP 22 |
|
Alice’s Workstation |
? |
MySQL database |
192.168.200.22 |
TCP 3360 |
|
Alice’s Workstation |
? |
Internal web-app |
192.168.200.23 |
TCP 80 |
|
Bob’s Workstation |
? |
MySQL database |
192.168.200.22 |
TCP 3360 |
|
Bob’s Workstation |
? |
Internal web-app |
192.168.200.23 |
TCP 80 |
|
Cindy’s Laptop |
? |
Internal web-app |
192.168.200.23 |
TCP 80 |
Add an entry for each connection between client and internal service you want to enable. Once you’ve completed that, come up with an available private-use IP address range to use for the WireGuard network, and fill in a WireGuard IP address for each client.
Also include entries in your table for any remote access you need to the jumphost itself for administration. In this example, we’ll grant Alice access to administer the jumphost over SSH from her workstation. We’ll use the 10.0.0.0/24
address space for our WireGuard network, and we’ll use 10.0.0.1
as the WireGuard IP address for the jumphost itself:
WireGuard Client | WireGuard IP Address | Site Service | Site IP Address | Port | |
---|---|---|---|---|---|
Alice’s Workstation |
10.0.0.11 |
Jumphost admin |
10.0.0.1 |
TCP 22 |
|
Alice’s Workstation |
10.0.0.11 |
Back-end server admin |
192.168.200.21 |
TCP 22 |
|
Alice’s Workstation |
10.0.0.11 |
MySQL database |
192.168.200.22 |
TCP 3360 |
|
Alice’s Workstation |
10.0.0.11 |
Internal web-app |
192.168.200.23 |
TCP 80 |
|
Bob’s Workstation |
10.0.0.12 |
MySQL database |
192.168.200.22 |
TCP 3360 |
|
Bob’s Workstation |
10.0.0.12 |
Internal web-app |
192.168.200.23 |
TCP 80 |
|
Cindy’s Laptop |
10.0.0.13 |
Internal web-app |
192.168.200.23 |
TCP 80 |
Configure WireGuard on the Jumphost
Now provision the jumphost server with a public IP address (in this example we’ll use 198.51.100.10
), connect to it, and install WireGuard on it.
Then run the following commands on the jumphost to generate a new WireGuard key pair for it:
$ wg genkey > jumphost.key
$ wg pubkey < jumphost.key > jumphost.pub
$ cat jumphost.key
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
$ cat jumphost.pub
/TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
Next, create a WireGuard configuration file at /etc/wireguard/wg0.conf
on the jumphost, and copy the private key from the jumphost.key
file (generated with the wg genkey
command) into it:
# jumphost /etc/wireguard/wg0.conf # local settings for the jumphost [Interface] PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE= Address = 10.0.0.1/24 ListenPort = 51820 PreUp = sysctl -w net.ipv4.conf.all.forwarding=1
Configure the rest of the fields like this:
- [Interface] Address
-
Set this to the WireGuard IP address you designated for the jumphost in the Inventory the Systems section (
10.0.0.1
in this example), and include the netmask (/24
) of the entire address range you designated for WireGuard IP addresses (10.0.0.0/24
). - [Interface] ListenPort
-
Set this to the UDP port that WireGuard clients will connect to (we’ll use
51820
in this example). This port must be exposed to the public Internet (more on that in the Configure the Firewall on the Jumphost section). - [Interface] PreUp
-
PreUp
,PostUp
,PreDown
, andPostDown
fields specify arbitrary commands that should be run when the interface is started up and shut down. We’ll use aPreUp
command to ensure that the kernel parameter which allows IPv4 packet forwarding (net.ipv4.conf.all.forwarding
) has been turned on.
We’ll add peers via [Peer]
entries to this WireGuard config later. You can now delete the jumphost.key
file generated above; the only place the private key should live is in the WireGuard configuration of the jumphost itself.
Start up this interface by running the following command on the jumphost:
$ sudo wg-quick up wg0
[#] sysctl -w net.ipv4.conf.all.forwarding=1
net.ipv4.conf.all.forwarding=1
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.0.0.1/24 dev wg0
[#] ip link set mtu 1420 up dev wg0
Tip
|
If you’re using systemd on the jumphost, run the following commands to have it start up this WireGuard interface, and to have it start up the interface automatically on system boot:
|
You can check that WireGuard is running via the wg
command:
$ sudo wg
interface: wg0
public key: /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
private key: (hidden)
listening port: 51820
Configure the Firewall on the Jumphost
Now we’ll configure the firewall on the jumphost to handle three things:
-
Limit access to the private site’s network from our WireGuard clients
-
Lock down access to the jumphost itself
-
Masquerade connections from the WireGuard clients
Since the latest versions of most Linux distros now come with nftables, we’ll use it for our firewall (but you could also use other firewall tools — check out our “point-to-site” guides for firewalld, UFW, and iptables for similar functionality).
On the jumphost, make sure the nftables
package is installed, and update (or create) the nftables ruleset at /etc/nftables.conf
(or /etc/sysconfig/nftables.conf
on Fedora-based distros) to the following:
#!/usr/sbin/nft -f flush ruleset define lan_iface = "eth0" define wg_iface = "wg0" define wg_port = 51820 define wg_jumphost = 10.0.0.1 define alices_workstation = 10.0.0.11 define bobs_workstation = 10.0.0.12 define cindys_laptop = 10.0.0.13 define backend_server = 192.168.200.21 define mysql_database = 192.168.200.22 define internal_web_app = 192.168.200.23 table inet filter { chain inventory-access-policies { # log access attempts log level debug prefix "inventory-access-policies: " # enforce access policies ip saddr . ip daddr . tcp dport { $alices_workstation . $wg_jumphost . 22, $alices_workstation . $backend_server . 22, $alices_workstation . $mysql_database . 3360, $alices_workstation . $internal_web_app . 80, $bobs_workstation . $mysql_database . 3360, $bobs_workstation . $internal_web_app . 80, $cindys_laptop . $internal_web_app . 80, } accept reject with icmpx type admin-prohibited } chain input { type filter hook input priority 0; policy drop; # accept all loopback packets iif "lo" accept # accept all icmp/icmpv6 packets meta l4proto { icmp, ipv6-icmp } accept # accept all packets that are part of an already-established connection ct state vmap { invalid : drop, established : accept, related : accept } # drop new connections over rate limit ct state new limit rate over 1/second burst 10 packets drop # accept all WireGuard packets received on a public interface iifname $lan_iface udp dport $wg_port accept # filter packets inbound from WireGuard network through inventory-access-policies chain iifname $wg_iface goto inventory-access-policies # reject with polite "port unreachable" icmp response reject } chain wg-forward { # forward all icmp/icmpv6 packets meta l4proto { icmp, ipv6-icmp } accept # forward all packets that are part of an already-established connection ct state vmap { invalid : drop, established : accept, related : accept } # filter through inventory-access-policies chain goto inventory-access-policies } chain forward { type filter hook forward priority 0; policy drop; # filter packets inbound from WireGuard network through wg-forward chain iifname $wg_iface goto wg-forward # forward packets outbound to WireGuard network that are part of an already-established connection oifname $wg_iface ct state vmap { invalid : drop, established : accept, related : accept } # reject with polite "host unreachable" icmp response reject with icmpx type host-unreachable } } table inet nat { chain postrouting { type nat hook postrouting priority 100; policy accept; # masquerade all packets from WireGuard network to LAN network iifname $wg_iface oifname $lan_iface masquerade } }
The first section of this nftables config file contains a bunch of variable definitions. Replace the lan_iface
variable value with the actual LAN interface name on the jumphost, and the wg_port
variable value with the WireGuard listen port you chose in the Configure WireGuard on the Jumphost section above.
define lan_iface = "eth0" define wg_iface = "wg0" define wg_port = 51820
Also replace all the IP address definitions with definitions for the actual systems in your WireGuard and private site’s networks:
define wg_jumphost = 10.0.0.1 define alices_workstation = 10.0.0.11 define bobs_workstation = 10.0.0.12 define cindys_laptop = 10.0.0.13 define backend_server = 192.168.200.21 define mysql_database = 192.168.200.22 define internal_web_app = 192.168.200.23
Below that, in the inventory-access-policies
chain, replace the access policy rules with rules that match the inventory table you drew up in the Inventory the Systems section above:
chain inventory-access-policies { # log access attempts log level debug prefix "inventory-access-policies: " # enforce access policies ip saddr . ip daddr . tcp dport { $alices_workstation . $wg_jumphost . 22, $alices_workstation . $backend_server . 22, $alices_workstation . $mysql_database . 3360, $alices_workstation . $internal_web_app . 80, $bobs_workstation . $mysql_database . 3360, $bobs_workstation . $internal_web_app . 80, $cindys_laptop . $internal_web_app . 80, } accept reject with icmpx type admin-prohibited }
Notice that each line in the policy rule above (such as $alices_workstation . $wg_jumphost . 22
) corresponds to a line from the Inventory the Systems table: the first value of the first line, $alices_workstation
(10.0.0.11
), corresponds to the “WireGuard IP Address” column; the second value, $wg_jumpost
(10.0.0.1
), corresponds to the “Site IP Address” column; and the third value, 22
, corresponds to the “Port” column.
Tip
|
If you need to allow access to any UDP services, you’ll need a separate rule for them. The following example shows a rule that allows access from Alice’s workstation to the back-end server on UDP port 3389 (remote desktop): ip saddr . ip daddr . udp dport { $alices_workstation . $backend_server . 3389, } accept |
The next chain definition, the main input filter hook, locks down access to the jumphost itself. The only inbound connections it allows to the jumphost (other than ICMP requests, which are used for “ping” and other network signaling) are through WireGuard. It also limits new connections to 1 per second (after an initial burst of 10 connections):
chain input { type filter hook input priority 0; policy drop; # accept all loopback packets iif "lo" accept # accept all icmp/icmpv6 packets meta l4proto { icmp, ipv6-icmp } accept # accept all packets that are part of an already-established connection ct state vmap { invalid : drop, established : accept, related : accept } # drop new connections over rate limit ct state new limit rate over 1/second burst 10 packets drop # accept all WireGuard packets received on a public interface iifname $lan_iface udp dport $wg_port accept # filter packets inbound from WireGuard network through inventory-access-policies chain iifname $wg_iface goto inventory-access-policies # reject with polite "port unreachable" icmp response reject }
The next two chains work similarly to lock down connections that the jumphost will forward, limiting it to forwarding WireGuard connections only:
chain wg-forward { # forward all icmp/icmpv6 packets meta l4proto { icmp, ipv6-icmp } accept # forward all packets that are part of an already-established connection ct state vmap { invalid : drop, established : accept, related : accept } # filter through inventory-access-policies chain goto inventory-access-policies } chain forward { type filter hook forward priority 0; policy drop; # filter packets inbound from WireGuard network through wg-forward chain iifname $wg_iface goto wg-forward # forward packets outbound to WireGuard network that are part of an already-established connection oifname $wg_iface ct state vmap { invalid : drop, established : accept, related : accept } # reject with polite "host unreachable" icmp response reject with icmpx type host-unreachable }
The last section of this nftables config file masquerades the packets it forwards from the WireGuard network to the private site’s network, so that packets from WireGuard clients will appear to the other servers in the site’s network as if they came from the jumphost itself (which allows replies to those packets to be correctly routed back through the jumphost, without additional configuration):
table inet nat { chain postrouting { type nat hook postrouting priority 100; policy accept; # masquerade all packets from WireGuard network to LAN network iifname $wg_iface oifname $lan_iface masquerade } }
You can activate this ruleset by running the following command on the jumphost:
$ sudo nft -f /etc/nftables.conf
Warning
|
If you are currently SSH’d into the jumphost, do not activate this ruleset yet! — wait until you have successfully set up a WireGuard client that can administer the jumphost through SSH. Otherwise, you will drop your SSH connection, and you will not be able to reconnect. |
Tip
|
If you’re using systemd on the jumphost, run the following commands when you are ready to activate the ruleset, as well to apply the ruleset on system boot:
|
Finally, adjust the private site’s network firewall to allow UDP port 51820
on the jumphost to be accessed from anywhere on the public Internet. Also make sure that the jumphost itself has been granted network access to all the services you listed in the Inventory the Systems section above.
Configure WireGuard on a Client
Now on Alice’s workstation, install WireGuard on it. On Windows or macOS, launch the WireGuard application, and add a new empty tunnel — the WireGuard app will automatically generate a new key pair for you.
On Linux, run the following commands to generate a key pair:
$ wg genkey > alice.key
$ wg pubkey < alice.key > alice.pub
$ cat alice.key
ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
$ cat alice.pub
fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
On Windows or macOS, keep the private key generated for you, and edit the tunnel configuration to be the following:
# local settings for Alice's Workstation [Interface] PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA= Address = 10.0.0.11 # remote settings for the jumphost [Peer] PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU= Endpoint = 198.51.100.10:51820 AllowedIPs = 192.168.200.0/24, 10.0.0.1
On Linux, edit the /etc/wireguard/wg0.conf
file to include the same content, pasting in the private key from the contents of the alice.key
file generated via the wg genkey
command.
Configure the rest of the fields like this:
- [Interface] Address
-
Set this to the WireGuard IP address you designated for the client in the Inventory the Systems section.
- [Peer] PublicKey
-
Copy the contents of the
jumphost.pub
file you generated in the Configure WireGuard on the Jumphost section, and paste it in here. - [Peer] Endpoint
-
Set this to the public IP address of the jumphost (
198.51.100.10
in this example), plus the WireGuard listen port of the jumphost (51820
). - [Peer] AllowedIPs
-
Set this to the list of internal network blocks used by the private site (just
192.168.200.0/24
in this example). For clients that may need to administer the jumphost itself, add the jumphost’s own WireGuard IP address (10.0.0.1
). These ranges should cover all the IP addresses listed in the “Site IP Address” column of the table drawn up in the Inventory the Systems section.
Copy the public key (the alice.pub
file, or the public key shown for the interface in the Windows or macOS app) from Alice’s Workstation to the jumphost (and you can now delete the alice.key
file, if you had generated it). On the jumphost, edit the /etc/wireguard/wg0.conf
file to add a [Peer]
entry for Alice’s Workstation:
# jumphost /etc/wireguard/wg0.conf # local settings for the jumphost [Interface] PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE= Address = 10.0.0.1/24 ListenPort = 51820 PreUp = sysctl -w net.ipv4.conf.all.forwarding=1 # remote settings for Alice's Workstation [Peer] PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds= AllowedIPs = 10.0.0.11
Paste in the public key copied from Alice’s workstation, and set the AllowedIPs
field to the WireGuard IP address you designated for her workstation in the Inventory the Systems section. You will probably also want to track the public key of each WireGuard client as another field in your inventory, so you can rebuild the jumphost configuration on demand from it.
On the jumphost, reload the updated WireGuard configuration by running the following command:
$ sudo bash -c 'wg syncconf wg0 <(wg-quick strip wg0)'
Now when you run the wg
command on the jumphost, you should see the newly added peer listed:
$ sudo wg
interface: wg0
public key: /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
private key: (hidden)
listening port: 51820
peer: fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
allowed ips: 10.0.0.11/32
Test It Out
Back on the client, Alice’s workstation, start up the WireGuard tunnel to the jumphost. Under Windows or macOS, select the tunnel in the WireGuard app, and click the “Activate” button. Under Linux, run the following command:
$ sudo wg-quick up wg0
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.0.0.11 dev wg0
[#] ip link set mtu 1420 up dev wg0
[#] ip -4 route add 192.168.200.0/24 dev wg0
[#] ip -4 route add 10.0.0.1 dev wg0
Now try SSH’ing into the jumphost from Alice’s workstation, using the jumphost’s WireGuard IP address (and Alice’s credentials):
$ ssh alice@10.0.0.1
...
alice@jumphost:~$
If that’s successful, and you’ve been using SSH to administer the jumphost, you can now safely activate the nftables firewall we set up in the Configure the Firewall on the Jumphost section:
alice@jumphost:~$ sudo nft -f /etc/nftables.conf
With the firewall up and operational, Alice will be able to access the other services in the private site’s network to which she’s been granted access.
She’ll be able to access the back-end server from her workstation:
$ ssh alice@192.168.200.21 -p 22
...
alice@backend:~$
And she’ll be able to access the MySQL database server from her workstation:
$ mysql -h 192.168.200.22 -P 3306
...
mysql>
And she’ll also be able to access the internal web-app from her workstation:
$ curl 192.168.200.23:80
<!DOCTYPE html>
<html>
...
The firewall logging we added in the Configure the Firewall on the Jumphost section will log every connection attempted from the WireGuard client through the jumphost (both when the connection is allowed and denied). These log messages will be sent to the kernel message buffer on the jumphost. The logging daemon on most Linux systems will automatically capture and store these messages; if you’re using systemd on the jumphost, you can view them using the following command:
$ journalctl --dmesg --grep inventory-access-policies
Nov 09 03:37:20 jumphost kernel: inventory-access-policies: IN=wg0 OUT=eth0 MAC= SRC=10.0.0.11 DST=192.168.200.21 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=50928 DF PROTO=TCP SPT=41612 DPT=22 WINDOW=64860 RES=0x00 SYN URGP=0
Nov 09 03:39:13 jumphost kernel: inventory-access-policies: IN=wg0 OUT=eth0 MAC= SRC=10.0.0.11 DST=192.168.200.22 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=57782 DF PROTO=TCP SPT=59294 DPT=3360 WINDOW=64860 RES=0x00 SYN URGP=0
Nov 09 03:40:01 jumphost kernel: inventory-access-policies: IN=wg0 OUT=eth0 MAC= SRC=10.0.0.11 DST=192.168.200.23 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=34377 DF PROTO=TCP SPT=35284 DPT=80 WINDOW=64860 RES=0x00 SYN URGP=0
You can now repeat the Configure WireGuard on a Client process for the rest of your clients (Bob’s workstation and Cindy’s laptop in this example), and they’ll have similar access to each service to which they’ve been granted access.
Tip
|
If you have an internal DNS server set up at the private site, you can configure your WireGuard clients to use it to resolve the DNS names for your internal services (so users can use friendly DNS names instead of IP addresses to access them). See the WireGuard With AWS Split DNS tutorial for an example of how to do this on AWS with Route53. |