OpenVPN server inside docker container

The problem

Start an OpenVPN server so that other users (i.e. workers) can connect to your internal network.

The network architecture looks like this:

graph LR
    subgraph Internal Network
    subgraph Docker Host<br/>192.168.13.20
        GW[OpenVpn gateway <br/> 172.0.0.2 <br/> 192.168.255.1]
    end
    A[Host A <br/> 192.168.13.1]
    B[Host B <br/> 192.168.13.2]
    end
    Client[Client <br/> 192.168.255.2] --> GW
    GW --- A
    GW --- B

We’ve got three separate subnets:

  • 192.168.13.0/24 - the target internal network (i.e. company network)
  • 172.17.0.0/16 - docker internal bridge network
  • 192.168.255.0/24 - openvpn network, from which addresses are assigned to clients

We want to be able to connect through OpenVPN and access any host in the internal network. This will be achieved through routing and NAT (as opposed to bridging, where VPN clients would get IP addreses from internal network).

Docker to the rescue

Let’s start with OpenVPN docker image. The heavy lifting has already been done and there is an image at https://github.com/kylemanna/docker-openvpn/tree/master that not only starts OpenVPN server, but also facilitates registering new clients and generating profiles for them. There’s also a guide on how to use it with docker-compose. Sweet.

The docker-compose.yml looks like this:

version: '2'
services:
  openvpn:
    cap_add:
     - NET_ADMIN
    image: kylemanna/openvpn
    container_name: openvpn
    ports:
     - "1194:1194/udp"
    restart: always
    volumes:
     - ./openvpn-data/conf:/etc/openvpn

Before running openvpn server itself, we need to generate it’s config and certificates, using scripts provided in the image.

> docker-compose run --rm openvpn ovpn_genconfig -u udp://VPN.SERVERNAME.COM
> docker-compose run --rm openvpn ovpn_initpki

You will be asked for private key passphrace and Common Name for server certificate, then DH keypairs will be generated. When it says it’s going to take a long time - it really is. Go make yourself a tea.

Configuration

ovpn_genconfig script that we used in previous step generates two config files: openvpn.config and ovpn_env.sh. It takes a bunch of additional arguments to help you customize the config. You can either play along with it or edit the generated config manually. ovpn_genconfig also does some basic iptables configuration, which otherwise you would have to edit by hand, so I’ll stick with the script.

Let’s see what ovpn_genconfig can do for us:

> docker-compose up -d openvpn 
> docker-compose exec openvpn bash
bash-4.3# ovpn_genconfig -?
Invalid option: -?
usage: /usr/local/bin/ovpn_genconfig [-d]
                  -u SERVER_PUBLIC_URL
                 [-e EXTRA_SERVER_CONFIG ]
                 [-E EXTRA_CLIENT_CONFIG ]
                 [-f FRAGMENT ]
                 [-n DNS_SERVER ...]
                 [-p PUSH ...]
                 [-r ROUTE ...]
                 [-s SERVER_SUBNET]

optional arguments:
 -2    Enable two factor authentication using Google Authenticator.
 -a    Authenticate  packets with HMAC using the given message digest algorithm (auth).
 -b    Disable 'push block-outside-dns'
 -c    Enable client-to-client option
 -C    A list of allowable TLS ciphers delimited by a colon (cipher).
 -d    Disable default route
 -D    Do not push dns servers
 -k    Set keepalive. Default: '10 60'
 -m    Set client MTU
 -N    Configure NAT to access external server network
 -t    Use TAP device (instead of TUN device)
 -T    Encrypt packets with the given cipher algorithm instead of the default one (tls-cipher).
 -z    Enable comp-lzo compression.

In this scenario, we want to:

  • enable NAT (-N)
  • push (-p) routes to the client, so it knows to it should use vpn network to connect to company network
  • use company’s internal dns for name resolution (-n) and set default domain to mycompany.net (push adequate dhcp-option to client)

Here’s the command to do that:

# remove old ovpn_env.sh
> docker-compose run --rm openvpn rm /etc/openvpn/ovpn_env.sh

# generate new config files
> docker-compose run --rm openvpn ovpn_genconfig -N -d -n 192.168.13.6 -u udp://vpn.mycompany.net -p "dhcp-option DOMAIN mycompany.net" -p "route 192.168.13.0 255.255.255.0" -p "route 172.17.0.0 255.255.0.0"

The generated config will be in ./openvpn-data/conf and it should look like this (you may also want to take a look at ovpn_env.sh):

server 192.168.255.0 255.255.255.0
verb 3
key /etc/openvpn/pki/private/dev.legimi.com.key
ca /etc/openvpn/pki/ca.crt
cert /etc/openvpn/pki/issued/dev.legimi.com.crt
dh /etc/openvpn/pki/dh.pem
tls-auth /etc/openvpn/pki/ta.key
key-direction 0
keepalive 10 60
persist-key
persist-tun

proto udp
# Rely on Docker to do port mapping, internally always 1194
port 1194
dev tun0
status /tmp/openvpn-status.log

user nobody
group nogroup

### Push Configurations Below
push "dhcp-option DNS 192.168.13.6"
push "dhcp-option DOMAIN legimi.com"
push "route 192.168.13.0 255.255.255.0"
push "route 172.17.0.0 255.255.0.0"

Client profiles

Now that openvpn server is configured, make sure it is up and running:

> docker-compose up

To connect, we’ll need a client profile.

# Generate a client certificate (you will be asked for a passphrase)
> export CLIENTNAME="your_client_name"
> docker-compose run --rm openvpn easyrsa build-client-full $CLIENTNAME

# Generate client profile for openvpn
> docker-compose run --rm openvpn ovpn_getclient $CLIENTNAME > $CLIENTNAME.ovpn

Finally, connect to openvpn:

> openvpn -c your_client_name.ovpn

If everything goes fine, you should be able to ping the internal network (i.e. 192.168.13.1 host).

Troubleshooting

If you cannot connect to openvpn:

  • make sure openvpn container starts without errors and port 1194 is exposed at docker host
  • make sure port 1194 is open at vpn url you specified when configuring (vpn.mycompany.net)

If you can connect to openvpn, but cannot ping internal network:

  • check if the client machine has an IP address from openvpn network assigned (192.168.255.x)
  • ping 192.168.255.1 (that’s the default gateway for openvpn connections). If you can’t, there’s probably something wrong with openvpn config.
  • check openvpn container’s internal IP (172.17.0.x) and ping it.
  • check routing tables on client machine. There should be routes for networks 172.17.0.0 and 192.168.13.0 going through Gateway 192.168.255.1.
  • check if you can ping internal network from openvpn container