Recently, I wanted to setup a private development environment for my home. Public places like slack, github, etc. are great for open source development and enterprise teams. It can be expensive to pay for some of their services though if you are on a small team or do side work here and there. For me, spending a few hundred dollars on a cheap computer (I went with a NUC), allowed me to setup all the services I needed for a small team that would quickly pay for itself. Over the next several posts I’ll explain how I did it all with open source software and docker. I was able to get up a git repo (gogs), a CI system (drone), and much more running in just a few days.

One note before we get started, I did all of this on an Arch Linux system so many of the tools are recent versions. For example, I used Docker 1.10.1 which may have features that other linux distro’s may not have with their canned docker packages. If you have problems running some of the commands, try installing the newer versions of the tools or adapting the commands to fit your version.

Let’s start with setting up OpenVPN. I wanted to be able to get access to the various services I’d be setting up remotely without exposing them to the greater public. It turns out others have already done some great work in simplifying this process. Much of my work below is adapted from the instructions at kylemann/openvpn’s docker hub page.

Server Initialization

The first thing we want to do is create a docker volume to store our configuration and key data.

docker volume create --name openvpn-data

This will make it simpler to backup your data (as we’ll see in a later post) as well as keep it separate from the actual container. With that done, we can initialize the configuration file as well as the certificate store.

docker run -v openvpn-data:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig -u tcp://HOSTNAME
docker run -v openvpn-data:/etc/openvpn --rm -it kylemanna/openvpn ovpn_initpki

Obviously, you’ll want to change HOSTNAME with the external DNS name that will be used to connect to this server. Also, in my case, I used TCP because OpenVPN seemed to be mistaking packets that are resent over the cellular network as a replay attack. Some googling suggested that configuration changes could fix this, but I felt like just using TCP would be fine for my case.

Server Configuration

Before you start the service you may want to modify the configuration information. You can do this by creating a docker container that has your favorite editor and including the volume for the openvpn-data or you can simply edit the files in _/var/lib/docker/volumes/openvpn-data/data/. In the file _ovpnenv.sh, you’ll want to make a few changes:

declare -x OVPN_CN="HOSTNAME"
declare -x OVPN_PROTO="tcp"
declare -ax OVPN_ROUTES='([0]="X.X.X.0/24")'
declare -x OVPN_SERVER="X.X.X.0/24"
declare -x OVPN_SERVER_URL="tcp://HOSTNAME"

Replace X.X.X with the subnet of your local network and HOSTNAME with the external DNS entry. Next, you can modify the openvpn.conf to suit your needs. Here were some notable changes I made:

server X.X.X.192 255.255.255.192
user nobody
group nogroup
push dhcp-option DNS X.X.X.1
push route X.X.X.0 255.255.255.0

Again, replace X.X.X with your local subnet. I chose the server IP and mask because I have my local DHCP server setup to give local addresses between 2 and 192. Using this server configuration, openvpn will setup a tunnel on the address X.X.X.193 and give IP Addresses between 194 and 255.

If you have a different DNS server, you’ll want to change the value above, but my local DNS server is the same as my router and yours is probably the same. This allows me to setup things like git.HOSTNAME locally and have it resolve when connected to the VPN. The route push is also important because it tells the client to send all traffic destined for our local subnet to use the VPN.

IP Forwarding

One thing to keep in mind is that many systems don’t allow IP packets to be forwarded, which means openvpn won’t work. You can enable this using sysctl but systemd won’t adhere to it. If you are using systemd, you may have to enable it explicitly. For me, I created the following file and restarted networking:

# cat /etc/systemd/network/eno1.network
[Match]
Name=eno1

[Network]
DHCP=yes
IPForward=yes

Running

Ok, we are ready to start the container now. Here is the command line I used:

docker run  -v openvpn-data:/etc/openvpn -d -p 1194:1194/tcp --cap-add=NET_ADMIN --restart=always --name=openvpn kylemanna/openvpn

First, we link to our data volume and expose our port to the server. Adding the _NETADMIN capability is necessary because the container will make a tunnel interface on the host. I love restart=always. It simplifies starting my services.

Network Details

This may be obvious, but there are some networking details to get this to work. First, your firewall will need to be setup to forward traffic to your host server on port 1194 (or whatever you configured). I can’t give you specific instructions on doing this for your router but most of the routers that have a UI section called something like “Port Forwarding” that should allow you to do it.

Also, your HOSTNAME value need to be on a public DNS server or it needs to be your public IP Address. In my case, I have a static IP address, so I was able to simply add a DNS entry on my public DNS server. If you’re IP Address is not static you can try using a dynamic DNS service like no-ip. You’ll then just need to configure your router to update the IP Address when it changes or use a service to do it. Many routers these days give you the ability to update a dynamic DNS entry.

Client Configuration

In order for a client to connect, we need create certificates for them. You should do this for each client replacing CLIENT with the actual client name.

docker run -v openvpn-data:/etc/openvpn --rm -it kylemanna/openvpn easyrsa build-client-full CLIENT nopass
docker run -v openvpn-data:/etc/openvpn --rm kylemanna/openvpn ovpn_getclient CLIENT > CLIENT.ovpn

In my case, I chose to only do certificate authentication, thus the nopass option. You can see the docker hub page listed earlier for more options and how to set a password for clients if you wish.

My Android devices worked out of the box. I simply installed OpenVPN and imported the configuration file made from above. My laptop had one issue though that might be noteworthy. I couldn’t get DNS to resolve out of the box with the configuration. I had to install openresolv, save the shell script update-resolv-conf.sh to /etc/openvpn/update-resolv-conf.sh, chmod it, and then add this to the bottom of my configuration:

script-security 2
up /etc/openvpn/update-resolv-conf
down /etc/openvpn/update-resolv-conf

With that done, it worked great.

Conclusion

Ok, that was a bit involved but if you are at all familiar with setting up a VPN service, most of it is a review. There are really just a few commands that might be new which are related to using the docker container for the service. I’ve been really happy with my setup. Not only do I use it to connect to my local systems but I also use it when I’m on public WiFi to ensure that my surfing is safe.