About NFS and WireGuard
NFS is a network file-system that’s included in the mainline Linux kernel. It’s everywhere that a complete Linux kernel exists. There are Windows and macOS clients for the protocol, and it can be used with Kubernetes and Docker Swarm. Out of the box, the protocol is not encrypted nor does it provide authentication.
WireGuard is a VPN protocol that’s in the process of being included in the mainline Linux kernel. Despite not being in the current mainline kernel, it’s easy to install on pretty much any Linux platform. It has cross-platform implementations, and there are WireGuard clients for Android, OpenWRT, macOS and Windows.
Update: within a few hours of writing this, WireGuard was slated for release in Linux 5.6. Pretty good timing.
Why tunnel NFS over a VPN?
As stated earlier, the NFS protocol is not encrypted. If you’re on an untrusted network, an attacker can easily sniff your NFS traffic. Even if you’re on a trusted network, an untrusted device on your network can sniff your traffic. On WiFi, an attacker doesn’t even need to connect to your network, because WEP, WPA, and WPA2 can be cracked. Enterprise-grade wireless encryption and isolation fare better, but why risk it?
The typical method of providing NFS authentication is via Kerberos. If you don’t already use Kerberos in your network infrastructure, there is an operational and cognitive overhead for its provision and maintenance. In my opinion, the less moving parts, the better.
NFS allows us to restrict access of our network shares to specific IP addresses, subnets and hostnames, giving us some privacy control. However, this is not entirely secure.
If we setup a VPN, we can have our NFS server only listen on that network, and not on the wider unencrypted network. Our VPN server can handle authentication and give our authenticated clients hardcoded IP addresses. In our NFS exports, we can restrict which IP addresses can access certain NFS resources, and have a fine-grained policy for both authenticated clients and our shared resources.
WireGuard is a simple protocol built upon cryptographic primitives that are modern and secure. Its concise implementation gives it a smaller attack surface than other solutions. The protocol and implementation itself have been formally verified, and it has the approval of security experts throughout the industry.
While OpenVPN is widely supported across platforms, it is a behemoth in comparison, which means that it has a wider attack surface. It’s relatively complicated to provision and maintain. As opposed to the in-kernel WireGuard protocol, there is at least one extra context switch between kernel networking and the userspace OpenVPN software. Not only that, but AES is resource intensive on platforms without hardware accelerated AES support, whereas ChaCha20-Poly1305 is performant on low-powered devices without hardware acceleration. Since I use many low-powered Linux devices, that context switch and lack of hardware acceleration for AES on OpenVPN has a measurable impact on the speed of my VPN traffic.
Our authentication will be based on the public key authentication WireGuard provides, and restricting clients to specific IP addresses on our WireGuard network. Authentication is based on our users’ knowledge of a secret, in this case the private key from the key pair our WireGuard server knows about. This is otherwise known as cryptokey routing, and is one of the principles WireGuard is built upon.
WireGuard clients will request a specific IP address from our WireGuard server. Our server will only provision a unique but unchanging IP to the client with the corresponding public key. That way, we can know that traffic from that address is definitely from our authenticated client. We can then use that IP address in our NFS exports to restrict our shares and privileges to only that user.
We can think of those client IP addresses as being our users, and their private keys as passwords. From there, we can configure our NFS server using the basic IP address restrictions the kernel server provides.
NFS over WireGuard
First, we need to choose a subnet for our VPN. I’m going to use
10.8.0.0/24 for our subnet, and
10.8.0.1 as the address for our WireGuard server.
For this article, our NFS server and WireGuard host will have the hostname
Setting up NFS
If you’re on a Debian-based Linux distribution, you can install an NFS server like so:
sudo apt install nfs-kernel-server
We need to setup our NFS server to listen only on
10.8.0.1. We’re also going to configure the server to use only NFSv4, versus older versions of the protocol.
On a Debian-based distribution, our configuration file lives on
/etc/default/nfs-kernel-server. We’re going to edit the
RPCMOUNTDOPTS variables in that file.
To force NFSv4, we need to disable NFSv2 and NFSv3. NFSv4 has less moving parts and operates only on a single port. Older versions of NFS use many ports and require ancillary services to run alongside the NFS server.
Add the following to the configuration file:
RPCNFSDOPTS="-N 2 -N 3"
Next, we’re going to instruct the server to listen on
10.8.0.1 and to further disable older versions of the NFS protocol:
RPCMOUNTDOPTS="--manage-gids -N 2 -N 3 -H 10.8.0.1"
To restrict the NFS server to listen only on the specified host, the
/etc/nfs.conf must be created or modified with the following configuration section.
Make sure you save the configuration files.
Adding Network Shares
Let’s create the directory structures and bind mounts needed for
sudo mkdir -p /export/example sudo touch /export/example/it_worked
Now we need to edit
/etc/exports to allow a client to mount our example share:
/export 10.8.0.0/24(fsid=0,no_subtree_check) /export/example 10.8.0.0/24(rw,sync,no_subtree_check,crossmnt,no_root_squash)
The first line and
fsid option sets the root for our shares. With
crossmnt, we can exclude the
/export prefix on our client at mount time, and just mount
The second line will allow any client on the
10.8.0.0/24 subnet to mount
/export/example as readable and writable. The
sync option makes writes synchronous, while
async makes them asynchronous. I find that synchronous writes are easier to reason about.
no_root_squash will prevent files that belong to
root from being exported with ownership to the
nobody user. I want my network shares to reflect their source ownership as accurately as possible, so I enable this option.
Let’s export our shares:
sudo exportfs -a
Starting the Server
First, let’s disable
rpcbind because it isn’t required for NFSv4:
sudo systemctl mask rpcbind.service sudo systemctl mask rpcbind.socket
Then, start the server, or restart it if it’s already running:
sudo systemctl restart nfs-kernel-server.service
Great, now let’s test it out on a client machine. Ironically, if everything works as configured, mounting the share should fail, but with specific errors.
Configuring the NFS Client
Connect a separate Linux machine to your network, such that it can reach your host via it’s hostname
Again, assuming you’re using a Debian-based platform, install the NFS client software like so:
sudo apt install nfs-common
Testing NFS Security
We’re going to try to mount our network share, and hopefully it will fail. Replace the hostname
host in the follow commands with the hostname of the machine running your NFS server.
sudo mkdir /mnt/NFS sudo mount -t nfs -o vers=4.2 host:/example /mnt/NFS
Hopefully, you will be met with either of the following errors:
mount.nfs: access denied by server while mounting host:/example
Or, this error:
mount.nfs: Operation not permitted
Setting up WireGuard
Head over to the official WireGuard website and choose your installation option.
On my platform, no configuration is needed and the following will install the needed packages:
sudo apt install wireguard
After the software is installed, insert the
wireguard module into the kernel:
sudo modprobe wireguard
Generating Key Pairs
Our server will need to generate a key pair, and each of our clients will need to generate their own unique key pairs.
On both our server and client issue the following:
umask 077 wg genkey > privatekey wg pubkey < privatekey > publickey
Remember to treat your private keys like passwords, and to delete the
privatekey files when you are done with them.
Configuring the WireGuard Server
On the server, identify the network device you’re using to connect to your network. I am using
eth0 with your network device.
You will need to choose a port to listen on. In this case, we’ll use
Copy the contents of our server’s
privatekey to your clipboard. Then, create a file located at
/etc/wireguard/wgNFS.conf. Keep the contents of your first client’s
publickey nearby, as we will use it in that file.
[Interface] Address = 10.8.0.1/24 PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT;iptables -A FORWARD -m conntrack --ctstate NEW -s 10.8.0.1/24 -m policy --pol none --dir in -j ACCEPT; iptables -t nat -A POSTROUTING -s 10.8.0.1/24 -m policy --pol none --dir out -j MASQUERADE; iptables -A INPUT -p udp --dport 51820 -j ACCEPT PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE ListenPort = 51820 PrivateKey = HOST_PRIVATE_KEY_GOES_HERE # client1 [Peer] PublicKey = CLIENT_PUBLIC_KEY_GOES_HERE AllowedIPs = 10.8.0.2/32
Configuring the WireGuard Client
For your client, you will need your host’s
publickey and your client’s
Create a file located at
[Interface] Address = 10.8.0.2/24 PrivateKey = CLIENT_PRIVATE_KEY_GOES_HERE [Peer] PublicKey = HOST_PUBLIC_KEY_GOES_HERE Endpoint = host:51820 AllowedIPs = 10.8.0.0/24
The last line tells the WireGuard client to route traffic on the
10.8.0.0/24 subnet through the WireGuard server endpoint.
On your host and clients delete your
privatekey files after configuring WireGuard.
Starting and Connecting to the VPN
On both the host and client, but starting with the host, issue:
sudo wg-quick up wgNFS
Test that the connection works by pinging the host from the client:
$ ping -c 1 10.8.0.1 PING 10.8.0.1 (10.8.0.1) 56(84) bytes of data. 64 bytes from 10.8.0.1: icmp_seq=1 ttl=64 time=0.89 ms --- 10.8.0.1 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 0.893/0.893/0.893/0.000 ms
Mount NFS Share over WireGuard
On the client machine, run the following:
sudo mount -t nfs -o vers=4.2 10.8.0.1:/example /mnt/NFS
Test to see if the share mounted.
$ ls /mnt/NFS it_worked
You should be able to access the
it_worked example file we created earlier.
Making it Permanent
WireGuard via systemd
On both the server and client, add a WireGuard service to systemd. This service will start at boot.
sudo systemctl enable [email protected] sudo systemctl daemon-reload
Tear down the existing
wgNFS connection on both machines and then create the VPN connection via the new systemd service.
# stop WireGuard sudo wg-quick down wgNFS # start WireGuard via systemd sudo systemctl start [email protected]
Unmount the example share on the client.
sudo umount -f /mnt/NFS
Mount NFS Share Automatically via systemd
Add an entry to
/etc/fstab on the client with the parameters for the shares.
10.8.0.1:/example /mnt/NFS nfs vers=4.2,_netdev,noauto,x-systemd.automount,[email protected]
Reload systemd and
sudo systemctl daemon-reload sudo systemctl restart remote-fs.target
Now, whenever the mountpoint is accessed, systemd will automatically mount the NFS share to the mountpoint specified in
$ ls /mnt/NFS it_worked