WireGuard Point to Point Configuration

This article will cover how to set up two WireGuard peers in a Point to Point topology. This is the configuration you’d use when you just want to connect a single endpoint running WireGuard to another single endpoint running WireGuard.

In the specific scenario I’ll cover for this article, we’ll have an end-user workstation, which I’ll call “Endpoint A”, on one LAN (Local Area Network); and an HTTP server listening on port 80, which I’ll call “Endpoint B”, on another; and the Internet between. Endpoint A is behind NAT (Network Address Translation) and a firewall that allows only established connections through. Endpoint B is also behind NAT and a firewall, but its NAT + firewall allows port 51822 from the Internet to be forwarded on to Endpoint B. We’ll use this port, 51822, for WireGuard on Endpoint B.

The diagram below illustrates this scenario:

WireGuard point-to-point scenario

The key requirement for a Point to Point topology is that one endpoint must allow public access (or at least access from the other endpoint) to its WireGuard port before the WireGuard tunnel itself is established. In the above diagram, UDP packets from the Internet to the IP address 203.0.113.2 on port 51822 are forwarded to Endpoint B (also on port 51822). This allows Endpoint A to access Endpoint B’s WireGuard port over the Internet, and establish the WireGuard tunnel.

If there were any different firewall rules or other network configuration that blocked this connection, it wouldn’t be possible to establish a Point to Point WireGuard connection between Endpoint A and Endpoint B. You’d instead have to use a different approach (see Primary WireGuard Topologies for some other options).

In this article, we’ll install WireGuard and create a WireGuard tunnel between the two endpoints. Inside the WireGuard VPN (Virtual Private Network) we’ll create, we’ll set Endpoint A to use an IP address of 10.0.0.1, and Endpoint B to an IP address of 10.0.0.2. Once we have WireGuard configured and running on both endpoints, a user using Endpoint A will be able to access the HTTP server listening on port 80 of Endpoint B simply by navigating to http://10.0.0.2/ in her web browser.

But first, take note of the IP addresses shown in the above diagram: In this scenario, Endpoint A’s IP address, from the perspective of the Internet, is 198.51.100.1; but from the perspective of its own LAN (Site A), it’s 192.168.1.11; and from the perspective of the WireGuard VPN that we’ll build, it’s 10.0.0.1. Similarly, Endpoint B’s IP address, from the perspective of the Internet, is 203.0.113.2; but from the perspective of its own LAN (Site B), it’s 192.168.200.22; and from the perspective of the WireGuard VPN we’ll build, it’s 10.0.0.2.

These are the steps we’ll follow:

  1. Install WireGuard
  2. Generate WireGuard Keys
  3. Configure WireGuard on Endpoint A
  4. Configure WireGuard on Endpoint B
  5. Start Up WireGuard
  6. Test Out the Tunnel
  7. Basic Troubleshooting
  8. Extra: Allow Endpoint B to Initiate Access to Endpoint A
  9. Extra: Configure Firewall on Endpoint A
  10. Extra: Configure Firewall on Endpoint B

Install WireGuard

Install WireGuard on both Endpoint A and Endpoint B by following the installation instructions for the appropriate platform on the WireGuard Installation page. You can verify that you’ve installed WireGuard successfully by running wg help on each endpoint.

Generate WireGuard Keys

Next, generate two WireGuard keys, one for Endpoint A, and one for Endpoint B. A WireGuard key (also known as a “key pair”) is composed of two parts, the “private key” (also known as the “secret key”), and the “public key”. The magic of this kind of crypto (known as “public-key cryptography”) is that it’s trivial to compute the public key if you know the private key, but practically impossible to compute the private key if all you know is the public key.

While you don’t have to generate the keys on the endpoints, generating an endpoint’s key on the endpoint itself often is the best way to keep its private key secure — that way the private key never leaves the endpoint. If you generate your keys outside of the endpoint, be very careful with the private keys, as WireGuard’s security depends entirely on keeping the private keys a secret.

Run the following commands to generate a new key pair for Endpoint A:

$ wg genkey > endpoint-a.key
$ wg pubkey < endpoint-a.key > endpoint-a.pub

And similar commands to generate a new key pair for Endpoint B:

