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.


No comments:

Post a Comment