This How-To will show you how to launch an OpenVPN server in Docker Swarm, running in dual (TCP/UDP) mode. It will use swarm-launcher to start the processes in privileged mode and Nginx as a loadbalancer/proxy for allowing connections to the VPN server.

Table of Contents

Rationale

If you ever tried running a privileged container in docker swarm, you might have noticed that it’s currently not possible use --cap-add , --cap-drop or --privileged with docker stack deploy . Here is a workaround for that.

I’ll be using in this example the following docker images:

ixdotai/openvpn:latest - OpenVPN server in a Docker container complete with an EasyRSA PKI CA

ixdotai/swarm-launcher:latest - A docker image to allow the launch of container in docker swarm, with options normally unavailable to swarm mode

nginx:latest - an open source reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer, HTTP cache, and a web server (origin server)

Objectives

The objective is to have a fully functional OpenVPN server, that you can connect to, running on docker swarm. I’ll be using ports 1194/udp and 8443/tcp in this example.

Note: All the commands below need to be run on a swarm manager. You can see your manager nodes by running: sudo docker node ls -f role=manager .

Prerequisites

Have a fully functional docker swarm

Have a public IP address that points is configured on the swarm

(optional) Have a DNS entry that points at the IP address

Architectures Supported

linux/amd64

linux/arm64

linux/arm/v7

linux/arm/v6

OpenVPN

Getting the OpenVPN server up and running requires a few manual steps before deploying it. Make sure you read the documentation if you run into any problems.

Storage

OpenVPN needs a storage for the configuration and certificates. Ideally, you have a secure distributed storage available in your swarm (like NFS or CEPH). For this tutorial, I will assume that it is mounted under /var/docker/openvpn .

Run the following on one of your swarm

export OVPN_DATA = "/var/docker/openvpn"

Initialise the Configuration

If you have a domain name, make sure to replace it below. If not, you can use the public IP address

export ENDPOINT = "VPN.EXAMPLE.COM"

Generate the Configuration

sudo docker run -v " ${ OVPN_DATA } " :/etc/openvpn --log-driver=none --rm ixdotai/openvpn ovpn_genconfig -u udp:// " ${ ENDPOINT } " -b

Click to see output example Unable to find image 'ixdotai/openvpn:latest' locally latest: Pulling from ixdotai/openvpn 8fa90b21c985: Already exists a6e1cf67f1ae: Pull complete 29c596d05c0e: Pull complete 3c36e8be4cab: Pull complete 748c329393a2: Pull complete Digest: sha256:c7eb026fad0d1b7f9d299d5375b025c3f84451782fc31e6baeed8bf171c17efc Status: Downloaded newer image for ixdotai/openvpn:latest Disable default setenv opt for 'block-outside-dns' Processing Route Config: '192.168.254.0/24' Processing PUSH Config: 'dhcp-option DNS 1.1.1.1' Processing PUSH Config: 'dhcp-option DNS 1.0.0.1' Processing PUSH Config: 'comp-lzo no' Successfully generated config Cleaning up before Exit ...

This will effectively set the default client configuration to use the port 1194/udp . Take a look at docs/tcp.md for information on TCP.

Initialise the PKI

sudo docker run -v " ${ OVPN_DATA } " :/etc/openvpn --log-driver=none --rm -it ixdotai/openvpn ovpn_initpki

You will need to choose a password (using the nopass option is not secure). You can also set a name for the PKI, or just use the default one.

The command will take some time, since it generates Diffie-Hellman parameters.