$ wg genkey > endpoint-b.key
$ wg pubkey < endpoint-b.key > endpoint-b.pub

This will generate four files: endpoint-a.key, endpoint-a.pub, endpoint-b.key, and endpoint-b.pub. The *.key files contain the private keys, and the *.pub files contain the public keys. The content of each will be 44 characters of Base64-encoded text:

$ cat endpoint-a.key
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
$ cat endpoint-a.pub
/TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=

$ cat endpoint-b.key
ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
$ cat endpoint-b.pub
fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=

You don’t actually need to keep these files around — the content of each will be used in the configuration files we build for Endpoint A and Endpoint B in the next section. The private key for an endpoint goes in the endpoint’s own configuration file; and its public key goes in the configuration file of every other endpoint you want to connect it to. Each end of a connection must be pre-configured with the other end’s public key in order for WireGuard to establish the connection.

Configure WireGuard on Endpoint A

Now let’s configure Endpoint A (the “client” side of the connection). On Endpoint A, create a new file at /etc/wireguard/wg0.conf with the following content:

# /etc/wireguard/wg0.conf

# local settings for Endpoint A
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32
ListenPort = 51821

# remote settings for Endpoint B
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
Endpoint = 203.0.113.2:51822
AllowedIps = 10.0.0.2/32

Set the file’s owner to root, and its permissions to 600 (owner can read+write; everyone else is denied access — the file contains the endpoint’s private key, which must be kept secret).

Let’s go through the configuration, setting-by-setting:

Interface.PrivateKey
This is Endpoint A’s private key — replace this value with the actual private key you generated for Endpoint A. This value is secret, and this is the only place where it should live.
Interface.Address

This is Endpoint A’s IP address inside the WireGuard VPN we’re building — replace this value with a suitable value for your network. You can use any address you want for this, but the address should be within the “Private-Use” IPv4 or “Unique-Local” IPv6 address space (like 10.0.0.0/8 — see RFC 6890 for all available address blocks), and should not collide with any other private subnets to which the endpoint itself or any of its peers can connect.

With a Point to Point topology, this setting in Endpoint A’s configuration file should match exactly the Peer.AllowedIps setting in Endpoint B’s configuration file (discussed in the next section).

This setting isn’t used by WireGuard directly — it’s only used by the wg-quick helper service when it sets up a virtual network interface for WireGuard. In order for network packets to be routed correctly to and from this endpoint when they’re outside of the WireGuard tunnel, they need to use the IP address you set here.

While you can configure multiple IP addresses for this setting, unless you have a special use-case, you should just use a single IP address (in the form of a /32 block with an IPv4 address, or a /64 block with an IPv6 address).

Interface.ListenPort

This is Endpoint A’s WireGuard port. Endpoint A must be able to receive UDP traffic for established connections on this port from the Internet (or wherever the traffic of the other WireGuard peers with which it will communicate comes from).

If you omit this setting, WireGuard will select a new random, unused port in the in the operating system’s ephemeral port range (which may range from 1024 to 65535, depending on operating system) every time it starts up.

Peer.PublicKey
This is Endpoint B’s public key — replace this value with the actual public key you generated for Endpoint B. This value is not secret; however, it is a globally-unique identifier for Endpoint B.
Peer.Endpoint

This is Endpoint B’s publicly-facing IP address (and port) — the IP address and port Endpoint A will use to connect over the Internet to Endpoint B to set up the WireGuard tunnel. Replace this value with the actual IP address that you would normally use to connect to Endpoint B from Endpoint A (and suffix it with the actual WireGuard port you’ll use for Endpoint B, if it’s not WireGuard’s default port of 51820).

Endpoint B must be able to accept new UDP connections from the Internet at this address and port; and Endpoint A must be able to send UDP traffic over the Internet to it at this address and port.

Peer.AllowedIps

This is Endpoint B’s IP address inside the WireGuard VPN we’re building — replace this value with a suitable value for your network. You can use any address you want for this, but the address should be within the “Private-Use” IPv4 or “Unique-Local” IPv6 address space (like 10.0.0.0/8 — see RFC 6890 for all available address blocks), and should not collide with any other private subnets to which the endpoint itself or any of its peers can connect.

