Traefik, OpenVPN and Direct Container Connections on Docker Swarm
Building on OpenVPN Server on Docker Swarm and Deploy traefik, prometheus, grafana, portainer and oauth2_proxy with docker-compose, this How-To will show you how to:
- Launch Traefik as an edge router in docker swarm
- Launch OpenVPN Server
- Connect to your docker web container via VPN by using name resolution
This documentation is oriented to advanced users. See Prerequisites.
Rationale
Since writing OpenVPN Server on Docker Swarm, Traefik v2.2 was released, with UDP
support. This allows now to ditch the use of nginx
from the configuration.
I’ll be using in this example the following docker images:
- traefik:latest - Traefik is the leading open source reverse proxy and load balancer for HTTP and TCP-based applications that is easy, dynamic, automatic, fast, full-featured, production proven, provides metrics, and integrates with every major cluster technology
- registry.gitlab.com/ix.ai/openvpn:latest - OpenVPN server in a Docker container complete with an EasyRSA PKI CA
- registry.gitlab.com/ix.ai/swarm-launcher:latest - A docker image to allow the launch of container in docker swarm, with options normally unavailable to swarm mode (needed for OpenVPN - to be able to start it on Docker Swarm)
- registry.gitlab.com/ix.ai/spielwiese:latest - A tiny python webserver that prints HTTP request information
Prerequisites
- Have a fully functional docker swarm
- Have a public IP address that points to one of the docker swarm nodes
- Have an
attachable
docker swarm network (see Traefik Network below) - (optional) Have a DNS entry that points at the IP address (something like
vpn.example.com
- which will be used in this example) - (optional) Have another DNS entry that points at the IP address (something like
spielwiese.example.com
- which will be used in this example)
Any commands listed below need to be run on a swarm manager. You can see your manager nodes by running: sudo docker node ls -f role=manager
.
Certain workloads are designed to run only on docker manager nodes (such as OpenVPN, Traefik). While this can be worked around, it’s out of scope for this How-To.
Files Needed
- services.yml - the Traefik external service definition
- stack-traefik.yml - the stack for Traefik
- stack-vpn.yml - the stack for OpenVPN
- stack-websites.yml - the stack for the websites
The stacks created with this How-To:
traefik
vpn
websites
Folders and persistent storage
You will want to store the certificates created for OpenVPN, so that they don’t get lost the first time you restart the container. I’m using for this a folder shared among all docker manager nodes, mounted under /var/docker
.
For Traefik and OpenVPN (as root):
mkdir -p /var/docker/traefik
mkdir -p /var/docker/openvpn
Networks
Make sure you have two attachable
swarm network for the communication between:
- Traefik <–> OpenVPN (
traefik-vpn
) - OpenVPN <–> Other Containers (
vpn
)
Also, one non-attachable swarm network for the web services:
- Traefik <–> Websites (
traefik-web
)
Create the networks
docker network create --driver overlay --attachable --scope swarm --opt encrypted traefik-vpn
docker network create --driver overlay --attachable --scope swarm --opt encrypted vpn
docker network create --driver overlay --scope swarm --opt encrypted traefik-web
Since we are using registry.gitlab.com/ix.ai/swarm-launcher to start OpenVPN, which is a glorified docker-compose
, the networks traefik-vpn
and vpn
must be attachable from outside the Docker Swarm!
OpenVPN
I have already covered the detailed configuration of OpenVPN in the post OpenVPN Server on Docker Swarm, so I will do only a summary here.
Variables
export OVPN_DATA="/var/docker/openvpn"
export ENDPOINT="vpn.example.com"
If you don’t have a FQDN for vpn.example.com
, replace it with the public IP of the Traefik container.
DNSMASQ
Assuming you haven’t changed the IP pool of the OpenVPN server, by running the following command, you can configure it to push itself (192.168.255.1
) as a DNS server to the clients:
docker run \
-v "${OVPN_DATA}":/etc/openvpn \
--log-driver=none \
--rm \
registry.gitlab.com/ix.ai/openvpn ovpn_genconfig \
-u udp://"${ENDPOINT}" \
-b \
-Q \
-n 192.168.255.1
-Q
enables the DNSMASQ server baked into the image-n 192.168.255.1
tells the clients to use the internal OpenVPN IP for DNS
Initialise the PKI
docker run \
-v "${OVPN_DATA}":/etc/openvpn \
--log-driver=none \
--rm \
-it \
registry.gitlab.com/ix.ai/openvpn \
ovpn_initpki
Stack File
See stack-vpn.yml:
|
|
registry.gitlab.com/ix.ai/swarm-launcher
converts @_@
to a single space. The OpenVPN container will have the variable OVPN_NATDEVICE="eth0 eth1 eth2"
. Simply put, in order to communicate with anything outside the OpenVPN container, you need at least one ethX
NAT device in the list plus one for every additional network (that’s listed under LAUNCH_EXT_NETWORKS
).
Deploy the Stack
docker stack deploy vpn -c stack-vpn.yml
Traefik
Since Traefik can’t run in dual mode, both with and without swarm
, we need the services.yml file to create an UDP router and its service for port 1194:
|
|
Note the name vpn_vpn_1
on line 11. This is the name of the docker container running OpenVPN, as seen when issuing docker ps
on the last column. If you use a different name for your container, it must be reflected here as well, or else connecting to your OpenVPN server will not work.
Considering that we need to get the services.yml
file in docker swarm, we will create a docker config:
docker config create traefik_services.yml.v1 services.yml
Creating a docker config can be integrated in the stack-traefik.yml. I prefer to create it outside of the stack, since I want to use versioning for the configs.
Letsencrypt
export CF_DNS_API_TOKEN="FOO"
export CF_ZONE_API_TOKEN="BAR"
The CF_DNS_API_TOKEN
(API token with DNS:Edit permission) can be set only for a specific zone (in our case, example.com
), but the CF_ZONE_API_TOKEN
(API token with Zone:Read permission) has to have permissions for all zones.
Stack File
See stack-traefik.yml:
|
|
Using mode: host
to be able to capture the real IP of the connecting clients.
Deploy the Stack
docker stack deploy traefik -c stack-traefik.yml
Websites
I’ll keep this part simple: only stack-websites.yml. Note the ai.ix.fqdn
label - it was configured in the Traefik Stack File with --providers.docker.defaultRule
.
export SPIELWIESE_DOMAIN="spielwiese.example.com"
|
|
Deploy the Stack
docker stack deploy websites -c stack-websites.yml
That’s it! If you now connect to your OpenVPN service, you should be able to:
curl -v http://websites_spielwiese:8000
* Trying 10.0.43.6:8000...
* Connected to websites_spielwiese (10.0.43.6) port 8000 (#0)
> GET / HTTP/1.1
> Host: websites_spielwiese:8000
> User-Agent: curl/7.72.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 271
< Content-Type: text/html; charset=utf-8
< Date: Fri, 18 Sep 2020 21:16:39 GMT
< Server: spielwiese 1.1.2-122639551
<
request: GET / HTTP/1.1
hostname: 18ddd83872a4
uname: #51-Ubuntu SMP Fri Sep 4 19:50:52 UTC 2020
ram: 63 GB
remote_addr: [10.0.43.5]:63760
lo: 127.0.0.1
eth0: 10.0.14.95
eth2: 172.18.0.20
eth1: 10.0.43.7
Host: websites_spielwiese:8000
User-Agent: curl/7.72.0
Accept: */*
* Connection #0 to host websites_spielwiese left intact
When connecting from a Linux client, using NetworkManager
and systemd-resolved
, you will probably not be able to resolve the host:
curl -v websites_spielwiese:8000
* Could not resolve host: websites_spielwiese
* Closing connection 0
curl: (6) Could not resolve host: websites_spielwiese
If this doesn’t work, add the name of the network as a domain name:
curl -v websites_spielwiese.vpn:8000
* Trying 10.0.43.11:8000...
* TCP_NODELAY set
* Connected to websites_spielwiese.vpn (10.0.43.11) port 8000 (#0)
> GET / HTTP/1.1
> Host: websites_spielwiese.vpn:8000
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 277
< Content-Type: text/html; charset=utf-8
< Date: Tue, 20 Oct 2020 11:51:01 GMT
< Server: spielwiese 1.1.3-192008834
<
request: GET / HTTP/1.1
hostname: c80c5e2825c7
uname: #57-Ubuntu SMP Thu Oct 15 10:57:00 UTC 2020
ram: 63 GB
remote_addr: [10.0.43.9]:43318
lo: 127.0.0.1
eth0: 10.0.14.50
eth2: 172.18.0.31
eth1: 10.0.43.32
Host: websites_spielwiese.vpn:8000
User-Agent: curl/7.68.0
Accept: */*
* Connection #0 to host websites_spielwiese.vpn left intact
Updates
This article has been updated:
- 2020-10-20: Add the warning about not being able to resolve the service
- 2020-11-01: Change the docker images from docker hub (
ixdotai/[...]
) toregistry.gitlab.com/ix.ai/[...]