Click to see output example init-pki complete; you may now create a CA or requests. Your newly created PKI dir is: /etc/openvpn/pki 1+0 records in 1+0 records out Using SSL: openssl OpenSSL 1.1.1d 10 Sep 2019 Enter New CA Key Passphrase: Re-Enter New CA Key Passphrase: Generating RSA private key, 2048 bit long modulus (2 primes) ....................................................................................................+++++ ....+++++ e is 65537 (0x010001) You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Common Name (eg: your user, host, or server name) [Easy-RSA CA]: CA creation complete and you may now import and sign cert requests. Your new CA certificate file for publishing is at: /etc/openvpn/pki/ca.crt Using SSL: openssl OpenSSL 1.1.1d 10 Sep 2019 Generating DH parameters, 2048 bit long safe prime, generator 2 This is going to take a long time ...........................................................................................................................................................................................................................................................................................................................................................+............................................................................................................................................................+.......................................+............................................................................+.......................................................................................................................................................................................................+..........................................................................+.............................................................................................................................+....................+........................................................................+...+................................................................+...........................................................................................+..................................................................................................................+........................................................................................................+...............................+.......+....................................+..+...................+...................................................................................................................+.................................................................+..........................+......................+..................................................................................................................................................................................+....+..........+...................................................+....................................................................................+...............................................+.............................................................................................+...............................................................................................................................+.......+................................+.................................................+..................................................................................+.............................................................+......................................................................................................+......................................+..............+...+...........................................+.............................................................................+......................................................................................................................+........................................................................................................................................................................+..................+..............................................................................................................................................................................................................................................................+............................+...........+..........................................................++*++*++*++* DH parameters of size 2048 created at /etc/openvpn/pki/dh.pem Using SSL: openssl OpenSSL 1.1.1d 10 Sep 2019 Generating a RSA private key .+++++ ...........................................................................................................................+++++ writing new private key to '/etc/openvpn/pki/private/VPN.EXAMPLE.COM.key.XXXXAEGCfL' ----- Using configuration from /etc/openvpn/pki/safessl-easyrsa.cnf Enter pass phrase for /etc/openvpn/pki/private/ca.key: Check that the request matches the signature Signature ok The Subject's Distinguished Name is as follows commonName :ASN.1 12:'VPN.EXAMPLE.COM' Certificate is to be certified until Feb 7 08:06:49 2023 GMT (1080 days) Write out database with 1 new entries Data Base Updated Using SSL: openssl OpenSSL 1.1.1d 10 Sep 2019 Using configuration from /etc/openvpn/pki/safessl-easyrsa.cnf Enter pass phrase for /etc/openvpn/pki/private/ca.key: An updated CRL has been created. CRL file: /etc/openvpn/pki/crl.pem

Nginx

Nginx needs a configuration file ( nginx.conf ). We’ll be using docker config to store it in the swarm.