With a Point to Point topology, this setting in Endpoint A’s configuration file should match exactly the Interface.Address setting in Endpoint B’s configuration file (discussed in the next section).

While you can configure multiple IP addresses for this setting, unless you have a special use-case, with a Point to Point topology you should just use a single IP address (in the form of a /32 block with an IPv4 address, or a /64 block with an IPv6 address).

Configure WireGuard on Endpoint B

Now let’s configure Endpoint B (the “server” side of the connection). On Endpoint B, create a new file at /etc/wireguard/wg0.conf with the following content:

# /etc/wireguard/wg0.conf

# local settings for Endpoint B
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/32
ListenPort = 51822

# remote settings for Endpoint A
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
AllowedIps = 10.0.0.1/32

Set the file’s owner to root, and its permissions to 600 (owner can read+write; everyone else is denied access — the file contains the endpoint’s private key, which must be kept secret).

Let’s go through the configuration, setting-by-setting:

Interface.PrivateKey
This is Endpoint B’s private key — replace this value with the actual private key you generated for Endpoint B. This value is secret, and this is the only place where it should live.
Interface.Address

This is Endpoint B’s IP address inside the WireGuard VPN we’re building — replace this value with a suitable value for your network. You can use any address you want for this, but the address should be within the “Private-Use” IPv4 or “Unique-Local” IPv6 address space (like 10.0.0.0/8 — see RFC 6890 for all available address blocks), and should not collide with any other private subnets to which the endpoint itself or any of its peers can connect.

With a Point to Point topology, this setting in Endpoint B’s configuration file should match exactly the Peer.AllowedIps setting in Endpoint A’s configuration file (discussed in the section above).

This setting isn’t used by WireGuard directly — it’s only used by the wg-quick helper service when it sets up a virtual network interface for WireGuard. In order for network packets to be routed correctly to and from this endpoint when they’re outside of the WireGuard tunnel, they need to use the IP address you set here.

While you can configure multiple IP addresses for this setting, unless you have a special use-case, you should just use a single IP address (in the form of a /32 block with an IPv4 address, or a /64 block with an IPv6 address).

Interface.ListenPort

This is Endpoint B’s WireGuard port. Endpoint B must be able to receive UDP traffic for new connections on this port from the Internet (or wherever the traffic of the other WireGuard peers with which it will communicate comes from).

With a Point to Point topology, this setting in Endpoint B’s configuration file should be the same as the port in the Peer.Endpoint setting of Endpoint A’s configuration file (discussed in the section above).

Peer.PublicKey
This is Endpoint A’s public key — replace this value with the actual public key you generated for Endpoint A. This value is not secret; however, it is a globally-unique identifier for Endpoint A.
Peer.AllowedIps

This is Endpoint A’s IP address inside the WireGuard VPN we’re building — replace this value with a suitable value for your network. You can use any address you want for this, but the address should be within the “Private-Use” IPv4 or “Unique-Local” IPv6 address space (like 10.0.0.0/8 — see RFC 6890 for all available address blocks), and should not collide with any other private subnets to which the endpoint itself or any of its peers can connect.

With a Point to Point topology, this setting in Endpoint B’s configuration file should match exactly the Interface.Address setting in Endpoint A’s configuration file (discussed in the section above).

While you can configure multiple IP addresses for this setting, unless you have a special use-case, with a Point to Point topology you should just use a single IP address (in the form of a /32 block with an IPv4 address, or a /64 block with an IPv6 address).

You will notice that we omitted the Peer.Endpoint setting from Endpoint B’s configuration file that we used in Endpoint A’s configuration file. In this scenario, because Endpoint A initiates all connections to Endpoint B (which happens when the end user makes HTTP requests from Endpoint A to Endpoint B), Endpoint B doesn’t need to know how to initiate a connection to Endpoint A — and so it doesn’t need a pre-configured value for Peer.Endpoint.

In most cases, you’ll only need to configure one side of a WireGuard tunnel with the Peer.Endpoint setting — you’ll need it on both sides only if a) you want either side to be able to initiate a connection, and b) your firewall rules and other network configuration actually allow either side to initiate a connection. If a) is true but b) is not, see the Extra: Allow Endpoint B to Initiate Access to Endpoint A section at the end of this article.

