NFS Authentication and Encryption via WireGuard
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 mentioned 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 set up 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.
Why WireGuard?
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.
Authentication Model
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 host
.
Setting up NFS
Server Installation
If you’re on a Debian-based Linux distribution, you can install an NFS server like so:
sudo apt install nfs-kernel-server
Server Configuration
We need to set up 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 RPCNFSDOPTS
and 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
file must be created or modified with the following configuration section.
[nfsd]
host=10.8.0.1
Make sure you save the configuration files.
Adding Network Shares
Let’s create the directory structures and bind mounts needed for exportfs
.
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 fsid
and crossmnt
, we can exclude the /export
prefix on our client at mount time, and just mount /export/example
as /example
.
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 host
.
Client Installation
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 following 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
Installing 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
In recent kernels, WireGuard ships with Linux by default and will automatically load the kernel module when necessary.
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.
Generate a Pre-shared Key (PSK)
For added security, generate a PSK on the client:
wg genpsk > psk
You will need to generate a new PSK for each of your clients and add them both on the client configuration and on your server. That means every client will have unique pre-shared keys, like they have unique key pairs.
Treat psk
like a password, as well, and delete it when you’re done.
Configuring the WireGuard Server
On the server, identify the network device you’re using to connect to your network. I am using eth0
. Replace eth0
with your network device, or use %i
.
You will need to choose a port to listen on. In this case, we’ll use 51820
.
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
and psk
nearby, as we will use it in that file.
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = HOST_PRIVATE_KEY_GOES_HERE
# client1
[Peer]
PublicKey = CLIENT_PUBLIC_KEY_GOES_HERE
PresharedKey = PRESHARED_KEY_GOES_HERE
AllowedIPs = 10.8.0.2/32
Remember to add your client’s pre-shared key under its [Peer]
column.
Configuring the WireGuard Client
For your client, you will need your host’s publickey
and your client’s privatekey
and psk
.
Create a file located at /etc/wireguard/wgNFS.conf
:
[Interface]
Address = 10.8.0.2/24
PrivateKey = CLIENT_PRIVATE_KEY_GOES_HERE
[Peer]
PublicKey = HOST_PUBLIC_KEY_GOES_HERE
PresharedKey = PRESHARED_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.
Delete privatekey
and psk
Files
On your host and clients delete your privatekey
files after configuring WireGuard, and delete your psk
files, as well.
rm privatekey psk
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 remote-fs.target
.
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 /etc/fstab
.
$ ls /mnt/NFS
it_worked