error_log /dev/stdout info; events {} http {} stream { resolver 127.0.0.11 valid=1s ipv6=off; map $remote_addr $backend_udp { default vpn_vpn-udp_1; # Set this to ${LAUNCH_PROJECT_NAME}_${LAUNCH_SERVICE_NAME}_1 or to ${LAUNCH_CONTAINER_NAME} } map $remote_addr $backend_tcp { default vpn_vpn-tcp_1; # Set this to ${LAUNCH_PROJECT_NAME}_${LAUNCH_SERVICE_NAME}_1 or to ${LAUNCH_CONTAINER_NAME} } server { listen 1194 udp; proxy_pass $backend_udp:1194; } server { listen 8443; proxy_pass $backend_tcp:1194; } }

Make sure this file is on the swarm manager and is called nginx.conf .

Create the Config

sudo docker config create nginx.conf.v1 nginx.conf

You can see that it’s been created by running:

docker config ls ID NAME CREATED UPDATED 8tx79ul78nwtjvb47xjm4qu7a nginx.conf.v1 14 seconds ago 14 seconds ago

You’ll notice that I’ve added .v1 to the config name. This will allow you to update later on the config and just point nginx to the new one.

Docker Stack

Luckily, the swarm-launcher doesn’t need any additional configuration, so we can now create and deploy the stack.

Create the Stack YML File

On a swarm manager, create stack.yml with the following content:

version : "3.7" services : vpn-launcher-udp : deploy : labels : ai.ix.auto-update : 'true' image : ixdotai/swarm-launcher:latest volumes : - '/var/run/docker.sock:/var/run/docker.sock:rw' environment : LAUNCH_IMAGE : ixdotai/openvpn:latest LAUNCH_PULL : 'true' LAUNCH_EXT_NETWORKS : 'vpn_vpn-proxy' # Set this to NameOfStack_NameOfNetwork LAUNCH_PROJECT_NAME : 'vpn' LAUNCH_SERVICE_NAME : 'vpn-udp' LAUNCH_CAP_ADD : 'NET_ADMIN' LAUNCH_PRIVILEGED : 'true' LAUNCH_ENVIRONMENTS : 'OVPN_NATDEVICE=eth1' LAUNCH_VOLUMES : '/var/docker/openvpn:/etc/openvpn:rw' vpn-launcher-tcp : deploy : labels : ai.ix.auto-update : 'true' image : ixdotai/swarm-launcher:latest volumes : - '/var/run/docker.sock:/var/run/docker.sock:rw' environment : LAUNCH_IMAGE : ixdotai/openvpn:latest LAUNCH_PULL : 'true' LAUNCH_EXT_NETWORKS : 'vpn_vpn-proxy' # Set this to NameOfStack_NameOfNetwork LAUNCH_PROJECT_NAME : 'vpn' LAUNCH_SERVICE_NAME : 'vpn-tcp' LAUNCH_CAP_ADD : 'NET_ADMIN' LAUNCH_PRIVILEGED : 'true' LAUNCH_ENVIRONMENTS : 'OVPN_NATDEVICE=eth1' LAUNCH_VOLUMES : '/var/docker/openvpn:/etc/openvpn:rw' LAUNCH_COMMAND : 'ovpn_run --proto tcp' nginx-proxy : deploy : labels : ai.ix.auto-update : 'true' image : nginx:latest networks : - vpn-proxy configs : - source : nginx.conf.v1 target : /etc/nginx/nginx.conf ports : - '1194:1194/udp' - '8443:8443' configs : nginx.conf.v1 : external : true networks : vpn-proxy : attachable : true driver : overlay driver_opts : encrypted : 'true'

Note: You’ll notice the label ai.ix.auto-update: 'true' . I use this with ixdotai/cioban, to automatically update swarm services to the latest version of the docker image. You can leave it out, if you don’t use it.

WARNING: If you decide not to use the name vpn for the stack, but something else (say beldeneige ), you must change the variable LAUNCH_EXT_NETWORKS to match it (e.g. beldeneige_vpn-proxy ).

Deploy the Stack

sudo docker stack deploy --compose-file stack.yml --prune vpn

That’s it. You should soon see the services running:

sudo docker service ls -f name=vpn ID NAME MODE REPLICAS IMAGE PORTS fp8qyz2divix vpn_nginx-proxy replicated 1/1 nginx:latest *:8443->8443/tcp, *:1194->1194/udp 7aix7z5mrtnk vpn_vpn-launcher-tcp replicated 1/1 ixdotai/swarm-launcher:latest xk14lwmvwbw7 vpn_vpn-launcher-udp replicated 1/1 ixdotai/swarm-launcher:latest

You can also look on which node ixdotai/swarm-launcher was started:

sudo docker service ps vpn_vpn-launcher-tcp ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS q02qnd9c9q1o vpn_vpn-launcher-tcp.1 ixdotai/swarm-launcher:latest docker-c Running Running 3 hours ago 1fziscmqskxn \_ vpn_vpn-launcher-tcp.1 ixdotai/swarm-launcher:latest docker-a Shutdown Shutdown 3 hours ago

Finally, you can look at all containers started by ixdotai/swarm-launcher locally:

sudo docker ps -f label=ai.ix.started-by=ix.ai/swarm-launcher CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e7e31a40dc79 ixdotai/openvpn:latest "ovpn_run --proto tcp" 3 hours ago Up 3 hours 1194/udp vpn_vpn-tcp_1 ab39ac215297 gitlab/gitlab-runner:alpine "/usr/bin/dumb-init …" 4 days ago Up 4 days gitlab-ci-runner

(Yes, I start my GitLab Runner also with ixdotai/swarm-launcher)

Troubleshooting

All the logs of ixdotai/swarm-launcher and of the ixdotai/openvpn container are available in the docker service. In our case, we can run:

sudo docker service logs vpn_vpn-launcher-tcp

Next Steps

Take a look at the OpenVPN README and Advanced Client Management documentation.

You can start by creating the configuration for your VPN clients:

sudo docker run -v "${OVPN_DATA}":/etc/openvpn --log-driver=none --rm -it ixdotai/openvpn easyrsa build-client-full CLIENTNAME nopass

You then can save the configuration: