As an IT consultant, I travel a lot. I mean, a lot. Part of the pleasure is having to deal with day-to-day online life on open, potentially free-for-all hotel and conference WiFi. In other words, the type of networks you really want to do your online banking, ecommerce and other potentially sensitive operations on. After seeing one too many ads for VPN services on bad late night TV I finally decided I needed to do something about it. Ideally I intended to this on the cheap and learn something in the process. I also didn’t want to spend the whole weekend trying to set it up, which is how WireGuard entered the picture. I only really needed to protect my most sensitive device - my personal travel laptop.
As I’m already a customer at Vultr (affiliate link) I decided to just spin up another of their tiny instances and set it up as my WireGuard VPN server. Note that I’m not setting up a VPN service for the whole family, all my friends and some additional people, all I’m trying to do is secure some of my online communications a little bit more.
I also decided to document this experiment, both for my own reference and in the hope that it will be useful for someone else. Readers will need to have some experience setting up and administering Linux server. Come on in and follow along!
Why WireGuard? Don’t I know that it’s experimental, unaudited, potentially harmful?
That would be a yes to all three, yes, I’m aware of the current state of WireGuard and that I really should set up something like OpenVPN or OpenBSD with OpenIKED2 for maximum security. The main downsides with these systems is that while they do a lot more, they’re also harder and more time consuming to set up. They are also overkill for my current needs. I do everything sensitive on my personal dual-boot Windows/Linux laptop running Linux. The Windows partition is really only there to play the occasional game, so I don’t absolutely need to secure its communications with a VPN. As I am only trying to secure communications between a single device and the VPN server, I didn’t think it was necessary to bring out the big guns.
But first, we need to set up CentOS
Oddly enough I’m not a big fan of CentOS, but I needed a stable Linux server platform that’s well maintained. Ease of setup also played a role. I mostly prefer (Free)BSD for my server needs, but WireGuard has a Linux kernel module and is easier to set up on a Linux server than the userspace implementations that are available on OpenBSD and FreeBSD. I tried to set up an Alpine Linux server first, but it looked like WireGuard wasn’t available in the edge repos or I didn’t configure the repos correctly. I suspect it’s more my unfamiliarity with Alpine than anything else. In the end I decided to stick to Linux distributions that I am familiar with. As I do a lot of work with clients who use RHEL or CentOS as their server operating system, it won out over Debian.
I picked one of the 512MB RAM, $3.50-a-month-with-IPV4 -address Vultr instances as a starting point. A lot of hotels don’t have IPV6 yet, so I needed to spend the extra dollar on an IPV4 address. I could get the same on Amazon Lightsail with a higher traffic allowance but as I mentioned I’m already a Vultr customer, very happy with their service and I don’t think Jeff Bezos needs even more of my money. Installation is straightforward when you pick the provided CentOS 7 image. Yes, there are a few downsides to picking this image over a manual install. The big one is that I couldn’t set up whole disk encryption when installing the VM. But it’ll do for now.
Setting up the VPN server
Creating the login user and updating ssh settings
The first thing I do with any server on the Internet is to move SSH to a different port, turn off root logins via ssh and disable password based authentication. As the installed server only has a root user out of the box, we have to ssh in as root once. I usually recommend keeping that connection open until you have the initial setup steps done so you don’t accidentally lock yourself out of the server.
So first, we create a new user using the adduser
command. Next, we set the user’s password using passwd
. Third, we add the user to the users who are allowed to sudo to root. The latter is important because we’ll turn off root access via ssh in a moment, so you need to make sure you have a way to become root. For this, I created a file in /etc/sudoers.d with the following content:
<new username> ALL=(ALL) ALL
At this point, we need try to log in with your newly created user and verify that we can sudo to root. If we can, we log out again and use ssh-copy-id
to copy our local user’s SSH key to the newly created server as we’ll use that from now on. Log back in again and verify that ssh didn’t prompt you for a password. Now it’s time to update your sshd configuration.
The three settings that we need to change in /etc/ssh/sshd_config
are:
- PermitRootLogin - set to no
- Port - set to a suitably obscure port of your own choosing. This will take care of a very large percentage of attempts to brute force your ssh user and password.
- PasswordAuthentication - set to no, after you are really really sure that you can either log into the server using your ssh key (see above) or can log in with your newly created user via the console
Save the updated file, but don’t restart sshd yet. If you are using the Vultr firewall for your VM, make sure that the port you selected as your new ssh port isn’t blocked. We also need to update the firewalld configuration to make sure that access to the new port isn’t blocked by the CentOS firewall.
Updating CentOS 7 firewall settings
At this point I’m assuming that the firewall is set to the public zone, which is likely the correct one for an Internet connected server. We can check which services the firewall is set up for by running firewall-cmd --zone=public --list-services
. If it’s anything like the default I had, you’ll get this output back:
dhcpv6-client ssh
In other words, the firewall blocks everything but the dhcpv6 client and ssh. The service configurations for all supported services can be found in /usr/lib/firewalld/services. The service definition for ssh is in the file ssh.xml and looks like this:
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>SSH</short>
<description>Secure Shell (SSH) is a protocol for logging into and executing commands on remote machines. It provides secure encrypted communications. If you plan on accessing your machine remotely via SSH over a firewalled interface, enable this option. You need the openssh-server package installed for this option to be useful.</description>
<port protocol="tcp" port="22"/>
</service>
I made a copy of the file, calling it ssh-alt.xml and updated the short description and the port to match the new port I had selected.
At this point, reload the service settings that firewall-cmd knows about using firewall-cmd --reload
, then add the newly created ssh-alt service to the public zone with the following command: firewall-cmd --zone=public --add-service=ssh-alt
. The output of firewall-cmd --zone=public --list-services
should now look like this:
dhcpv6-client ssh ssh-alt
Now it is time to restart the sshd server with systemctl restart sshd.service
and try to log in on your new, super secret port. Assuming this all worked, we can now make the firewall change permanent using firewall-cmd --zone=public --permanent --add-service=ssh-alt
While we’re updating the firewall, we need make sure that we also create a rule to allow the UDP traffic generated by WireGuard through. As I mention further down, I chose port 443, thus the firewalld configuration looks like this:
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>WireGuard masquerading as https</short>
<description>WireGuard running on the https port</description>
<port protocol="udp" port="443"/>
</service>
We need to follow the same steps we followed to add the ssh-alt service to also open the port for WireGuard.
Running unbound as your secure locally caching DNS resolver with DNS-over-TLS and DNSSEC
Instead of relying on the Vultr DNS resolver that is sent down with the DHCP settings, I prefer to run my own resolver. For a little bit of extra privacy and security, I’ll also set it up using DNS-over-TLS and DNSSEC.
First, we need to install unbound. yum install unbound
does the trick if we’re willing to live with an older version - 1.6.6 at time of writing. If we want a newer version, the GhettoForge repo has unbound 1.8.0 here. I stuck with 1.6.6 as I’m trying to set the server without too many third-party repositories.
As we don’t want to run a recursive resolver for the rest of the Internet, we need to make sure unbound is only listening on the loopback interface and the interface(s) we will be using for the WireGuard server with the following settings:
server:
interface: ::1
interface: 127.0.0.1
interface: <Address of your WireGuard interface>
If you forget to add the IP addresses of your server-side Wireguard endpoints (ie, the WireGuard interfaces) to the list of interfaces above, you’ll be spending a surprising amount of time trying to figure out why your VPN clients don’t seem to be able to get any working DNS resolution. Don’t ask me how I figured that out, OK?
While we’re in the unbound.conf file, also add the forwarder configuration unless you really, really want to run a full on caching resolver. Thanks to the awesome unbound tutorial over at Calomel.org for all this setup info.
server:
forward-zone:
name: "."
forward-ssl-upstream: yes
forward-addr: 1.1.1.1@853#one.one.one.one
forward-addr: 8.8.8.8@853#dns.google
forward-addr: 9.9.9.9@853#dns.quad9.net
forward-addr: 1.0.0.1@853#one.one.one.one
forward-addr: 8.8.4.4@853#dns.google
forward-addr: 149.112.112.112@853#dns.quad9.net
I put the above into a separate file in /etc/unbound/conf.d. We could also just put the forward-zone block into the main unbound.conf file. From a privacy perspective it’s not ideal to have the Google DNS servers in there, but at least there will be some round-robining between servers. Also, note that the setting that enables DNS-over-TLS for unbound 1.6.6 is called forward-ssl-upstream
. This changed to forward-tls-upstream
in later versions.
With the above settings in place, we can now start unbound using systemctl start unbound.service
and verify that it is working using nslookup www.lego.com localhost
- assuming we have nslookup installed. If you don’t I would strongly recommend installing it on both server and client as it’ll be helpful debugging potential issues.
Some last minute configuration updates
Once we have WireGuard set up, the server will act as a network gateway, forwarding packets from and to the virtual WireGuard interfaces to the greater Internet. We thus need to make sure that forwarding is enabled for ipv4 packets using sysctl net.ipv4.ip_forward=1
. We’ll also want to make this setting permanent by adding it to /etc/sysctl.conf.
As we will be using private, non-routable IP addresses for the actual VPN, we’ll also need to make sure that the firewall has NAT enabled (called masquerading in firewalld speak). Don’t forget to turn that on for your firewall zone using firewall-cmd --zone=public --add-masquerade
and make the setting permanent using firewall-cmd --zone=public --add-masquerade --permanent
Can we get on with this and finally install WireGuard?
Patience, grasshopper. Yes, we are finally at the point where we can install WireGuard.
Installing the WireGuard server
We first have to install the WireGuard repo for epel:
wget -O /etc/yum.repos.d/wireguard.repo https://cpor.fedorainfracloud.org/coprs/jdoss/wireguard/repo/epel-7/jdoss-wireguard-epel-7.repo
After we successfully downloaded the repo definition, a quick yum install epel-release
downloads the updated repo information. We then use yum install wireguard-dkms wireguard-tools
to download the actual WireGuard module and all its dependencies. This will likely take some time as it will probably have to download GCC and other development packages before being able to build the kernel mode.
Time to create the basic WireGuard configuration
Make sure you create the directory /etc/wireguard if it doesn’t exist already and create an empty file called wg0-server.conf in this directory with permissions 0600.
Let’s add the interface, assign it a private IP address and save the configuration to the file we just created:
ip link add dev wg0-server type wireguard
ip addr add dev wg0-server 192.168.0.1/24
wg set wg0-server listen-port 443 private-key <(wg genkey)
wg-quick save wg0-server
Note that I set the port WireGuard is listening to as port 443 - WireGuard uses UDP as its transport protocol so nobody’s going to think we’re dealing with https here but why not. Pick a port that works for you.
After the above, we should have a file wg0-server.conf that looks roughly like this:
[Interface]
Address = 192.168.0.1/24
ListenPort = 443
PrivateKey = <private key we generated above>
We will have to add a little more to the above config file once we have the client set up to make the VPN work, but this is good enough for now. Before we go on to configuring the client, we need to run wg
to get the public key for this connection that we need to share with the client. The output should look something like this:
interface: wg0-server
public key: <we'll need this to set up the client>
private key: (hidden)
listening port: 443
Installing the WireGuard client on Manjaro
I’ve pretty much switched all my desktop Linux installs to Manjaro Linux and my travel laptop is no exception. Manjaro has WireGuard in its repos already, which makes the installation significantly easier. Before you start, make sure that you have the correct version of the Linux kernel header files installed that match the kernel version you’re running. If you do, installing the WireGuard binaries is a simple matter of running the following command:
sudo pacman -S wireguard-dkms wireguard-tools
This will take a while as it may have to pull down a bunch of dependencies and build the kernel module. Like in the server case above, we want to make sure that the /etc/wireguard directory exists - create it if you don’t one yet. You’ll also have to execute all the following commands as root .
Like we needed to for the server, let’s generate a private key for the client: bash -c "umask 077; wg genkey > private.key"
. The key has to have permissions 0600 for root, hence the umask setting. Next, we have to create a configuration file for the client endpoint that roughly looks like this:
[Interface]
Address = 192.168.0.2/24
PrivateKey = <the client side private key we just created>
DNS = 192.168.0.1
[Peer]
PublicKey = ≶the server's public key shown by wg show above>
AllowedIPs = 0.0.0.0/0
Endpoint = ≶Your endpoint's IP address or hostname and port in the format hostname:port>
PersistentKeepalive = 15
At this point we have a client configuration that we can use with wg-quick
to bring up and shut down the client side of the VPN. What we don’t have yet is a working VPN. For that, we have to go back to the server.
More server configuration
There are several server configuration examples floating around the Internet that only have the [Interface] block for the server side, however I was not able to get the VPN working that way. The way I did get it to work was to add a [Peer] block to the server side WireGuard configuration file as well. This makes sense to me conceptually as you’d really want to be able authenticate both client and server against each other. However, not being a WireGuard expert I might simply have overlooked something and it could well be possible to set up the server side without the [Peer] block. My complete wg0-server.conf file looks like this:
[Interface]
Address = 192.168.0.1/24
ListenPort = 443
PrivateKey = <server private key>
[Peer]
PublicKey = <Client's public key, retrieved by>
AllowedIPs = 192.168.0.0/24
I’m not sure if the AllowedIPs setting in the Peer block is strictly necessary. It likely provides more of a warm fuzzy feeling than additional security, because if someone has the keys to connect to the server side of the VPN, you have much bigger problems than the IP range of the client!
Once we have updated the settings above and saved the configuration file, we can restart the server side of the VPN using wg-quick down wg0-server && wg-quick up wg0-server
.
Final configuration on the client side and VPN test
Yes, we’re almost done at this point. We have the fully configured server running on our remote host and can now bring up the client side of the VPN using wg-quick up wg0
.
At this point we want to make sure that we can ping both ends of the tunnel. No point in proceeding further if we can’t. So let’s test our end on the client:
ping -c 1 192.168.0.2
Followed by pinging the remote end of the tunnel:
ping -c 1 192.168.0.2
Both should succeed at this point, and we can use wg show
on the client to verify that traffic is flowing in both directions. The output of wg show should look something like this:
interface: wg0
public key: <redacted>
private key: (hidden)
listening port: 39277
fwmark: 0xca6c
peer: <redacted>
endpoint: 108.61.224.154:443
allowed ips: 0.0.0.0/0
latest handshake: 36 seconds ago
transfer: 3.40 MiB received, 483.41 KiB sent
persistent keepalive: every 15 seconds
If you only see traffic flowing to the server but no data coming back, check that you have an appropriate [Peer} block on the server end and that it has the correct keys in it. Also, we need to make sure that all firewalls at the server end are set up to pass through UDP traffic on your selected port.
Once traffic flows in both directions, the next step is to check that DNS resolution is working correctly. wg-quick should have updated your resolv.conf to point at the VPN server’s DNS as configured in the client settings. I’d check that first, then use `nslookup www.google.com 192.168.0.1` to verify that the client can talk to the VPN server’s unbound DNS server. If that works and yields the same results as a simple nslookup www.google.com
, you should be good to go.
Please note that there is a myriad of possibilities when configuring DNS on a modern Linux system, especially one that uses systemd-resolve. In my case, I have the basic setup that only relies on the dhcp client to update resolv.conf, which makes this undertaking somewhat simpler.
Are we done yet?
Almost. We haven’t set up the VPN server to come up automatically whenever the Vm is rebooted. We do this by running systemctl enable wq-quick@wg0-server.service
You have made it to the point where we have a working WireGuard VPN, what’s next?
At this point, we have a basic WireGuard VPN working, so we’re a little bit better off than before when you’re using the Internet on public WiFi. We cleared the first hurdle, but there is still more to be done. For starters, ads and trackers still merrily flow over your VPN. Second, I noticed that when I checked for DNS leaks using ipleak.net and dnsleak.com, they’re both showing some WebRTC and DNS leaks, so I’ll have to look into that. I suspect that some of the DNS leaks come from the fact that I set up unbound as a forwarder and not as a pure caching and validating server. More interesting to me is that ipleak.net is showing some queries coming from the Vultr.com DNS server, which should definitely not be the case.
So yes, there’ll likely be a few more blog posts on this topic.
If you enjoyed this blog post or have questions, please leave a comment.
Thanks to Calomel for the unbound tutorial, stigok’s blog post about setting up WireGuard on Centos and ckn.io’s older blog post on a typical WireGuard VPN setup.
This site is hosted on Vultr.com. And yes, the links to Vultr.com are affiliate links - if you need hosting, consider going through my link to help offset the hosting costs for this blog.