How to implement access controls in Wireguard® virtual networks with Netmaker

Posted by
Alex Feiszli
published
March 17, 2023

Organizations are switching to WireGuard®. Why?

  • It’s very fast
  • It’s very efficient
  • It’s baked into the Linux kernel

This makes WireGuard® ideal as a networking layer for distributed systems like IoT and Kubernetes, as well as for remote access.

However, there are still many challenges involved in setting up a WireGuard® virtual network at scale. Specifically, configuration management and access controls.

Netmaker automates WireGuard® configuration, decreasing the level of complexity necessary to manage WireGuard® across many devices.

In v0.12.0, Netmaker introduced Access Control Lists, which allow operators to manage fine-grained ACLs. Put simply, this creates an easy system for controlling which machine can talk to which.

In this article, we give a quick demonstration of managing WireGuard® access controls with Netmaker.

Setup

Our setup consists of a Netmaker server and 6 machines running in AWS: Four in us-east-1 and two in eu-central-1. All machines run Linux. All machines have been added to the Netmaker network acl-net-1.

By default, Netmaker creates a “full mesh” P2P network, meaning every machine can reach every other machine. We can see this in the Graph visualization:

This is confirmed by viewing the output of “wg show” locally:

root@linux-large-3-germany:~# wg show nm-acl-net-1
  interface: nm-acl-net-1
  public key: xxxxxxxxxxxxxxxxxxxx
  private key: (hidden)
  listening port: 34314

peer: xxxxxxxxxxxxxxxxxxxx  
 endpoint: (hidden):42953
 allowed ips: 10.83.18.5/32
  latest handshake: 1 minute, 48 seconds ago
 transfer: 1.13 KiB
 received, 1.27 KiB sent  
 persistent keepalive: every 20 seconds

peer: xxxxxxxxxxxxxxxxxxxx
 endpoint: (hidden):53856  
  allowed ips: 10.83.18.4/32  
 latest handshake: 1 minute, 48 seconds ago  
  transfer: 1.30 KiB received, 928 B sent  
 persistent keepalive: every 20 seconds

peer: xxxxxxxxxxxxxxxxxxxx  
  endpoint: (hidden):44417  
  allowed ips: 10.83.18.3/32  
  latest handshake: 1 minute, 48 seconds ago  
  transfer: 340 B received, 272 B sent  
  persistent keepalive: every 20 seconds

peer: xxxxxxxxxxxxxxxxxxxx  
  endpoint: (hidden):54572  
  allowed ips: 10.83.18.2/32  
  latest handshake: 1 minute, 48 seconds ago  
  transfer: 340 B received, 272 B sent  
  persistent keepalive: every 20 seconds

peer: xxxxxxxxxxxxxxxxxxxx  
  endpoint: (hidden):37150  
  allowed ips: 10.83.18.1/32  
  latest handshake: 1 minute, 48 seconds ago  
  transfer: 1.02 KiB received, 1.02 KiB sent
 persistent keepalive: every 20 seconds

peer: xxxxxxxxxxxxxxxxxxxx  
  endpoint: (hidden):51822  
  allowed ips: 10.83.18.254/32  
  latest handshake: 1 minute, 50 seconds ago  
  transfer: 124 B received, 340 B sent  
  persistent keepalive: every 20 seconds

A ping test confirms connectivity:

root@linux-large-3-germany:~# ping 10.83.18.3
PING 10.83.18.3 (10.83.18.3) 56(84) bytes of data.
64 bytes from 10.83.18.3: icmp_seq=1 ttl=64 time=89.0 ms
64 bytes from 10.83.18.3: icmp_seq=2 ttl=64 time=89.1 ms
64 bytes from 10.83.18.3: icmp_seq=3 ttl=64 time=89.0 ms
64 bytes from 10.83.18.3: icmp_seq=4 ttl=64 time=89.1 ms
^C
--- 10.83.18.3 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3003msrtt min/avg/max/mdev = 89.016/89.083/89.143/0.057 m

Now, let’s restrict this network with ACLs.

ACLs

In the ACL page on Netmaker’s UI, you get a full list of all P2P connections in the network. You can disable any pair of connections by clicking on it.

As you can see, by default, all connections are enabled. Lets make it so the nodes in Germany cannot talk to the nodes in Virginia.

