Wednesday, 4 July 2018


I have written some articles already about making your own VPN gateway or even your VPN server, using BSD OS (OpenBSD, or FreeBSD). Recently I came to learn Docker and felt in love with it, and I wondered how I would make a fully Dockerized VPN gateway. I wondered also how secure Docker truly was, and if I could trust it for my VPN gateway. It was a chance to give Linux back a shot too, and to build a VPN gateway using also ProtonVPN.

In this article we will:
- check the security of Docker and ProtonVPN, and see what we could possibly do to improve it (if needed).
- quickly review Ubuntu's default security as well.
- see all of the steps to build a VPN gateway on Ubuntu 18.04 LTS, using Docker: OpenVPN and ProtonVPN, DNScrypt and Unbound, Dhcp, all as Docker containers (you can jump right away at this part if you don't care for the lengthy beginning).

As Docker is involved, a lot of configuration files will be copy/past ready thanks to "Docker compose" as we will see later. This article should, configuration wise, be faster to complete than previous related articles. Please notice that this setup can be done with any VPN provider, the choice of ProtonVPN is only because I already trust it and use it, for many reasons I will outline later. Feel free to use any VPN provider you like.

Also as usual, I'll focus not only on making a functional setup, but also a secure setup. Let's dive in!



Docker is a container platform, born in 2013, enabling you to containerise various applications in their own "box" (container). This capability brings:
- security: in a container no unecessary packages are installed, the image is very lean and provides consequently less attack surface.
- agility: by creating a Docker Swarm (cluster) you can add automatic redundancy and load-balancing for your containers.
- simplicity: if you develop an application, it's easier to test on another container without modifying the one in production, and no need anymore to develop variants for various Operating Systems.

Docker is very popular because it answers needs from developpers, allows for designing secure architectures, and enables companies to diminish their IT costs. Google itself is running Docker and contributes to the community by sharing images on Docker Hub. Other companies are using it such as Visa, Paypal, and at least one french bank. A 2014 article also says that Red Hat and Spotify are embracing Docker.

Now let's take a look at its security from different angles:

A Docker container has its own IP network and IP address by default, and is not reachable from the outside world. That lowers also the attack surface as you will only expose required ports, independantly of how many ports your application is opening inside the container. However, and that is one of the biggest difference with a full blown Virtual Machine (VM), Docker containers are using the host's kernel. If a vulnerability exists in the host's kernel, it could be used by a compromised container to break out of its isolation and reach the host.

Docker using "namespace" Linux technology, it is able to isolate containers processes and networks from the host. Using the kernel level feature "Control groups" or "cgroups", it can restrict which ressources containers have access to.

On a Docker cluster called Swarm, overlay networks are natively encrypted in AES. Containers can share "secrets" which can be anything such as password, and which are natively stored encrypted too. In fact, the cluster Raft database is fully encrypted by default. Also, nodes and managers in a swarm cluster communicate with TLS exclusively by mutual authentication. Certificates can be used as well in this swarm context. Swarm seems like well designed from the ground up with security in mind.

Security scanner
Docker Entreprise Edition (EE) or Aqua MicroScanner enables you to check the Docker images you are building against known vulnerabilities. In Docker EE, which is definitely not free and more aimed at entreprises, an automatic security scan is available. For home users, an alternate free image scanner "Aqua MicroScanner" can be used by modifying your Dockerfile and inserting lines into it. It will stop image building if a vulnerability is detected in the packages you are using. A paid version exists, which does not require you to modify the Dockerfile.

Docker's default security is fine, but it can be maxed out a bit. Indeed, by default User Namespace isolation is not enabled. A namespace provides an isolation of something, for instances process IDs, between the container which has its own PID namespace and the host which has another one. This works too for networks where containers can have their own, isolated from the host. User namespace not being enabled by default, means that the root account from your container is the root account from the host. Consequently if a bad guy breaks out from the container with the root user, it becomes instantly root on the host. In reality it is not as bad as it seems, as the root account inside the container is a neutered one with reduced capabilities, as I'll explain later. Still, even if the container root user is an underpowered one, it kind of breaks the idea of "isolation" between the container and the host. The solution is perfectly documented in Docker, you can enable user namespace. What happens is that you create a dummy no privileges account on the host, and Docker will map this account to the root account from the containers. For this to work, user namespace has to be enabled in Docker, as we will see in the configuration chapter. This security is not enabled by default, because it can break incompatible Docker volume plugins for instance. I strongly advise to enable anyway this setting globally, and to opt-out this feature per container if you encounter an incompatible one. Big warning: if you enable this feature on a production server, all of your images and volumes will be wiped, as the remapping creates new directories and environnement for your containers, which must be redeployed and configuration files copied back into the new volumes.

Additionally, you can set your containers to a read only file system! This can greatly improve your security if your container one day is attacked, if the attacker is prevented to easily write new malicious files on the file system, it will greatly improve your security. This is not bulletproof because most containers require at least one writable area to write their PID or temp files, but still configuration and critical files being read only improves your protection.

Also, you can play with capabilities. Capabilities allows for granular privileges instead of either being root, or an unprivileged user. An unprivileged user having the net_bind_service capability for instance can open ports below 1024, without the need for being root. You can consequently in Docker add capabilities to your containers if required, without giving too much rights. Also, while containers start with default reduced capabilities, if you want to disable more you can play with it. One important point to understand is that the root account inside the container does not have all of the capabilities the true root account on the host has, it is a neutered account. Another example, is to give the net_admin capability to OpenVPN, as we will see later, instead of giving it all privileges as it was the case in older Docker version (nullifying the idea of container and isolation).

Finally, Docker is running on the host, and therefore the host kernel security is of the utmost importance. If all your containers are well hardened and as strong as Wolverine Adamantium, but that you are on a weak and leaky floor, chances are you will fall trough this unsecure floor. Said differently, if a vulnerability exists in your host kernel, it can be used to escape from your containers, to cause denial of service, and execute arbitrary code. It is thus a wise idea to harden the host kernel as well. The basic I advise is to enable Kernel ASLR (KASLR). Address Space Layout Randomozation (ASLR) broadly speaking is a mean to randomize addresses so that vulnerabilities exploits cannot easily predict memory addresses to base their attack on. This is not a perfect defense, as attacker can find some memory leaks elsewhere to help them guess what they need, but it is still useful to improve your host kernel security. To harden your kernel more, you can even apply PAX, GRSECURITY, and SSP kernel patches to significantly increase your kernel security. However it is not that simple as the patch has to be compatible with your kernel version. For instance Ubuntu 18.04 LTS has currently a 4.15 kernel version, while grsecurity website has a patched 3.1 kernel version. Moreover, grsecurity requires you to purchase a subscription to be able to download.

Breaking out
Docker container are similar to FreeBSD jails. However as an example, FreeBSD jails suffered vulnerabilities in 2004 with CVE-2004-0126 (Jail Unauthorized Access Vulnerability) and CVE-2004-125 (Jailed processes can manipulate host routing table). Still today, with this August 2017 CVE-2017-1087 (SHM) allowing different jails to communicate bypassing isolation. This is not to say FreeBSD jails are insecure, but just to highlight that a similar technology which was born earlier in FreeBSD 4.0 (March 2000) still has vulnerabilies found today, 18 years later.

Docker uses LXC on Linux and offers high level commands to make containers that use "lower level" LXC transparently. If critical vulnerabilities are found either on Docker or LXC, we are in trouble. Sometimes, a vulnerability can be found on another component used by Docker, such as this one on March 2018 on Windows OS with CVE-2018-8115. The component "Windows Host Compute Service Shim" had a vulnerability allowing, from inside a container, to write files on the underlying Windows host, leading to remote code execution. However, at the end do not think that VM are bulletproof because the host has its own kernel. Please check this link where some serious CVE are listed for VMware ESXi, VMWare Workstation, Xen, Virtualbox, and Hyper-V. No virtualization technology, either OS virtualisation such as containers, or Hypervisor virtualisation, can reach 100% isolation. The container or the VM is running on a host or an hypervisor, a layer which can have vulnerabilities.

Breaking in
Containers are no different than VM or bare metal systems, if you have a bad security hygiene, you can inadvertedly expose your containers orchestration to the Internet (Theatpost article: 22 000 containers orchestration and API management exposed and unprotected!). If you poorly configure your containers, they will be poorly protected, that's obvious. This aformentioned article shows that containers are not magically secure by themselves, they are a tool you can use safely or not. Also you can safely configure your Docker settings, but the application running inside the container must be secured as well.

Improvements required
Docker is running as root on the host, which is the biggest concern to me. Granted, to do all of the aformentioned privileged stuff it requires privileges, but privilege dropping is something often used by applications which start, listen to a port, and then drop their privileges to nobody or any unprivileged user. I do not say it is that easy and that Docker could just do it, if it was that simple it would already be done. Docker is a more complex framework, but in the end currently it is running as root and you should be aware about it. Consequently, do not give access to untrusted users to your system, and I advise against adding your user into the docker group to avoid using sudo . You certainly do want to ensure sudo is required before running any privileged commands.

Secure or not?
In the end yes, I see Docker as being fairly secure. It is still a work in progress and there is always room for improvement in the security field, but correctly configured and used, Docker seems pretty solid to me, given all of the limitations I have explained above. You can read this article talking about Docker security: Fact vs. fiction: 6 myths about container security.


In the setup below I have chosen to use Ubuntu Server OS. But is Ubuntu any secure? Isn't there a huge security gap down from OpenBSD? Sure, Ubuntu is popular and used by many entreprises, but we can wonder which security features it includes. Last time I compared FreeBSD vs OpenBSD security, it was obvious that commonly used was not equivalent to secure by default.

About Ubuntu, a good place to start is We can see that, by default:
- there is no open ports
- SHA512 is used for password hashing
- SYN cookies, against SYN flood/DoS, is enabled by default
- it uses a cloud PRNG seed
- supports /home and filesystem encryption
- has some memory protection: Stack Protector, Heap Protector, Pointer Obfuscation, Stack ASLR, Libs/mmap ASLR, Exec ASLR, brk ASLR, VDSO ALSR, PIE, NX, /dev/mem protection, RO Data Section, and some more.
- kernel patches against Meltdown and Spectre (Ubuntu 18.04) - it brings automatic security updates (you can opt-out)

Not enabled by default is Kernel ASLR, which can be easily turned on. Enabling KASLR will disable hibernate mode however.

What can be done too at setup time, is to use the minimalist network Ubuntu Server image, and selecting no package to install except OpenSSH. Doing so will install few packages, and they will be up to date by default, because downloaded from the Internet as the setup media does not have them.

In the following video Ubuntu What's the Security Story, the following relevant subjects are mentioned (some are already listed above):
- Kernel Livepatch, without rebooting. Handy to apply security fix without rebooting
- From Ubuntu 16.04 LTS, unattended-upgrades is configured automatically to apply security updates daily
- Ubuntu has AppArmor and SELinux
- Ubuntu can encrypt all partitions including swap
- Userspace Hardening: default compiler flags and kernel settings are tuned for security
- Various memory protections
- NULL dereference kernel attack protection
- Kernel Address Display Restriction

Clearly Ubuntu cares about security, and its Long Term Support (LTS) version is supported for 5 years, and will therefore receive security patches for this long.

I do not have the ressources to compare every Linux distribution out there, thus I cannot tell you that Ubuntu is more secure than X, and Y is more secure than Ubuntu. What I can say, is that Ubuntu seems fairly decent security wise to me.


I am already a paid user of ProtonMail, and I trust their ProtonVPN service as well. However trust does not imply to trust blindly! So let's see below if it has any good security under the hood.

What we want to know when we choose a VPN service is: log policy, trust, privacy, security, and ultimately speed and usablity. All VPN services claim to be secure, if not the most secure. All we can do is to check the claims one by one and see if the reality meets the advertising. We can begin by checking what is said on the ProtonVPN Security page:

Strong encryption
"highest strength encryption used", AES-256, key exchange with 2048-bits RSA, and message authentication with HMAC-SHA256

I have read elsewhere on ProtonVPN blog or article that all encryption settings are maxed out by default. I was then disapointed to see that it was HMAC-SHA256 purpotedly used whereas HMAC-SHA512 would be obviously safer. However, when I downloaded an ovpn file from ProtonVPN to be used by OpenVPN on Linux, SHA512 is defined, not SHA256. When I asked the question to support, they replied that SHA256 could be used on older router negotiating a connection with ProtonVPN, which is why they only advertised SHA256. That's honesty at its best, I'm sure other VPN services would have had no problem advertising SHA512 even if sometimes it could fall back to SHA256 for older hardware. On one hand that is honest, on the other hand that is slightly misleading in a good way, as you have a better security than claimed on their website.

Authentication algorithm being checked, the ovpn file to download also clearly shows they indeed use a 2048-bits RSA key for exchange, and that traffic is encrypted using AES-256. All of these are confirmed in my router's log when my OpenVPN connection is made. Relevant log lines are:
Outgoing Control Channel Authentication: Using 512 bit message hash "'SHA512' for HMAC authentication
Incoming Control Channel Authentication: Using 512 bit message hash 'SHA512' for HMAC authentication
Control Channel: TLSv1.2, cipher TLSv1/SSLv3 ECDHE-RSA-AES256-GCM-SHA384, 2048 bit RSA
Data Channel Encrypt: Cipher 'AES-256-GCM' initialized with 256 bit key
Data Channel Decrypt: Cipher 'AES-256-GCM' initialized with 256 bit key

Forward Secrecy
"carefully selected encryption cipher suites" which provide Perfect Forward Secrecy (PFS).

PFS is a big deal, because without it, an adversary can record your traffic and try to decrypt it later if he succeeds to compromise afterwards one of your session key. With PFS, a new encryption key is used for each session. Consequently, to check if this claim is true, we check again the last log lines, especially:
Control Channel: TLSv1.2, cipher TLSv1/SSLv3 ECDHE-RSA-AES256-GCM-SHA384, 2048 bit RSA

- ECDHE: Elliptic Curve Diffie-Hellman. DHE/Diffie-Hellman is required for PFS to be used, so this point is validated.
- RSA: key validation is done using RSA
- AES256-GCM: control channel encryption is AES-256 in GCM mode
- SHA384: parts of TLS message will be hashed using SHA384

Strong protocols
exclusive use of VPN protocols which are known to be secure.

This points is explained as ProtonVPN not using unsecure protocols such as PPTP or L2TP/IPSec. Even if not written in their website, I'm also very interested by the TLS version used. Indeed, TLS is used to exchange HMAC and encryption/decryption keys, if it is compromised, the whole VPN session is. Moreover, there is an important security gap between TLS v1.1 on one side, and TLS v1.2 on the other side. Main advantages of TLS 1.2:>
- the only TLS version providing AES in GCM mode (Galois Counter Mode). AES in CBC (Cipher Block Chaining) in TLS suffers attacks such as Lucky-13 and BEAST.
- the pseudorenadom function (PRF) replaces MD5/SHA1 with SHA-256
- provides advanced cipher suites that support Elliptic Curve cryptography

For these three reasons, especially the first one, TLS 1.2 is mandatory if you want to talk about a secure VPN service. As we have seen in the log above, TLS 1.2 is automatically selected on the test I have done on my router, without the need to force it in the client side (which would be done in OpenVPN with the file setting tls-version-min 1.2 .

Legal protection & Log policy
Swiss based with some of the world's strongest privacy laws.

That is not a technical point, but still is a valid one. Indeed, I would personnally not trust a US (NSA) or UK (GCHQ) based VPN provider. Besides, Switzerland is not part of the fourteen eyes country as advertised on ProtonVPN page, thus this country is not engaged in spying and sharing information with other countries (as far as we know). On the log side, ProtonVPN claims to not log traffic. While it is true that Swiss law could not force ProtonVPN to log its user traffic, this point is a matter of trust, and we cannot prove it either way. I personnally do trust ProtonVPN, but you may disagree.

DNS leak prevention & Kill Switch
ProtonVPN routes DNS queries trought the VPN tunnel.

DNS leaks are common with VPN software, when Windows uses its default settings and queries the ISP DNS servers instead of using the VPN tunnel. If that happens, your ISP can see all of your DNS queries and know where you go and when. Also, if your VPN disconnects, all of a sudden your traffic goes in clear trought your ISP as well. In both cases, DNS leak or disconnection, no matter the encryption level used and performances, your traffic is busted. Enabling Kill Switch keeps the ProtonVPN offer pretty secure. However I have seen one report of someone having issues with the Kill Switch, and traffic leaking out in the clear. While I do not have all information at hand, ProtonVPN support replied that they may have found a problem in OpenVPN itself and are working on a fix. Also, it could be any software conflict between ProtonVPN and a security product, which is why it is always good to test and check that your VPN is working as expected. I like to check my visible IP and DNS at The best is to combine it with a live network capture with Wireshark to ensure the usual network adapter has no outbound traffic while VPN is connected (if you are on a LAN some local broadcast could still be seen). To exclude the VPN traffic, apply a capture filter not udp port 1194 after you have selected your network card in Wireshark, but before you run the capture.

I strongly advise enabling the Kill Switch feature if you use the Windows client and do not intend to build a home VPN gateway. Kill Switch is meant to cut all traffic when the VPN is disconnected to avoid traffic leaks, because the first default route created by the LAN adapter is deleted. This has the side effect to prevent any leaks that could happen while the VPN is running too, as a single default route remains (TAP adapter), leaving no choice to potential rebel applications. However it is not a proper "fix" as all traffic should go trought the VPN tunnel no matter the Kill Switch feature is enabled or not. I encountered such issues in an old version of the Windows client (1.0.3) when it was released, as I had leaks with Kill Switch disabled. The support at that time told me this was forwaded to the administrators and developpers, and that it should be hopefully fixed in a future version.

That is why I prefer generally the VPN being done at the router level so I can fully control the potential leaks, and avoid OS bugs with network adapters and DNS queries. So far we have seen that ProtonVPN network and protocols are very secure.

Please use the Network Installer of the latest Ubuntu LTS version (18.04 at the time of this writing).

This chapter is not a step by step install, you are assumed to know how to install Ubuntu Server. While installing Ubuntu, just ensure to select "install security update automatically" (we'll come back to that later) and in the packages select only "OpenSSH server".

At this point it is of course crucial to have properly configured your egress (outbound) interface and your LAN (inbound) interface. You must know your interfaces names and IP addresses as it will be the base for the rest of the setup.

From now on, and for the rest of the article, we will assume our router has the following interfaces names and IP addresses:

Keep in mind you must obviously replace the given interfaces names and IP addresses by yours, in all examples that will be given.

After the install is complete, the base install does not contain much. It is necessary to install the following packages we will need later:
$ sudo apt install net-tools curl

net-tools is required to use ifconfig . Optionnaly, you can install htop too as it is easier to check used ressources with it.

Now we need to configure the network. In Ubuntu 18.04, parameters we are interested in are in the following file:
$ sudo nano /etc/netplan/01-netcfg.yaml
     addresses: [ ]
         - ""
     addresses: [ ]

These parameters here are based on the virtual router of this article and are merely displayed as an example. You must modify them according to your interfaces names, IP addresses and DNS.

Then, to apply our modifications:
$ sudo netplan apply

You can now connect with SSH from your computer. For best security, create an SSH key and connect with it.

As explained previously, Kernel ASLR is not enabled by default on Ubuntu. To enable it, modify the grub file by adding "kaslr" at the end of the line below:
$ sudo nano /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT="splash quiet kaslr"

Then to apply our modification to grub:
$ sudo update-grub

Enabling kASLR will disable the ability to enter hibernation mode, as explained on the Ubuntu Security page. However this is not of concern for our router.

We enabled automatic security updates in the Ubuntu setup to install the unattended-upgrade package. However I want to launch it manually when I want, instead of letting it being scheduled and ran automatically. If you prefer like me to control the update process, you can disable it:
$ sudo nano /etc/apt/apt.conf.d/20auto-upgrades
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "0";

Setting to zero the second line will disable the automatic schedule of unattended-upgrade . By the way, if you want to run it manually, just type:
$ sudo unattended-upgrade -v

Now that our base system is installed, up and running, we need to install the tools we will based our container architecture on. Welcome to Docker:
$ curl -fsSL -o
$ sh

This is better to install it this way to retrieve the latest stable version available. Docker by itself will run our containers, but to build or "compose" our container architecture, it is easier to install Docker Compose as well:
$ sudo -i
# COMPOSE_VERSION=$(curl -s | grep 'tag_name' | cut -d\" -f4)
# curl -L${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
# chmod +x /usr/local/bin/docker-compose
# exit
$ sudo docker-compose version

Docker compose will allow us to make a compose file, including all of our containers descriptions and parameters, and to run them all at once easily. Before getting to that part, there is few settings to configure before. Default settings are good, but user namespace is not enabled as explained in a previous chapter. Also, by default Docker manipulates iptables rules to create a flawless user experience. While it is a good intent, as we want to tighthly control our firewall in this case, we will disable this behavior:
$ sudo nano /etc/docker/daemon.json
  "userns-remap": "dockeruser",
  "iptables": false

Before enabling user namespace remapping, we need to create our unprivileged dockeruser:
$ sudo adduser dockeruser
$ cat /etc/subuid
$ cat /etc/subgid

If the two last commands return lines with your user created, all is good. If however your newly created user does not appear, something is wrong, you cannot go further, delete and create again your user (I never encountered such trouble in all my tests, just being careful in case you do). To apply our modifications, we must restart Docker service:
$ sudo service docker restart

Roadmap from now on to build our router, so as you can have an overview of the process:
1 - write our compose and env files
2 - write our iptables script
3 - write our *.conf files (openvpn.conf, unbound.conf, etc.)
4 - start our containers

To speed things up greatly, you have to download all of the required files I prepared for you below and that you will need in the following install steps. You do not need to open them all now and to wonder what to do with them. They can be downloaded all at once, but then they will be viewed one by one in the rest of the setup steps, don't worry.

You will find below a summary of the files and those that you will have to modify (*) in the next steps we will see together:
- compose-vpn.yml
- .env (*)
- firewall (*) (wait for the firewall chapter)
- firewall.service
- unbound.conf
- openvpn.conf (*) (generic one without certs)
- auth.conf (*) (generic one)
- (*)
- dhcpd.conf (*)

To download them:
$ mkdir Docker && cd Docker
$ sudo apt install git
$ git clone
$ mv docker-vpn-router/* .
$ rmdir docker-vpn-router
$ chmod +x ./*.sh

Now you have all of the files sitting here, ready to be used. We will check the next steps one by one, follow the guide.

To build our router, we need the following services as container:
- DHCP: to give network settings to our LAN clients
- DNSCrypt: to make encrypted DNS requests
- Unbound: as a DNS cache between the LAN and DNSCrypt
- OpenVPN: our main service, to establish a VPN connection with an external VPN server

We will see together how to setup each service configuration file(s) we downloaded before, and then we will see how to setup all containers in a single compose file at the end. For each service I have built and uploaded my images on Docker Hub. This will consequently be easy to pull them on our router to be up and running quickly.

This one is easy enough, you just have to setup basic information such as the network subnet range, gateway, and DNS server:
$ nano dhcpd.conf
option domain-name-servers;

subnet netmask {
option routers;

Please customise this file according to your subnet.

All DNScrypt configuration is passed to the daemon through parameters. However these parameters are taken from the .env file, which contains other variables as well. This .env file will in fact be used by Docker Compose we will talk about soon, to fill its Docker Compose file. This file is therefore vital to complete properly. Please update it to choose which DNSCrypt servers you want to use (last 3 lines), but also and above all your host LAN IP address and interface name (first 2 lines). Do not modify other lines unless you know what you are doing:
$ nano .env
# local variables used in Compose file



# Choose from

I am using three servers as DNSCrypt ones are often unavailable. They can be slow if you choose random ones far away from your current location. Choose closer servers for a better experience. The servers shown above are just an example.

Unbound is our local DNS cache, as DNSCrypt does not have one. It is configured to use the three DNSCrypt servers:
$ nano unbound.conf
   do-not-query-localhost: no
   access-control: allow
   logfile: "/dev/stdout"
   verbosity: 2
   hide-identity: yes
   hide-version: yes
   auto-trust-anchor-file: "/etc/unbound/trusted-key.key"
   root-hints: "/etc/unbound/root.hints"
   do-daemonize: no

   name: "."

You don't need to modify this file if you keep the default .env file settings for the containers networks.

Our main container here! The configuration will have to be done according to your VPN provider. With ProtonVPN, you can login with your account at ProtonVPN website, then go to the Download menu on the left, and choose Linux/UDP with the country or server you like, for instance:

1. Select platform
-> Linux

2. Select protocol
-> UDP

3. Select connection and download
-> Country Config : Switzerland (click the download link at the right in the same line)

Of course you can choose any country, server, or server core (two servers chained) you want. This will download an .ovpn file which includes your certificates. Open this file, and copy/past your certificates and TLS key where it is shown in openvpn.conf you have downloaded earlier. Likewise, copy/past the country selected:
$ nano openvpn.conf
dev tun
proto udp

# Your selected country or server below
remote 1194

resolv-retry infinite

cipher AES-256-CBC
auth SHA512
verb 3
tun-mtu 1500
tun-mtu-extra 32
mssfix 1450

# Privileges, chroot
user nobody
group nogroup

ping 15
ping-restart 0
reneg-sec 0
remote-cert-tls server

# Your confidential login and password below, in addition to your certificate and key.
# Do not forget to 'chown root:root' and 'chmod 600' this file
auth-user-pass "/etc/openvpn/auth.conf"

# This script is used to enable NAT inside the VPN container
# and set a network route for the traffic to go back to the LAN
script-security 2
up /etc/openvpn/

your certificate here

key-direction 1

# 2048 bit OpenVPN static key
-----BEGIN OpenVPN Static key V1-----
your static key here
-----END OpenVPN Static key V1-----

Please use the provided openvpn.conf file and just insert your certificate, static key, and chosen country or server. This openvpn.conf file has important settings not included in the official protonvpn .ovpn file, to fit perfectly in our dockerised VPN router :-) (init script, auth.conf referenced, daemon user and group). If you use another VPN provider, either insert your cert and keys in the one I provide, or use the file given by your provider and add the reference to the init script, auth.conf , and define user and group as shown above.

If you use ProtonVPN or any similar VPN provider, you will also need to fill the following file with your VPN login and password:
$ nano auth.conf
your login (first line)
your password (second line)

Ensure that only root can get access to it:
$ chown root:root auth.conf
$ chmod 600 auth.conf

Docker compose file
Now that every config file is ready, we need to build and start our containers, using these configuration files. This can be done with the docker command line, but Docker Compose enables us to fill a compose file, to reference all our containers with their variables and parameters, and to start them easily all at once. Below is such a compose file, that you already downloaded earlier. This file is using variables automatically from our .env file (below is an example, the file you have downloaded might be more up to date): (if you do not see the code above, please unblock in your AD blocker)

Some of the features included in this compose file:
- chains containers startup in correct order
- sets containers read-only when possible
- uses variables from '.env' file automatically
- uses images from my gkweb76 Docker Hub account

This is still not finished yet, as we will later use a script to pull all images, create volumes to store persistent data (conf files), copy our files to the correct places, and start our containers automatically. However before using this script, we must configure our firewall script first in our next chapter.

As for every router and VPN gateway, it is critical to control tightly our firewall rules. That's why we disabled earlier the possibility for Docker to add its own rules. However doing so makes things a bit more complicated, because containers being ran into their own network, they go through the FORWARD chain of iptables, and not only INPUT or OUTPUT chains. Usually on a router the FORWARD chain is used for the LAN traffic going out to the Internet or the other way around. The containers networks add a layer of internal forwarding between their interfaces and the host interface. Additionally, as the containers can be up or down, the firewall script has to detect the containers and load some rules only if they are running.

Before diving into our firewall script, we need to add a separate routing table to our router. Indeed, incomming LAN traffic by default will not be automatically directed to our OpenVPN container. As OpenVPN is not running on the host, the host default route is not the VPN tunnel. Consequently, we need to create a dedicated routing table redirecting traffic to our OpenVPN container. This table will then be used by the firewall script to send LAN traffic into it. Let's create an empty table first :
$ sudo nano /etc/iproute2/rt_tables.d/openvpn.conf
1 openvpn

Inside the firewall script, a routing rule is added:
ip rule add iif $lan table openvpn
ip route add default via $container_openvpn dev $docker_openvpn_if table openvpn

This adds a routing rule to redirect incoming traffic on the LAN interface, not destinated to a local address, to our separate routing table. In this table, we add a default route to our OpenVPN container. This way, all traffic from the LAN to the Internet is sent to OpenVPN.

Below is the whole firewall script tailored for this article's router:
(if you do not see the code above, please unblock in your AD blocker)

Then, to make sure this script starts at the router startup, on Ubuntu 18.04 LTS and probably beyond, you can do it like this:
$ nano firewall
$ sudo cp firewall /etc/init.d/
$ sudo chmod +x /etc/init.d/firewall
$ sudo cp firewall.service /etc/systemd/system/
$ sudo nano /etc/systemd/system/firewall.service
Description=Enable firewall rules after Docker containers are started



Enable the firewall service:
$ sudo chmod 644 /etc/systemd/system/firewall.service
$ sudo systemctl daemon-reload
$ sudo systemctl enable firewall.service

To edit your firewall script in an easy way, you can link your firewall script into your home directory:
$ ln -s /etc/init.d/firewall ~/

Once you are sure of your script, you can continue to the next step.

The long awaited final step is here! Every configuration file is ready, our firewall script is ready, our compose file is ready too. To deploy everything, the following steps are required:
- either create the container volumes manually, to store persistent data such as configuration files, or starts the containers and stop them (this creates the volumes as well)
- copy your configuration files to the volumes
- start your containers
- restart your firewall script

All of these steps are automated by the script:
(if you do not see the code above, please unblock in your AD blocker)

If you wrote your configuration files, env files, and everything else without any mistake, this will start everything up in one command:
$ sudo ./

The last modification is to make systemd resolver to use our local Unbound DNS service:
$ sudo nano /etc/systemd/resolved.conf
[Resolve] DNS= # replace by your local LAN IP address

Then reload systemd resolver:
$ sudo service systemd-resolved restart

Now you can test that your DNS resolution is working (replace by your LAN IP address):
$ nslookup
$ nslookup

It should work fine, but DNS written to netplan file are still used by the system and spams iptables DROP logs. Set your local LAN IP address as DNS:
$ sudo nano /etc/netplan/01-netcfg.yaml
    - ""

$ sudo netplan apply
$ systemd-resolve --status

Then check with that your IP seen is the VPN server you have chosen.

Troubleshooting if your setup is not working:
- check you did not miss any steps
- ensure that your .env file and firewall script both have correct interfaces and LAN IP address
- ensure your script, used inside OpenVPN container, has the correct LAN network declared
- ensure that your openvpn.conf and auth.conf file contains your correct certificates, keys, and credentials
- check your /var/log/syslog for iptables DROP
- check that your containers are actually started and healthy, and not in a restarting loop ( sudo docker container ls )
- test your DNS resolution with nslookup

Below is a recap of the files you must modify according to your network:
- .env
- firewall
- openvpn.conf
- auth.conf
- dhcpd.conf

Check again every file, and if you modify anything, you can either use the script, or these ones directly (as we do not need to create the existing volumes):
$ ./
$ ./

The first script will copy every configuration files to the correct containers volumes, and reload the containers. If you just modified one configuration file, the compose up command may not update your container as the Dockerfile and Docker compose file will not have been modified. In this case, you will have to restart your container. For instance if you modified openvpn.conf , you may have to:
$ ./
$ sudo docker container restart openvpn

If you want to check the ressources used by your containers, you can type:
$ sudo docker container stats

(extract, screenshot was too large)

Be curious, explore, read the official documentation, look and find by yourself.

To allow you to have an overwiew of the process, below are the steps to complete the Docker VPN router from this article:
1 - Install Ubuntu LTS
2 - Install Docker and Docker compose
3 - Download all config files with 'git clone', and modify them
4 - Setup your firewall script
5 - Start script to initialise and run the containers
6 - Modify local DNS settings

This is as simple as that. I have done this process multiple times, on VMs, and once on my physical router, and if all configuration files are correct it works from the first try.

As we have seen, Docker is a very handy tool to tailor our projects, and it fits our needs to build our home VPN gateway. Docker can be setup securely, and once prepared, allows us to build very fast a complex project. In the entreprise world, it can be even more convenient with a native cluster Swarm support, and eventually combined with an orchestration tool. Docker security is great, and combined with an hardened host and proper settings, it adds another layer of protection an attacker would need to overcome to reach the host OS. It does not 100% isolate the container from the host though, as the host kernel is shared, and because vulnerabilities exist. However such vulnerabilities exist too in VM Hypervisors, virtual isolation does not reach the same level as bare metal isolation (even that is not 100% safe, as clever specialised viruses can communicate between air gapped computers).

If the future of computing really becomes serverless, Docker clearly has a card to play. Docker is not the only container technology, other players are in the game. However Docker seems the more popular from what I see, and is actively supported and maintained.

Finally, I would like to thanks ProtonVPN for their service I fully trust, and for their transparency when they are under attack. Their service is great and secure, they offer software clients for Windows and Mac, and there is an app for Android (hopefully for iOS soon too). I would also like to thanks Bret Fisher for his wonderful Docker course Docker Mastery: The Complete Toolset From a Docker Captain which introduced me to the Docker world :-)

I hope this article has been useful to you, and that you enjoyed reading it.


Sunday, 30 April 2017


I did a previous article in 2015 about this very subject, and explained how to build your VPN server on a Virtual Private Server (VPS). It was based on OpenBSD 5.6 and Vultr VPS provider. Since then, VPN has remained a hot subject of interest for a lot of people, especially after learning about all of the spying around (NSA's leaks, Wikileaks, etc...). Around me I have seen more people starting to use a VPN, and I received some questions since my last article. The main need remains the same however: if you neither trust your ISP nor a dedicated VPN provider (log or security wise), your best option is to be your own VPN provider.

This new article to build your own VPN server is an upgrade of the previous one. At a glance, the upgrades are:
- Full Disk Encryption (FDE)
- OpenBSD 6.1 and new syspatch utility
- Comparison of OpenVPN and IPSEC
- Stricter firewall rules marking all inbound traffic to be blacklisted
- Scheduled Python script (instead of bash) to blacklist the intruders
- Separate CA/signing machine (optional)
- Multiple DNSCrypt proxy instances for failover
- OpenVPN: Certificate Revocation List/CRL (optional)
- OpenVPN: TLS 1.2 only
- OpenVPN: TLS cipher based on AES-256-GCM only
- OpenVPN: HMAC-SHA512 instead of HMAC-SHA1
- OpenVPN: TLS encryption of control channel (makes it harder to identify OpenVPN traffic)

The others OpenVPN's encryption parameters are identical: Diffie Helman 4096 bits, server's private RSA key of 4096 bits, TLS key of 2048 bits for HMAC, AES-256-CBC used for encrypting the traffic, and Perfect Forward Secrecy (PFS).

The steps described in this article have been made on a VPS from Vultr provider, with which I have no affiliation of any kind, I merely use it because it fits my needs: possibility to boot a custom ISO of any OS, console access, Two Factor Authentication with Yubikey, snapshots, and an affordable starting plan of 2.5$/month (512Mo RAM, 1 CPU, 20GB SSD, and 500GB of traffic). You are free to follow this guide and installation steps on any VPS provider of course! :-)

In what follows, I expect that you were able to boot on an OpenBSD 6.1 amd64 ISO, ready to install the OS. Also, as I did it on Vultr on a SSD VPS, drive letters will be specific to it. If you install your server on a mechanical HDD, instead of /dev/sd0a you will have /dev/wd0a, and instead of a RAID /dev/sd1 you will have /dev/sd0. Therefore you must adapt the commands from this article to your particular environement.


I have been asked which was better, the most secure, which one should we choose to make our VPN server. I do not intend to start a war as to which is best, and to make the suspens short, both are good. Both can be used to encrypt traffic with AES-256, they both can use certificates as well, and can work for remote "road warrior" nomade users, or mobile phones. Both use HMAC authentication for incomming packets, allowing them to drop "valid" IPSEC/OpenVPN packet which are however not signed. Both can be weaken by choosing weak encryption algorithm and/or weak passwords/keys. Fact is, I'm using both for professional use, it all depends on the context.

That being said, we can still find differences between both, which may help you choose.

IPSEC: has two easily identifiables UDP ports, 500 by default, and 4500 for NAT Traversal (NAT-T), and uses AH/ESP protocols. This means it can potentially be blocked voluntarily or unvoluntarily by an ISP or by a WIFI or hotel network. Also, my personal experience with IPSEC is that it is "sensitive" and can have packet loss when SA rekey timeout is reached, or when there is packet fragmentation. It can be difficult to diagnose when unexpected issues appear, such as packets not coming back despite VPN tunnel still being up, no timeout is reached, and no error messages from iked. On the other hand, OpenIKED on OpenBSD has native commands to create a CA and certificates, which is very handy. It enables us to choose from a wide selection of authentication and encryption algorithm, and the iked.conf syntax is rather easy. I find it generaly more suited for site to site VPN. Obviously IPSEC can be made perfectly stable and secure, however in my experience it requires more troubleshooting when something goes wrong.

OpenVPN: CA and certificates management require an external tool, either OpenSSL or a package such as Easy-RSA. About ports, a single one needs to be used, and we are free to choose the one we want, as well as the TCP or UDP protocol. OpenVPN is a SSL VPN and does not use a separate VPN protocol. This point allows you to circumvent potential network restrictions, as at worst you will always have at least ports 80, 443, and 53 available. OpenVPN has no problem with NAT and firewalls, and works flawlesly. In my experience, I found OpenVPN perfectly stable after 2 years of usage.

To be honest, when I started my new VPN server to make this article, I wanted to do it with IPSEC as iked was native to OpenBSD. I did manage to have a functional tunnel, however it never was stable no matter what I tried. It is very likely a configuration error on my side, but still it was pretty frustrating. When I finally went back to OpenVPN, I had it working at first try with a perfect stability. I personaly prefer OpenVPN, and it can also be configured to be pretty secure as well.

In my previous article, I did set up a scenario where everything was in your hands: the server, the encryption algorithm, the keys and certificates, the firewall rules. It was pretty secure, in part because of running on OpenBSD which enables a lot of security features by default. However, as some people mentioned it to me, if the VPS provider is either malicious, or hacked, or for any reason has to give access to your server or give a copy of it, all of your security falls flat. Indeed, your certificates, keys, and may be even the Certificate Authority, are all stored on the server, which is not encrypted. There is therefore in this scenario a required trust toward the VPS provider.

By doing a Full Disk Encryption (FDE), you are further hardening your VPN server from unauthorized access. Fortunately, FDE is natively supported on OpenBSD. From the official FAQ, OpenBSD uses AES in XTS mode. It is not specified if it is AES-128 or AES-256 though at this link. However I found a detailed analysis of OpenBSD encryption source code, and it clearly shows that AES-256 XTS is used.

Please note however that while it is an improvement to setup a FDE, and an absolute security requirement in my opinion nowadays, it is still not 100% secure as the VPS provider can do a snapshot of the running system and extract information from memory.


When you first boot onto the ISO to install OpenBSD, you end up on this screen:

We need to create an encrypted volume before proceeding with the installation. For that, we must escape to a Shell first (last option). The OpenBSD FAQ linked above advises to write random data first to the whole drive. Indeed, if we do not, it may be possible for an adversary to tell the difference between used and unused space. It costs nothing to do it, except some patience, and if we encrypt the whole drive it's better to do it right. As a reminder, drive paths from now on are based on a SSD of Vultr VPS, if you use a classical HDD, you will have to adjust the commands.
# dd if=/dev/random of=/dev/rsd0c bs=1m

This command reads data from /dev/random and writes it to the whole /dev/rds0c device. It takes 5 minutes on my server's 20GB SSD, but it can take a lot more time on bigger drives, especially on HDD. Now we need to initialise the disk and create label for it:
# fdisk -iy sd0
# disklabel -E sd0

Here, choose to create a partition using the entire disk, this way is easier. Notice we choose a "RAID" filesystem here, as it is what will allow us to enable encryption afterwards:
> a a
offset: [64]
size: [41929586] *
FS type: [4.2BSD] RAID
> w
> q
No label changes.

Now we will create an encrypted volume. By default, from the bioctl man page, there is 16 rounds of the KDF algorithm used when converting your passphrase into key. This is a defense against bruteforce, as it requires, for every passphrase to try, to make 16 rounds of it. It increases required CPU power, and will increase the time necessary to crack the passphrase. However, 16 rounds, althought conservative and being able to run on even less powerful systems, is not that much against someone having great CPU power to attack your passphrase. On a decent and recent system with fast SSD, upping it to 8192 rounds is transparent with no noticable delay after having typed the passphrase to boot the server (on my VPS server). However if I test it on a local VM on my computer, boot is delayed by 55 seconds, so it really depends on your hardware.

Choose here a strong passphrase! Having a high number of rounds with a weak passphrase is useless. If in the future you want to modify your passphrase, from a user shell you will have to type this command:
$ doas bioctl -P sd1

Before coming back to the setup, as we created a new sd1 device, we have to make sure the nodes are created for it:
# cd /dev
# sh MAKEDEV sd1

Last step, also from the official FAQ, is to zeroing the first chunk of the new device, where should normally be a Master Boot Record and disklabel instead of some garbage:
# dd if=/dev/zero of=/dev/rsd1c bs=1m count=1
# exit

Now you come back to the setup prompt, with an sd0 device filled with an sd1 encrypted volume. You just need to answer the questions, essentially:
Initial prompt : I (Install)
Keyboard layout: fr (for instance)
Hostname : MY-SERVER
Network : keep defaults and dhcp
Root password : choose a strong password, different than the boot passphrase
Start sshd : yes
Run X Window : no
Setup a user : no
Allow root ssh login : no
Timezone : choose your own (e.g Europe/Paris)

We do not create another user for now, and we deny root from connecting to SSH. This implies that for the begining of the setup which follows, you have to have an access to the server's console, that Vultr for instance provides.

Be carefull when the disk to install to is asked for, remember to change the default from the unencrypted device to the encrypted volume:
Which disk is the root disk? ('?' for details) [sd0] sd1

Then, for the following questions:
Use Whole disk : whole
Use Auto layout : a
Location of sets : http
Package selection : -g* [enter]
Package selection : done
Location of sets : done
# reboot


When you want to upgrade an existing system, always first read the official upgrade guide pertaining to your upgrade (here from 6.0 to 6.1). For instance, before upgrading from OpenBSD 6.0 to 6.1, some cleanup have to be made first, before booting onto the new version ISO.

If you already have an encrypted server, or if you want to know how you will update it when the next OpenBSD will be released, the install steps are different. The obvious way seems to boot onto the ISO, at the setup prompt drop to a shell, mount the encrypted volume, and continue with the setup. That is in fact correct, but the bioctl manpage, althought having a parameter to "detach" a volume, does not mention how to "attach" or "mount" an existing one. The parameter to use, althought not explicitely written in the manpage, is the "-c" parameter we used to "create" our volume. If the volume already exists, bioctl will just mount it, not overwrite it:
Once the volume is attached as sd1, as before we need to create nodes for it:
# cd /dev
# sh MAKEDEV sd1

Then quit shell with CTRL+D, and at the setup prompt select "(U)pgrade". Once done and your upgraded server is rebooted, follow the post-upgrade steps given at the official upgrade guide.

OpenBSD default settings are very secure, and no services are listening on the outside except SSH. However SSH is listening on the default 22 port, accepting password authentication. Before configuring our server, it is best to block any inbound access except from our computer public IP, and then take our time to lock down SSH. Let's start by a basic pf ruleset:
# vi /etc/pf.conf
block in quick from ! x.x.x.x # your public IP address
pass out quick

Replace "x.x.x.x" by your computer public IP address you are connecting from. You can check on if you don't know it. Of course this ruleset is temporary. Now apply it:
# pfctl -f /etc/pf.conf

Now we will create a regular user, that we will use afterwards. I'm creating a user named "guillaume" just for the example, but pick the one you want:
# adduser

First time this command is ran, some general questions are asked, keep the defaults. Then when a username is asked to create a user, enter yours, and keep the defaults for all other questions regarding your user. When that is done, we want to be able to run commands as root, in same way we used "sudo" in previous OpenBSD versions. Since 5.8, we have to use "doas" instead, which has an easier configuration syntax:
# vi /etc/doas.conf
permit persist guillaume as root

The "persist" option makes doas command request the password the first time, and then not asking it again for a period of time. If you do not use this option, a password is requested everytime doas command is used (which will makes you crazy while configuring a server!). Now, logout from root, and try your fresh new user and enjoy the doas command from now on :-) Of course this configuration is only temporary, afterwards you should restrict the commands your user is allowed to do. You can check the doas.conf manpage for more information.

We now have few things to do: create an SSH key, disable root login and password base authentication, and make SSH listening on another port as a bonus (to avoid automated scans adding "noise" to our logs). For Linux/BSD clients you can create a key with -t ed25519, but for Windows your SSH client may only be compatible with keys created using -t rsa. In the following I will suppose you have a Windows client.
$ cd /home/guillaume
$ ssh-keygen -t rsa

Then copy your public key to ~/.ssh/authorized_keys :
$ cd .ssh
$ cp authorized_keys

Copy the content of your private key "~/.ssh/id_rsa" (not on the remote computer you will use to connect to your router, and set strict permissions on it. If on Linux or BSD, do a "chmod 600", and you will be able to connect later with "ssh -i your_private_key your_server_ip". On Windows, I advise you to run a recent client such as Royal TS to SSH into your server. This software is free under 10 connections, but a licence is needed if you have more. A regular Putty could do the trick, but last time I tried it would not connect (with the regular ppk file converted with puttygen), and RoyalTS has some additional interesting features such as encrypting the database, multi-tab window, custom macro, and the absolutely vital possibility to select custom icons :-)

Now modify your SSH server with this temporary configuration, the TCP port 21598 being an example, choose the port you want. You can choose a higher port to avoid scans, or choose a port such as 443 to be able to connect to your server from everywhere, when outbound TCP port 22 could be blocked:
$ doas vi /etc/ssh/sshd_config
# Modify default listening port
Port 21598

# Authentication
PasswordAuthentication yes # temporary
PermitRootLogin no
AllowUsers YOUR_USER
AuthorizedKeysFile .ssh/authorized_keys
AllowTcpForwarding no
UsePrivilegeSeparation sandbox # Default for new installations.
Subsystem sftp /usr/libexec/sftp-server

Restart sshd :
$ doas rcctl restart sshd

Now you can connect by SSH with your user and its password, and then retrieve your id_rsa private key file. You will then need to convert it to a ".ppk" file to be able to use it to connect from Windows. One easy way to do it is to use puttygen. Just run this utility, open your id_rsa file from it, and convert it into id_rsa.ppk.

Connect from your remote computer with the private key, check that connecting with your regular user and with the SSH key fully works. Once it's working, modify the following line in /etc/ssh/sshd_config :
PasswordAuthentication no

Restart sshd :
$ doas rcctl restart sshd

On the client side, if you are using a Linux/BSD client, you can enable SSH key fingerprint visual display in /etc/ssh/ssh_config which displays your SSH key in hex format and an ACSCII graphic everytime you connect. Once you get used to the ASCII graphic of your server, you should notice if all of a sudden it is completely different (probably a man-in-the-middle):
$ doas vi /etc/ssh/ssh_config
# Display fingerprint in hex and ASCII graphic when connecting
VisualHostKey yes

You now have a SSH listening on a non default port, root denied from connecting in, password authentication disabled, and authencation based on SSH keys and passphrase. For further SSH hardening, read the following excellent article describing which protocols and ciphers to use for optimum security. I may in the future update the SSH settings given in this article to follow some of the advices from this page.

Finally, to update the system and packages easily, I really liked to use "openup" from M:Tier. However, starting with OpenBSD 6.1, a new "syspatch" command appears to easily update the base system. It won't take care of the additinal packages however, which have to be taken care of with the ports tree if you want to rely only on OpenBSD official commands and repositories. You can learn more details at the patches FAQ. "pkg_add -u" only upgrades packages if you are running the -current flavor, not the -release flavor we are running in our example. If you want the easiest way you can use openup from M:Tier, which is a third party not related to OpenBSD project, so you have to decide if you wish to trust their repository or not. If you want to use "openup":
$ ftp
$ chmod +x openup
$ doas openup
===> Checking for openup update
===> Installing/updating binpatch(es)
===> Updating package(s)

If however you want to use syspatch instead as I do, start it this way, and then update the external packages using the ports tree:
$ doas syspatch

To fetch the ports tree, if you don't use openup, you can follow the steps outlined in the patches FAQ link given above. Basically fetch the ports tree with CVS, run the "out-of-date" script to know which package needs an update, and run a "make update" in the affected port directories. I'll try to detail these steps later.

Here we go, a fully updated system. If you use the syspatch option "-c" it will list available updates. You can read syspatch manpage for more information. Let's continue to the next part.

Before going further, as "vi" is not my favorite text editor, let's install "nano" instead:
$ doas pkg_add nano

A few tweaks we can make before configuring further our server. As Vultr VPS are hosted on SSD, it is a good idea to add in the fstab file the mount options "softdep" and "noatime". The first one increase disk performance, while the second will prevent the "last access time" file properties to be written:
$ sed 's/rw/rw,softdep,noatime/g' /etc/fstab > ./fstab_tmp
$ doas mv fstab_tmp /etc/fstab
$ cat /etc/fstab

Now, remember we are running on a fully encrypted volume. However OpenBSD also encrypt the swap by default, therefore you can disable it as it is not necessary in our context:
$ doas nano /etc/sysctl.conf
# Disable swap encryption (whole disk is already encrypted)

Your server has booted with DHCP and has acquired its network configuration from your VPS provider. As the server IP address is fixed, I prefer setting manually the network configuration to avoid relying on another server (VPS provider's DHCP and DNS) I do not control. Modify the following files according to your server public ip address, mask, and gateway:
$ doas nano /etc/hostname.vio0
inet server_public_ip server_netmask

Add the gateway :
$ doas nano /etc/mygate

Our server will forward traffic between its VPN interface and its default network interface. We have to enable forwarding:
$ doas sysctl net.inet.ip.forwarding=1
$ doas nano /etc/sysctl.conf
# Enable forwarding

At this point you still rely on your provider DNS server in /etc/resolv.conf, we will take care of that later.

The pf ruleset below does many things:
- denies all inbound, except SSH and OpenVPN on non default ports
- protects SSH from SYN flood, and bruteforce
- detects accesses to all other ports and blacklist the miscreants for 24H (you cannot expect less from a Blowfish mascot!)
- allows VPN clients to make DNS requests to unbound on localhost (which uses dnscrypt, as it will be setup in the next part)
- does not log blocked network traffic from the provider's DHCP

You should modify this ruleset according to your chosen SSH and VPN listening ports (we will setup OpenVPN later, but you can choose a random port now). The scan detection and blacklisting concept can theoretically backfire, if someone sends spoofed packets with trusted IPs. However, as it is implemented, only incoming traffic will match the blacklist table. Therefore, if a spoofed packet is sent with a website IP you trust, it will not prevent you to access that website at all as your traffic will be outbound. Also, as we will write a script to parse pf logs and add IPs into the blacklist, we will add an exception for our own trusted client computer public IP address to avoid being locked out. As a last resort the remote console access from the VPS provider could allow us to login, if we were totally locked out for any reason. This adaptative behavior is not mandatory for your VPN server to be operational, but it is useful to block automatic scans on the Internet from querying your SSH and VPN ports once they hit your server on another port.

$ doas cp /etc/pf.conf /etc/pf.allow.conf
$ doas nano /etc/pf.conf
# Title: "Being your own VPN provider with OpenBSD v2"
# Author: Guillaume Kaddouch:
# Date: ruleset last modified: 2017 April 17 for OpenBSD 6.1
# Github:

# ---------------------------------------------------------------------------------------
ssh_port="21598" # just a random example, modify to match your chosen SSH port
vpn_port="21599" # just a random example, modify to match your chosen OpenVPN port

bad_ports="{ 1:66, 69:21597, 21600:65535 }" # adjust according to your SSH and VPN ports (+ DHCP)

table <internet> const { $all_networks, !self, !$private_networks }
table <myself> const { self }
table <bruteforce> persist
table <badguys> persist

# ---------------------------------------------------------------------------------------
set block-policy drop
set loginterface egress
set skip on lo
block log all
match in all scrub (no-df max-mss 1440 random-id)
block in log quick from <bruteforce> label "bruteforce"
block in log quick from <badguys> label "old_guys"

# --------------------------------------------------------------------------------
match in on egress proto tcp from <internet> to port $ssh_port
match in on egress proto { udp tcp } from <internet> to port $bad_ports
match in on egress proto udp to port $vpn_port
match in on $vpn proto { icmp udp tcp }
match in on $vpn proto { udp tcp } to $vpn_ip port domain
match out on egress tagged VPN_TUN_IN
match out on egress proto tcp from <myself> to port { http https }
match out on egress proto { udp tcp } from <myself> to port domain
match out on egress proto udp from <myself> to port https
match out on egress proto udp from <myself> to port ntp
match in on egress from { no-route urpf-failed } to any
match out on egress from any to no-route
match inet6 all
   tag SSH_IN
   tag BAD_GUYS
   tag VPN_TUN_IN
   tag VPN_DNS_IN
   tag HTTP_OUT
   tag DNS_OUT
   tag DNS_OUT
   tag NTP_OUT
   tag IPV6

# ---------------------------------------------------------------------------------------
match in tagged VPN_EGRESS_IN set tos lowdelay set prio 6
match out tagged VPN_FORWARD nat-to (egress) set prio 6

# Blocking spoofed or malformed packets, IPv6, and some bad traffic
antispoof log quick for egress label "antispoof"
block quick log tagged BAD_PACKET label "noroute_urpf"
block quick log tagged IPV6 label "ipv6"
block quick log tagged BAD_GUYS label "new_guy"

# Standard rules
# protect SSH from SYN flood and bruteforce
pass in quick tagged SSH_IN synproxy state \
(max-src-conn 10, max-src-conn-rate 5/5, overload <bruteforce> flush global)

# Redirect VPN clients DNS requests to unbound
pass in quick inet tagged VPN_DNS_IN rdr-to port domain

pass in quick tagged VPN_EGRESS_IN
pass in quick tagged VPN_TUN_IN

pass out quick tagged HTTP_OUT
pass out quick tagged DNS_OUT
pass out quick tagged VPN_FORWARD modulate state
pass out quick tagged NTP_OUT

# no log for
block in quick proto udp from port bootpc to port bootps
block in quick proto udp from any port bootps to port bootpc

If you prefer, you can now fetch this ruleset from this Github link.

Check the ruleset syntax, with the first command, and then apply it if no error returned:
$ doas pfctl -nf /etc/pf.conf
$ doas pfctl -f /etc/pf.conf

Now we have to make our script which will look for IPs blocked, because they dare approaching our server, labeled "new_guy". Script will be in /home/user/scripts:
$ doas pkg_add python3.5
$ cd ~
$ mkdir scripts
$ cd scripts
$ doas nano ./
If you do not see the embedded source code frame above (you are blocking scripts from, you can access with this direct link the script source code.

Edit the crontab to schedule execution of this script, as well as check every hour to expire table entries older than 24 hours. Be sure to make the script readable, executable, and writable only by root, as it will run in root's crontab. Replace "/home/your_user" below with your username:
$ doas chown root:wheel
$ doas chmod 700
$ doas crontab -e
# add badguys to the pf table to be blocked
*/5 * * * * python3.5 /home/guillaume/scripts/

# Clear pf tables
0 * * * * pfctl -t bruteforce -T expire 86400
0 * * * * pfctl -t badguys -T expire 86400

You can check with another script the state of your blacklist/badguys table, ports blocked to them once blacklisted , and number of blocked IP in your bruteforce table. Here is the script example, you can freely modify:
$ doas nano ./
If you do not see the embedded source code frame above (you are blocking scripts from, you can access with this direct link the script source code.

Below is also an example of a possible output from the second script:
$ doas chown root:wheel
$ doas chmod 700
$ doas ./

You now have a nice adaptative firewall, however as warned above, be careful as it can backfire if trusted IPs are not excluded and become blacklisted. It is a working configuration I had no trouble with, in my context, but in your situation it may have to be modified to fit your case.

6. DNS
We will use DNSCrypt to make our DNS requests encrypted, and Unbound to have a local DNS cache. This will allow us to avoid using our VPS provider DNS servers, and will also be useful to your future VPN clients which will be able to use your VPN server as their DNS server too, if they wish too (e.g mobile phones). Both dnscrypt and unbound will listen on the localhost only, not to the outside. They will be reachable nonetheless later to your VPN clients trough the VPN tunnel, using a firewall redirection.

$ doas pkg_add dnscrypt-proxy
$ doas rcctl enable dnscrypt_proxy
$ doas rcctl set dnscrypt_proxy flags -a -l /dev/null -R

You can choose your dnscrypt enabled DNS server at the following list (choose a logless DNSSEC enabled one).

Now let's start it:
$ doas rcctl start dnscrypt_proxy

Before configuring Unbound, which is the local DNS cache which will make requests to dnscrypt_proxy, we can configure an additional dnscrypt instance, as explained in the pkg readme. Indeed, dnscrypt DNS servers being public ones, they often goes into maintenance, become offline or temporarily unreachable. To address this issue, it is possible to setup multiple dnscrypt instances. Below are the steps to follow to add one, but you can add more if you wish. Notice the different local port 41 and different DNS server:
$ doas ln -s dnscrypt_proxy /etc/rc.d/dnscrypt_proxy2
$ doas rcctl enable dnscrypt_proxy2
$ doas rcctl set dnscrypt_proxy2 flags -a -l /dev/null -R
$ doas rcctl start dnscrypt_proxy2
$ ps aux | grep dnscrypt

You should see now two dnscrypt processes running, listening on ports 40 and 41, and linked to a different remote DNS server. This setup will be reflected in Unbound configuration below.

We now configure and enable unbound, already included in the base system. Unbound will drop privileges to user _unbound and will be chrooted in /var/unbound by default. Also by default, only localhost is allowed and everything else is refused. This is why it is unnecessary to specify the username/directory/chroot options below, or to define a default access-control. We just add what we need:
$ doas nano /var/unbound/etc/unbound.conf
do-not-query-localhost: no
access-control: allow
hide-identity: yes
hide-version: yes
auto-trust-anchor-file: "/var/unbound/db/root.key"

name: "." # use for ALL queries
forward-addr: # dnscrypt-proxy
forward-addr: # dnscrypt-proxy failover

Do not forget to modify your /etc/resolv.conf:
$ doas nano /etc/resolv.conf
nameserver # unbound is listening there at port 53

To prevent dhclient from overwriting our nameserver in resolv.conf, add this line to dhclient.conf:
$ doas nano /etc/dhclient.conf
supersede domain-name-servers

Run Unbound, and enable it to launch at startup :
$ doas rcctl enable unbound
$ doas rcctl start unbound

Now that the outbound DNS requests are only made using dnscrypt on UDP port 443, we can comment/disable/remove the rule that allows outbound DNS requests on UDP/TCP 53:
$ doas nano /etc/pf.conf
#match out on egress proto { udp tcp } from <myself> to port domain tag DNS_OUT

$ doas pfctl -nf /etc/pf.conf
$ doas pfctl -f /etc/pf.conf

Test that your DNS chain is working:
$ host has address mail is handled by 6 mail is handled by 10

Unbound is listening on locahost port 53, and when contacted is forwarding to dnscrypt listening on locahost port 40, itself contacting an external dnscrypt enabled DNS server.

We need to create a Certificate Authority (CA) which will enable us to create certificates for our VPN server, our home router, and any other client we may have. A Certificate Authority will also be able to revoke certificates, which will prevent unused or lost ones to be accepted by our VPN server.

This CA, as you can see, has a critical role. A breach of the CA would means an attacker could steal certificates and private keys and spy on our communications, and could create certificates for himself. Therefore, I strongly advise you to create a dedicated CA server, that you can store and run on your local computer as a virtual machine for instance that you can start on demand. It is possible though to follow the steps below directly on your VPN server, but I do not recommend it.

Ideally, from a dedicated OpenBSD 6.1 CA host:
$ doas pkg_add easy-rsa
$ cd /usr/local/share/easy-rsa/
$ doas cp vars.example vars
$ doas nano vars
# 2048 - > 4096
set_var EASYRSA_KEY_SIZE 4096
# sha256 -> sha512
set_var EASYRSA_DIGEST "sha512"

Now it's time to create our new Public Key Infrastructure (PKI) and CA :
$ doas easyrsa init-pki
$ doas easyrsa build-ca

Choose your CA's passphrase wisely, preferably a strong one, it will be requested for every subsequent certificates you will want to create.

Then, from easy rsa readme, the recommanded steps to create certificates is to install easy-rsa on every requesting hosts, server and clients, generate a request with "./easyrsa init-pki" and "./easy-rsa gen-req", import it on the CA server, and sign them. Then transport the newly created certs to each hosts. However this is not always doable, for instance if the client is a mobile phone.

If you prefer to do it all at once on the CA signing server directly:
$ doas easyrsa build-server-full your-server-name nopass
$ doas easyrsa build-client-full your-home-router-name nopass

The "nopass" option creates certificates without password, to allow openvpn to start automatically with the certificate without additionally prompting for a password. Then, we will create a Certificate Revocation List (CRL). This is optional, but I highly recommend it, as it will enable you to revoke certificates in the future if you need it. Without it, if you loose a certificate (stolen phone) or if you want to blacklist a certificate for any reason, the only possibility will be to create a whole new PKI and CA, and recreate all certificates. Better be safe than sorry and create a CRL:
$ doas easyrsa gen-crl

This will create the file /usr/local/share/easy-rsa/pki/crl.pem

Now we have to build 4096 bits Diffie-Hellman parameters, be warned it can take a while!
$ doas easyrsa gen-dh
$ doas mv ./pki/dh.pem ./pki/dh4096.pem

You can additionally generate an OpenVPN Pre-Shared Key (PSK) that will have to be copied to all server and clients:
$ doas pkg_add openvpn
$ doas mkdir -p /usr/local/etc/openvpn/secret
$ cd /usr/local/etc/openvpn/secret
$ doas openvpn --genkey --secret ta.key

On server, we will need to copy :
- ca.crt : Root CA certificate [.../easy-rsa/pki/]
- crl.pem : Certificate Revocation List [.../easy-rsa/pki/] (optional)
- dh4096.pem : Diffie Hellman parameters [.../easy-rsa/pki/]
- server.crt : Server Certificate [.../easy-rsa/pki/issued/]
- server.key : Server Key (private) [.../easy-rsa/pki/private/]
- ta.key : OpenVPN TLS PSK [/usr/local/etc/openvpn/secret]

On clients, we will need to copy:
- ca.crt : Root CA certificate [.../easy-rsa/pki/]
- client.crt : Client Certificate [.../easy-rsa/pki/issued/]
- client.key : Client Key (private) [.../easy-rsa/pki/private/]
- ta.key : OpenVPN TLS PSK [/usr/local/etc/openvpn/secret]

This way, your CA private key stays on your signing host, if you choosed a separate host as advised.

To transfer the required files to the aformentioned hosts, server and clients, an encrypted channel must be used. If you have network access from your signing host to your server and clients, you can do it directly with SCP. Or if you prefer doing it from an intermediate Windows machine, you can use WinSCP to connect simultaneously to the signing machine, server, and client, and transfer the files this way.

8.1 - SERVER

Now that our server is up and running, and our CA and certificates are created, we can finally setup OpenVPN on our server. Let's download our packages and create the required directories:
$ doas pkg_add openvpn
$ doas mkdir -p /usr/local/etc/openvpn/{public,secret}

Do not forget to copy your certificates and keys, by changing directory to the folder you copied them to earlier:
$ cd folder_where_certs_and_keys_are
$ doas mv *.crt /usr/local/etc/openvpn/public/
$ doas mv *.key /usr/local/etc/openvpn/secret/

Making sure these secret files are only accessible to root:
$ doas chmod -Rf 600 /usr/local/etc/openvpn/secret/
$ doas chown -Rf root:wheel /usr/local/etc/openvpn/secret/

Let's create the main OpenVPN server configuration file:
$ doas nano /usr/local/etc/openvpn/server.conf
# Server configuration
# SSL/TLS certificate and keys, PFS enabled by default

# Since OpenVPN 2.4, "tls-crypt" can be used instead of "tls-auth", as it does authentication and
# encryption of all control channel packets. Purpose of encrypting these packets: provides more
# privacy, makes it harder to identify OpenVPN traffic, and masks the pre-shared key
# (if I understand the official OpenVPN 2.4 manpage)

# ** WARNING ** as of 28 May 2017, "OpenVPN Connect" v1.1.1 app on iOS/iPhone does not
# yet support "tls-crypt" option. Consequently, if you have mobile phone clients,
# you should use "tls-auth /your_path/ta.key 0" instead on the server.

ca "/usr/local/etc/openvpn/public/ca.crt"
crl-verify "public/crl.pem" # Certificate Revocation List (optional)
cert "/usr/local/etc/openvpn/public/server.crt"
dh "/usr/local/etc/openvpn/public/dh4096.pem" # Diffie Helman 4096 bits
key "/usr/local/etc/openvpn/secret/server.key" # RSA 4096 bits
tls-crypt "/usr/local/etc/openvpn/secret/ta.key" # TLS 2048 bits for HMAC and encryption

# Protocols and ciphers
cipher AES-256-CBC # AES 256 bits
tls-version-min 1.2 # Only allow TLS 1.2
tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384
auth SHA512 # HMAC-SHA512 (default is SHA1)

# Network parameters
port 21600 # as an example, pick your own port
proto udp
dev tun
ifconfig-pool-persist ipp.txt
push "redirect-gateway def1 bypass-dhcp"

# push our DNS server to clients accepting it (will not override a home router DNS configuration
# with fixed DNS settings). Usefull for mobile phones for instance, where installing
# dnscrypt requires a rooted phone
push "dhcp-option DNS"

keepalive 10 120
comp-lzo yes

# Limits
max-clients 5 # change this value if you plan on connecting from more clients

# Privileges, chroot
chroot /var/openvpn
user _openvpn
group _openvpn

verb 4
mute 20

Now we should prepare the chroot environnement, as OpenVPN will chroot itself after being started. We must copy the CRL file in the chroot folder as it is checked after OpenVPN has chrooted its process, making the initial "/usr/local/etc/openvpn/" directory not accessible. That's also why the CRL path in the previous configuration file is not a full path, as the root folder changes after initilization:
$ doas mkdir -p /var/openvpn/{tmp, public}
$ doas cp public/crl.pem /var/openvpn/public

It is time to start our server and to make it start at boot time:
$ doas /usr/local/sbin/openvpn --config /usr/local/etc/openvpn/server.conf --daemon
$ doas nano /etc/rc.local
# OpenVPN
/usr/local/sbin/openvpn --config /usr/local/etc/openvpn/server.conf --daemon

You should check with tail -f /var/log/messages   that OpenVPN started successfully. If something went wrong, check your files and folders ownership and rights. Make also sure that both client and server time are close enough, to avoid any trouble with your certficates.

8.2 - CLIENT

In my OpenBSD router article there is a chapter about an OpenVPN client installation and configuration. Below I will just provide the OpenVPN configuration file to use on the client side:
$ doas nano /usr/local/etc/openvpn/client.conf
# Client configuration (router, computer)
# SSL/TLS certificate and keys
ca "/usr/local/etc/openvpn/ca.crt" # public
cert "/usr/local/etc/openvpn/myhome.crt" # public
key "/usr/local/etc/openvpn/myhome.key" # secret
tls-crypt "/usr/local/etc/openvpn/ta.key" # secret (OpenVPN 2.4 or higher)

# Protocols and ciphers
cipher AES-256-CBC # AES 256 bits
tls-version-min 1.2 # Only allow TLS 1.2
tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384
auth SHA512 # HMAC-SHA512 (default is SHA1)

remote-cert-tls server
dev tun
proto udp
resolv-retry infinite
comp-lzo yes

remote YOUR_SERVER_IP 21600

# Privileges, chroot
user _openvpn
group _openvpn
chroot /var/empty

verb 3
explicit-exit-notify 5

Client keys must be copied from the CA server to the client over a secure channel, for instance by using SCP/WinSCP to transfer them trough SSH, as advised earlier.


A smartphone such as Android or iOS can download and install "OpenVPN Connect". Then you will have to transfer on the phone an OpenVPN configuration file, which will require to have the whole configuration in it, including certificates and keys. The configuration file has the certificates and keys inside it. Basically you just copy/paste the content of the required files and insert them between tags like below (as of 28 May 2017, "OpenVPN Connect" v1.1.1 app on iOS/iPhone does not yet support "tls-crypt". We should use tls-auth on both client and server) :
# Protocols and ciphers
cipher AES-256-CBC # AES 256 bits
tls-version-min 1.2 # Only allow TLS 1.2
tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384
auth SHA512 # HMAC-SHA512 (default is SHA1)

# Global options
remote-cert-tls server
dev tun
proto udp
resolv-retry infinite
comp-lzo yes

remote YOUR_SERVER_IP 21600

key-direction 1

verb 3
explicit-exit-notify 5
# ca.crt below, just a random example. Full extract is above 35 lines

# myphone.crt below, just a random example. Full extract is above 35 lines
Version: 3 (0x2)
Serial Number: 4 (0x4)
Signature Algorithm: sha512WithRSAEncryption

# myphone.key below, just a random example. Full extract is above 35 lines

# ta.key below, just a random example. Full extract is above 20 lines
# 2048 bit OpenVPN static key
-----BEGIN OpenVPN Static key V1-----
-----END OpenVPN Static key V1-----

This configuration file, you can name "myserver.ovpn" for instance, must be transfered into the phone via a secure channel. As I'm using SpiderOak I used the Hive folder (which is a synchronized folder among many clients/devices) to retrieve the file. However, once you copied with SCP the phone keys and certificate on your computer, I guess a classical USB transfer will do the job as well :-)

Your VPN server is now finished!

We have seen how to build an OpenVPN server based on OpenBSD with Full Disk Encryption, to benefit from OpenBSD's memory protection, randomness implementation, LibreSSL, and secure by default philosophy. I find the VPS server to be a cost effective way to build our own VPN server, with many benefits such as snapshot before an upgrade, full access to boot the wanted ISO, remote console access, datacenter country location choice, and Two-Factor authentication with Yubikey. It is a way to be in full control of your computer, your home router, and your VPN server at the other end.

I used previously systrace which was removed starting from OpenBSD 6.0, because an application could circumvent it. Systrace was replaced by Pledge which is aimed at being more secure, but it works differently. Now with pledge, programmers have to include pledge API calls inside their code to be able to benefits from Pledge. Basically an application can request a mode allowing it to open files, or manage memory, but as soon as it will try for instance a network action not previously requested, the process will be killed. Theo de Raadt himself presented this new security feature at Hackfest 2015, you can find in the following links the slides and the video. A lot of base programs are already pledge protected: cat, chmod, mkdir, tar, gzip, ping, ftp, doas, nc, openssl, tcpdump, ntpd, httpd, smtpd, etc... From this 2015 slide, more than 400 programs were converted in 6 months. Apparently however, if I'm not mistaken, OpenVPN is not yet in the list unfortunately.

Also, in my previous article I talked about Security vs Anonymity, I won't include it here but you can jump to this link if you never read it.

In the end, there is still benefits of using an external VPN provider such as AirVPN. You may spend less money yearly depending on your required traffic volume, and spend less time as you do not have to maintain a server or upgrade it.

By using an external VPN provider you choose convenience, whereas by managing your own server you prioritize control and trust. Make your choice!