Start Up WireGuard

On both Endpoint A and Endpoint B, start the wg-quick service. On Linux with systemd, run the following commands:

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

On systems without systemd, run this command instead:

$ sudo wg-quick up /etc/wireguard/wg0.conf

Either way, starting up the wg-quick service will set up a WireGuard network interface named wg0 on the endpoint, and configure some routing rules to route packets destined for any IP address listed in the Peer.AllowedIPs setting(s) of the /etc/wireguard/wg0.conf file to go out the wg0 interface.

Note that the name wg0 is just the standard convention for your first WireGuard interface. You can create as many WireGuard interfaces as you like, and name them however you like. For example, you could create another configuration file named /etc/wireguard/mytunnel.conf, and start it up with the command wg-quick up /etc/wireguard/mytunnel.conf; this would create a new WireGuard interface named mytunnel.

But for now, if you ran wg-quick up directly, you’ll see output like the following:

$ sudo wg-quick up /etc/wireguard/wg0.conf
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.0.0.1/32 dev wg0
[#] ip link set mtu 8921 up dev wg0
[#] ip -4 route add 10.0.0.2/32 dev wg0

The output shows the routing rules that wg-quick set up for you automatically.

If you ran systemctl start instead, you can see the same output by running the journalctl tool, like so:

$ journalctl -u wg-quick@wg0.service
systemd[1]: Starting WireGuard via wg-quick(8) for wg0...
wg-quick[271288]: [#] ip link add wg0 type wireguard
wg-quick[271288]: [#] wg setconf wg0 /dev/fd/63
wg-quick[271288]: [#] ip -4 route add 10.0.0.1/32 dev wg0
wg-quick[271288]: [#] ip link set mtu 8921 up dev wg0
wg-quick[271288]: [#] ip -4 address add 10.0.0.2/32 dev wg0
systemd[1]: Started WireGuard via wg-quick(8) for wg0.

Test Out the Tunnel

On Endpoint B, start up a webserver on port 80 (or any other port, like 8080). If you don’t have a webserver handy, a dead-simple substitute is to use the http.server module of Python 3, like the following, running as root to listen on port 80:

$ sudo python3 -m http.server 80

Or run it as a regular user to listen on any port above 1023 (like port 8080):

$ python3 -m http.server 8080

The http.server module will serve the directory listing of and files in your current directory. A somewhat safer version of this is to create an empty directory in your current directory, and serve that instead:

$ mkdir dummydir && cd dummydir
$ python3 -m http.server 8080

On Endpoint A, use curl (or a regular webrowser) to request a page from the webserver running on Endpoint B port 80:

$ curl 10.0.0.2

Or if the webserver is running on port 8080 (or some other port), specify that port explicitly:

$ curl 10.0.0.2:8080

If you see any HTML output from this, your WireGuard tunnel works!

Basic Troubleshooting

If curl hangs, or you see an error like like Connection refused or No route to host, you likely have some firewall rules or other network configuration either blocking the WireGuard tunnel itself from being set up, or blocking packets on one side of the tunnel or the other. But first, try running sudo wg show all dump on both endpoints, to double-check that the WireGuard interface on both is up and running and configured as you expect.

On Endpoint A, you’ll probably see output like this:

$ sudo wg show all dump
wg0     AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=    /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=    51821   off
wg0     fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=    (none)  203.0.113.2:51822       10.0.0.2/32     0       0       1234       off

For each interface (eg wg0), the first row of the output shows information about the endpoint itself (Endpoint A), and the following rows show information about the endpoint’s peers (like Endpoint B), one peer per line. The second-to-last column for a peer shows the bytes sent to the peer, and the third-to-last column shows the bytes received. If the second-to-last column is non-zero, but the third-to-last column is zero (as it is in the example above), WireGuard is attempting to send packets to the peer, but is getting nothing in return.

On Endpoint B, you’ll probably see output like this:

$ sudo wg show all dump
wg0     ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=    fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=    51822   off
wg0     /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=    (none)  (none)  10.0.0.1/32     0       0       0       off

This output shows no bytes sent or received. If this is the case for you, you need to fiddle with your firewalls or other network configuration until they allow Endpoint A to send UDP packets to Endpoint B via the IP address and port configured in Endpoint A’s Peer.Endpoint setting (in this example, it’s 203.0.113.2:51822).

Extra: Allow Endpoint B to Initiate Access to Endpoint A

In our example scenario, Endpoint A initiates all connections to Endpoint B (which happens when the end user makes HTTP requests from Endpoint A to Endpoint B). This works well with the scenario’s site-level firewall rules (shown in the diagram at the top of this article), where port 51822 on Endpoint B is open to all new connections, but port 58121 (and all other ports) on Endpoint A are open only to already-established connections.

If, however, we wanted the relationship between Endpoint A and Endpoint B to be a “two-way street”, where either end could initiate a connection (like if sometimes we need to open up an SSH connection from Endpoint A to Endpoint B, and sometimes we need to open an SSH connection from Endpoint B to Endpoint A), we’d need to make one configuration change:

On Endpoint A, edit the /etc/wireguard/wg0.conf file to add a Peer.PersistentKeepalive setting, like the following:

# /etc/wireguard/wg0.conf

# local settings for Endpoint A
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32
ListenPort = 51821

# remote settings for Endpoint B
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
Endpoint = 203.0.113.2:51822
AllowedIps = 10.0.0.2/32
PersistentKeepalive = 25

Then restart the wg-quick service on Endpoint A with systemctl — or just bring the wg0 interface down and up with the wg-quick command directly:

$ sudo wg-quick down /etc/wireguard/wg0.conf
$ sudo wg-quick up /etc/wireguard/wg0.conf

This updated configuration will cause Endpoint A to send a keepalive packet to Endpoint B every 25 seconds. This proactively opens a connection to Endpoint B, and keeps it established, so that Endpoint B is free to send packets through the firewall to Endpoint A at its own discretion.

Extra: Configure Firewall on Endpoint A

If you already have some firewall software set up on Endpoint A itself, you don’t need to do anything else — in most cases the firewall rules you have set up to apply to non-local traffic will also apply to your WireGuard traffic. Or if you have some preferred software for managing local firewalls on your endpoints, use that software instead of trying to manage extra rules in your WireGuard configuration.

Otherwise, for a Linux endpoint, you may want to add a few simple iptables rules to reject unexpected traffic coming out of your WireGuard tunnel. This will prevent a malicious actor who’s gotten a foothold on Endpoint B from having unrestricted network access to Endpoint A.

On Endpoint A, first double-check what iptables rules you have in place with this command:

$ sudo iptables -L

If you get an error like command not found, you need to install iptables. If you have iptables installed, but don’t have any firewall rules set up, you’ll see output like the following:

$ sudo iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

If you instead you see several screen-fulls of rules listed, you’re probably using some other tool to manage the firewall on Endpoint A, and should use that tool instead of following the next steps.

As the next step, bring down the WireGuard interface with wg-quick or systemctl:

$ sudo wg-quick down /etc/wireguard/wg0.conf
$ # or alternately:
$ sudo systemctl stop wg-quick@wg0.service

Then edit the /etc/wireguard/wg0.conf file to add several Interface.PreUp and Interface.PostDown settings, like the following:

# /etc/wireguard/wg0.conf

# local settings for Endpoint A
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32
ListenPort = 51821

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

# remote settings for Endpoint B
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
Endpoint = 203.0.113.2:51822
AllowedIps = 10.0.0.2/32

Then bring the interface back up again with wg-quick or systemctl:

$ sudo wg-quick up /etc/wireguard/wg0.conf
$ # or alternately:
$ sudo systemctl start wg-quick@wg0.service

This updated configuration will cause wg-quick on Endpoint A to add two new iptables rules before it brings up the WireGuard interface, and remove the same rules after it brings the interface down. The first rule will accept all packets for already-established connections (ie connections that Endpoint A initiated) coming through the WireGuard tunnel; and the second rule will reject anything else coming into Endpoint A through the tunnel.

Note that if you use an IPv6 address for your WireGuard interface, substitute the ip6tables command for the iptables command in everything above.

Also note that when you’re fiddling with PreUp and PostDown settings, it’s best to bring the interface down first, make your configuration changes, and then bring the interface back up again (rather than making the configuration changes while the interface is still up, and then trying to restart afterward). The reason is that most of the time, your “Up” and “Down” settings are meant to be symmetrical (like start up a service with PreUp and shut it down with PostDown, or add a firewall rule with PreUp and remove it with PostDown) — so if you change a PostDown rule while your WireGuard interface is still running, you might inadvertently leave a stray service running or firewall rule in place from the previous configuration version (which you’ll have to track down and clean up manually).

Extra: Configure Firewall on Endpoint B

Similarly, if you already have some firewall software set up on Endpoint B, you don’t need to do anything else — in most cases the firewall rules you have set up to apply to non-local traffic will also apply to your WireGuard traffic. Or if you have some preferred software for managing local firewalls on your endpoints, use that software instead of trying to manage extra rules in your WireGuard configuration.

Otherwise, for a Linux endpoint, you may want to add a few simple iptables rules to reject unexpected traffic coming out of your WireGuard tunnel. This will prevent a malicious actor who’s gotten a foothold on Endpoint A from having unrestricted network access to Endpoint B.

On Endpoint B, first double-check what iptables rules you have in place with this command:

$ sudo iptables -L

If you get an error like command not found, you need to install iptables. If you have iptables installed, but don’t have any firewall rules set up, you’ll see output like the following:

$ sudo iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

If you instead you see several screen-fulls of rules listed, you’re probably using some other tool to manage the firewall on Endpoint B, and should use that tool instead of following the next steps.

As the next step, bring down the WireGuard interface with wg-quick or systemctl:

$ sudo wg-quick down /etc/wireguard/wg0.conf
$ # or alternately:
$ sudo systemctl stop wg-quick@wg0.service

Then edit the /etc/wireguard/wg0.conf file to add several Interface.PreUp and Interface.PostDown settings, like the following:

# /etc/wireguard/wg0.conf

# local settings for Endpoint B
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/32
ListenPort = 51822

PreUp = iptables -A INPUT -i wg0 -m state --state ESTABLISHED,RELATED -j ACCEPT
PreUp = iptables -A INPUT -i wg0 -m state --state NEW -p tcp --dport 80 -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 -m state --state NEW -p tcp --dport 80 -j ACCEPT
PostDown = iptables -D INPUT -i wg0 -j REJECT

# remote settings for Endpoint A
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
AllowedIps = 10.0.0.1/32

Then bring the interface back up again with wg-quick or systemctl:

$ sudo wg-quick up /etc/wireguard/wg0.conf
$ # or alternately:
$ sudo systemctl start wg-quick@wg0.service

This updated configuration will cause wg-quick on Endpoint B to add three new iptables rules before it brings up the WireGuard interface, and remove the same rules after it brings the interface down. Like the rules for Endpoint A in the previous section, the first rule for Endpoint B will accept all packets for already-established connections (ie connections that Endpoint B initiated) coming through the WireGuard tunnel.

The second rule will allow new connections coming through the WireGuard tunnel to port 80. If you want to allow access to a different port instead of port 80 (like 8080), specify that port instead of 80. If you want to allow access to multiple ports, adjust the rule slightly to use the multiport match directive with the --dports flag (note the trailing s in --dports) — like, for example, to allow both port 80 and 443:

PreUp = iptables -A INPUT -i wg0 -m state --state NEW -p tcp -m multiport --dports 80,443 -j ACCEPT

Make sure you make the exact same change to both the PreUp setting and its corresponding PostDown setting.

The third iptables rule will reject anything else coming into Endpoint B from the tunnel.

Note that if you use an IPv6 address for your WireGuard interface, substitute the ip6tables command for the iptables command in everything above.

Also note that when you’re fiddling with PreUp and PostDown settings, it’s best to bring the interface down first, make your configuration changes, and then bring the interface back up again (rather than making the configuration changes while the interface is still up, and then trying to restart afterward). The reason is that most of the time, your “Up” and “Down” settings are meant to be symmetrical (like start up a service with PreUp and shut it down with PostDown, or add a firewall rule with PreUp and remove it with PostDown) — so if you change a PostDown rule while your WireGuard interface is still running, you might inadvertently leave a stray service running or firewall rule in place from the previous configuration version (which you’ll have to track down and clean up manually).