You can click on an individual node to restrict its connections:

You can also change rules from the main screen:

Once you click “SUBMIT CHANGES,” the nodes will be notified immediately and update their local settings.

This also propagates to the Graph view, which shows the update:

We confirm this change locally by looking again at the peer list. For instance, on our machine in Germany, it shows only 1 other machine in Germany, and the Netmaker server:

root@linux-large-3-germany:~# wg show nm-acl-net-1
interface: nm-acl-net-1  
  public key: xxxxxxxxxxxxxxxxxxxxxxxx  
  private key: (hidden)  
  listening port: 34314

peer: xxxxxxxxxxxxxxxxxxxxxxxx
 endpoint: (hidden):54572
 allowed ips: 10.83.18.2/32
 latest handshake: 22 seconds ago
 transfer: 956 B received, 456 B sent
 persistent keepalive: every 20 seconds

peer: xxxxxxxxxxxxxxxxxxxxxxxx
 endpoint: (hidden):51822
 allowed ips: 10.83.18.254/32
 latest handshake: 24 seconds ago
 transfer: 308 B received, 956 B sent
 persistent keepalive: every 20 seconds

This node in Germany is no longer able to ping the node in Virginia, which was previously available:

root@linux-large-3-germany:~# ping 10.83.18.3
PING 10.83.18.3 (10.83.18.3) 56(84) bytes of data.
From 10.83.18.6 icmp_seq=1 Destination Host Unreachable
ping: sendmsg: Required key not available
From 10.83.18.6 icmp_seq=2 Destination Host Unreachable
ping: sendmsg: Required key not available
From 10.83.18.6 icmp_seq=3 Destination Host Unreachable
ping: sendmsg: Required key not available
^C
--- 10.83.18.3 ping statistics ---
3 packets transmitted, 0 received, +3 errors, 100% packet loss, time 2052ms

We have successfully segmented the network.

This is great, but what about the inverse? What if, by default, no machine should have access to any other machine. In this scenario, you want to manually approve every machine’s access.

Default Deny

We can achieve this by creating a network with a “default deny” policy. Any machine that enters the network will have no peers, and no connections. Let’s create a network with this setting enabled:

Note that “Default Access Control” is switched off. This is what changes the default policy to “deny.”

After creating, we join all of our nodes to the new network.

As you can see via the Graph, none are connected:

This is confirmed locally by checking the WireGuard interface again, where we see there are no peers:

root@linux-large-3-germany:~# wg show nm-acl-net-2
interface: nm-acl-net-2
 public key: xxxxxxxxxxxxxxxxxxxxxxxxxxx
 private key: (hidden)
 listening port: 51821

This allows us to “build up” our network from nothing using the UI:

This time, we enable all the small machines to talk to each other:

Again, this populates to the UI after submitting:

And we confirm by checking the WireGuard peers locally:

root@linux-large-3-germany:~# wg show nm-acl-net-1
interface: nm-acl-net-1
 public key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 private key: (hidden)
 listening port: 34314
peer: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 endpoint: (hidden):54572
 allowed ips: 10.83.18.2/32
 latest handshake: 22 seconds ago
 transfer: 956 B received, 456 B sent
 persistent keepalive: every 20 seconds
peer: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 endpoint: (hidden):51822
 allowed ips: 10.83.18.254/32
 latest handshake: 24 seconds ago
 transfer: 308 B received, 956 B sent
 persistent keepalive: every 20 seconds

*Important Note on Default Deny

We also switched off UDP Hole Punching in this scenario. Why? UDP Hole Punching relies on the client having a connection to the server node. If this is disabled, connections will break. Alternatively, we could have switched on UDP Hole Punching, and then ensure we manually enable every node’s connection to the server in the ACL page.

Conclusion

This concludes our quick demonstration of implementing ACLs on WireGuard networks with Netmaker. There are many use cases this applies to, from IoT, to Kubernetes, to Remote Access. Stay tuned for additional tutorials where we look at some specific use cases.

Disclaimer: WireGuard is a registered trademark of Jason A. Donenfeld.

More posts

GET STARTED

A WireGuard® VPN that connects machines securely, wherever they are.
Star us on GitHub
By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.