OpenVPN AS Under Docker

How to run OpenVPN Access Server under Docker
August 15, 2016
docker openvpn vpn

Working on a setup of OpenVPN Access Server (AS) to use with my Lan Turtles. I run everything under Docker, and the two images for this suite leave me wanting.

The primary issue is that I run a bunch of other services on the host, so 443 is already taken. One of the containers is so proud of their ability to run with networking: host so that effectively they’re just running AS in a jail. Combined with priveleged: true to give it the ability to manipulate iptables and they might as well just run the binary outside of Docker.

The next issue is that I want to use more than one turtle, and OpenVPN sells licenses in blocks of 10.

The final issue is that I have more than one turtle deployed.

So this leaves me to figure out how I can run multiple copies of AS on different ports under Docker, one for each turtle, while still keeping encapsulation of the processes internal to Docker and making the ports something manageable.

HAProxy Doesn’t Work (Or Does It?)

Almost all of my Docker services run behind haproxy. I use SNI and domain mapping to route traffic from 80443 to the appropriate backend container, running Apache or Gitlab or whatever else. For services that aren’t HTTP-backed I run haproxy with mode: tcp to just ferry the packets to and from the container. I do this because all of my container port allocations follow a specific convention. I prefer to know that port 1XYZ and 2XYZ go to the same container. This is better than remembering that 44 goes here and 1234 goes there but is bundled with 1117. (All port numbers are made up to protect the innocent.)

Remember: humans are terrible at remembering things. Don’t set yourself up for failure.

Anyway, this doesn’t work. I punched a hole from XYZ to 943 and told it not to do anything with the data, but haproxy reports that whatever is on the other side of 943 is a Bad Gateway.

The reason for this is because I do all SSL offloading on haproxy and run HTTP on localhost to the backends. This keeps my SSL component in one place. For AS, though, we need to make some changes to how haproxy talks to it:

option tcp-check
server s1 localhost:XYZ ssl check verify none fall 3 rise 2 maxconn 16
  1. option tcp-check tells it to only to L4 checks, not L7
  2. ssl check verify none tells it to speak SSL to the backend and not check or certificate validity (self-signed is okay)

This is necessary because my frontend decodes the SSL stream to capture the Host: header and then uses that to route to the correct backend. It’s not enough to simply speak TCP to the backend - the stream is already decoded. haproxy needs to speak SSL to the backend, effectively acting as a MiTM.

(Before you freak out that I’m MiTM’ing my traffic, consider that it’s on localhost and that I already decode SSL within haproxy and speak HTTP to my backends. What’s that called? There’s no middle, so is it just a Man?)

With this configuration, we can access multiple AS hosts running on the same host without having to know their admin port, and we don’t need to configure them to run with an admin port other than the default. Win!

Another benefit to running this way is you can have turtles sitting on identically-numbered subnets. Many of today’s home routers are installed in offices and left at the defaults, and the access server will have a fit if you tell it that two devices have 192.168.1.0/24 behind them.