How to secure containers with VPN?

ProtonVPN

Below article will explain how to configure OpenVPN container to make it work with any VPN provider. Next I will reconfigure one of the existing container from the stack to make it use a VPN network as its gateway - (in case of VPN connection dropping containers using VPN gateway should also loose access the Internet). Next I will make container behind VPN gateway reachable from withing the local network. In the last step I will do a few tests to check if VPN is actually working and if traffick goes the way we expect it to go.

Before you start, please backup your LMDS system, make a copy of your current docker-compose.yml file, as we will be modifying it quite extensively. Please bear in mind that indentation in *.yml files is important and incorrect syntax will make it stop working. Be precised while changing docker-compose.yml file.

Disclaimer

VPN Container - dperson/openvpn-client

This container, among the others is one of the few that is actually a VPN client implementation, majority of the VPN containers available are server ones - not the ones you need. I did not create this container, I only describe its usability in Docker in LMDS stack. This guide can be adopted to any Docker stack not just to LMDS. Please be aware that in case of issues with VPN connectivity, container workability or accessability overall, I wont be able to help you diagnose the problem. At this level of complexity you have to spend some time and educate yourself about the tools you are about to use. It is not that, I do not want to help but this might be extremely difficult and very frustrating for both of us on this subject. This is also why I did not attempt to script this procedure as this would be impossible to get right for everyone.

I hope you are successful while following this instruction.

How to configure VPN client container?

While you inside LMDS folder run:

./deploy.sh

Choose "Build LMDS Stack" and then select "vpn-client" from the list and OK, hit Enter couple of times confirming default options, when you are back to the shell run:

docker-compose up -d

After "vpn-client" container is created, edit section of the docker-compose.yml shown below:

You can also copy/paste below to your custom docker-compose file and follow along, even if you did not deploy entire LMDS stack.
  vpn:
    container_name: vpn-gateway
    image: dperson/openvpn-client
    restart: unless-stopped
    ports:            # List all port numbers of the containers that you would like to put behind VPN. 
                      # Remember, these ports can only exist in a single place inside entire docker-compose.yml file.
      - 90:80         # Redirecting to port 90 as 80 we will need this at some point for reverseproxy traefik. 
    dns:              # Use VPN provided DNS IPs if you have them otherwise leave as is.
      - 8.8.8.8
      - 8.8.4.4
    cap_add:
      - NET_ADMIN
    devices:
      - '/dev/net/tun:/dev/net/tun:rwm'
    environment:
      FIREWALL: ''    # If you use different VPN ports then default 1194 add them in here, otherwise leave it empty as is.
     #VPN 'server_address;user;password;port'    # Configure VPN server address, authentication and port if needed by your VPN provider (port value is optional) we will use an external config file for this, leave commented as is.
      PUID: 1000
      PGID: 1000
      TZ: UTC+0
      ROUTE: '192.168.0.0/16'    # Specify subnet of your home LAN in order to connect into the container behind VPN - if you don't, containers will work but you wont be able to connect to them locally.
    networks:
      - default
    read_only: true
    tmpfs:
      - /run
      - /tmp
    security_opt:
      - label:disable
    stdin_open: true
    tty: true
    volumes:
      - ./vpn:/vpn    # This folder should contain two files:
                      # 1. Copy .ovpn file you received from VPN provider in here and rename it to vpn.conf
                      # 2. Create vpn.auth file and put inside your VPN access username and password in two separate lines one under another.
                      # Important: edit vpn.conf file you renamed and find line called auth-user-pass append it with a path to your vpn.auth file, in my case: auth-user-pass /vpn/vpn.auth 

There is few things you have to adjust in above example:

  • ports: add all port numbers of the containers that you are planing to put behind VPN in this section, in example if you plan to hide deluge container behind VPN, you should add deluge container exposed ports like 8112 and 58846 in here and remove them from deluge container definition itself. These ports can only exist in one place inside docker-compose.yml file. For the containers like Portainer on Nginx that should not go through VPN, keep their port numbers under container declaration ony as they initially are.
  • dns: You can leave as is or use VPN provided DNS IPs - if you have them. Before changing them, test with Google ones first.
  • FIREWALL: '' if you use custom VPN ports (not default 1194) added here and under VPN section, otherwise leave it empty as is.
  • VPN Configure VPN server address, authentication and port if needed by your VPN provider (port value is optional) we will use an external config file for this, leave commented out as is.
  • ROUTE: '192.168.0.0/16' this value is adding a static route inside the VPN container. Provide a subnet that reflect your home LAN addressing. This is needed in order to reach containers connected to the VPN from your local network - if you misconfigure this part, containers will work, but you wont be able to access them from your laptop or PC in local network. You will still be able to access containers hidden behind VPN using host IP address and port exposed for the container as before.
  • ./vpn:/vpn This folder should contain two files:
    • Copy .ovpn file you received from VPN provider in here and rename it to vpn.conf
    • Create vpn.auth file and put there your username and password in two separate lines one under another.
    • Important: edit vpn.conf file you renamed and find line called auth-user-pass append it with a path to your vpn.auth file, in my case: auth-user-pass /vpn/vpn.auth
Below you can find two example of the *.ovpn files:
  • vpngate.ovpn - this is an example of free VPN provider config file. You can find more of them at vpngate.net. No subscription required - these services are not suitable for majority of the cases, but you can use them for testing or as a reference point. P2P not allowed.
  • nl-free-07.protonvpn.com.udp.ovpn - this is an example of subscription based VPN provider config file. This is going to be equivalent of your NordVPN, PIA, SurfShark and other providers type configuration files. All of them will look very similar, you will usually have to setup an account with the provider and opt for a paid subscription plan. This is going to be your preferred way of utilizing VPN on majority of the cases.
I will use ProtonVPN as an example, showing you potential issues that you can expect while setting this up. I use ProtonVPN and I am finding them very good for my needs, if you did not purchase any VPN service yet, consider using ProtonVPN, you can also support this blog by using this link ProtonVPN.

How to use free VPN in Docker container?

All what you have to do in order for VPN gateway to utilize free VPN provider services is to copy *.ovpn file in to ~/LMDS/vpn/ folder and restart the container. You can grab some example files from vpngate.net. After you restart VPN container give it a minute before testing connectivity, it might take a while before VPN tunnel is established. On this occasion I did not even had to rename *.ovpn to *.conf and all worked ok.

How to use proper VPN in Docker container?

ProtonVPN

Utilizing VPN provider, that requires you to have an account and paid subscription is little more involved. Below is what I had to do to make it work - it wasn't as straight forward as I though it could be. Container I selected for this project can utilize any VPN provider config with little persuasion and it has some additional features that other containers might not.

What needs to be done:

  • Create vpn.auth file inside ~/LMDS/vpn/ folder. vpn.auth file should contain VPN access username and password in two separate lines. Please double check with your VPN provider, what username and password you should use while connecting manually using OpenVPN as these credentials might be different from the ones you use with their app. ProtonVPN has i.e. one set of credentials for dedicated app they provide and completely different set of credentials for manual connection from OpenVPN.
  • Copy your *.ovpn file in to ~/LMDS/vpn/ and rename it to vpn.conf i.e. nl-free-07.protonvpn.com.udp.ovpn rename to vpn.conf. Inside that file find a line "auth-user-pass" - it should be there somewhere, if not there add it manually "auth-user-pass /vpn/vpn.auth". This will provide credentials for VPN tunnel to be made by OpenVPN software inside the Docker container.

This should be all, but in my case I had to remove one section from the vpn.conf file, that was causing a problem. When I initially started VPN container using "docker-compose up -d vpn" container was rebooting itself over and over again, I checked the logs "docker-compose logs vpn" and saw below message popping up after each reboot of the container:

Options error: --up script fails with '/etc/openvpn/update-resolv-conf': No such file or directory (errno=2)

From inside vpn.conf I had to remove below three lines in order to keep VPN container up:

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

After removing above, container started ok and successfully connected to ProtonVPN server:

vpn-gateway | 2021-06-08 20:25:41 TUN/TAP device tun0 opened
vpn-gateway | 2021-06-08 20:25:41 /sbin/ip link set dev tun0 up mtu 1500
vpn-gateway | 2021-06-08 20:25:41 /sbin/ip link set dev tun0 up
vpn-gateway | 2021-06-08 20:25:41 /sbin/ip addr add dev tun0 10.30.0.9/16
vpn-gateway | 2021-06-08 20:25:41 /sbin/ip route add 185.107.80.216/32 via 172.18.0.1
vpn-gateway | 2021-06-08 20:25:41 /sbin/ip route add 0.0.0.0/1 via 10.30.0.1
vpn-gateway | 2021-06-08 20:25:41 /sbin/ip route add 128.0.0.0/1 via 10.30.0.1
vpn-gateway | 2021-06-08 20:25:41 Initialization Sequence Completed

How to make other containers use VPN network?

If you previously deployed any container you should find it definition somewhere inside docker-compose.yml file.

i.e. initially deluge container will be defined as shown below:

   deluge:
    image: linuxserver/deluge
    container_name: deluge
    stdin_open: true
    tty: true
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/London
      - UMASK=022 #optional
    volumes:
      - ./volumes/deluge/config:/config
      - ./downloads:/downloads
    ports:
      - 8112:8112
      - 58846:58846

We will change it as follows:

   deluge:
    image: linuxserver/deluge
    container_name: deluge
    depends_on:     # Add dependency on vpn container, this container won't start if vpn container is not running.
      - vpn
    network_mode: service:vpn     # This line will put container behind VPN one.
    stdin_open: true
    tty: true
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/London
      - UMASK=022 #optional
    volumes:
      - ./volumes/deluge/config:/config
      - ./downloads:/downloads
#   ports:    # Comment out all the ports you had exposed before and add them to the vpn container definition underneath PORTS section.
#     - 8112:8112
#     - 58846:58846
  • depends_on: - vpn adding dependency on vpn container, container having this flag wont start until vpn container is up.
  • network_mode: service:vpn adding this line will make particular container seat behind VPN gateway and pass its traffick through VPN provider network.
  • ports: Comment out all the ports you had exposed before and add them to the vpn container definition underneath PORTS section instead.

How to access containers locally while they use VPN network?

There are two mandatory things that I hope you already did in order to access your VPN protected containers from local network. Inside docker-compose.yml locate vpn-client container definitions and make sure that you exposed ports of the containers you linked with vpn-client under that container, also make sure ROUTE section has your local network subnet configured properly. I would imagine that what is set up there by default would be correct for most of the cases. If you have different subnet - more likely you know what you are doing anyway.

in order to put deluge container behind VPN I will add deluge ports to vpn-client container as shown below:

  vpn:
    container_name: vpn-client
    image: dperson/openvpn-client
    restart: unless-stopped
    ports:            # List all port numbers of the containers that you would like to put behind VPN. Remember, these ports can only exist in a single place inside entire docker-compose.yml file.
      - 90:80         # Redirecting to port 90 as 80 we will need this at some point for reverseproxy traefik. 
      - 8112:8112
      - 58846:58846 
    dns:              # Use VPN provided DNS IPs if you have them otherwise leave as is.
      - 8.8.8.8
      - 8.8.4.4
    cap_add:
      - NET_ADMIN
    devices:
      - '/dev/net/tun:/dev/net/tun:rwm'
    environment:
      FIREWALL: ''    # If you use different VPN ports then default 1194 add them in here, otherwise leave it empty as is.
     #VPN 'server_address;user;password;port'    # Configure VPN server address, authentication and port if needed by your VPN provider (port value is optional) we will use an external config file for this, leave commented as is.
      PUID: 1000
      PGID: 1000
      TZ: UTC+0
      ROUTE: '192.168.0.0/16'    # Specify subnet of your home LAN in order to connect into the container behind VPN - if you don't, containers will work but you wont be able to connect to them locally.
    networks:
      - default
    read_only: true
    tmpfs:
      - /run
      - /tmp
    security_opt:
      - label:disable
    stdin_open: true
    tty: true
    volumes:
      - ./vpn:/vpn    # This folder should contain two files:
                      # 1. Copy .ovpn file you received from VPN provider in here and rename it to vpn.conf
                      # 2. Create vpn.auth file and put inside your VPN access username and password in two separate lines one under another.
                      # Important: edit vpn.conf file you renamed and find line called auth-user-pass append it with a path to your vpn.auth file, in my case: auth-user-pass /vpn/vpn.auth 

If you did everything as instructed, save the changes and you should be ready to recreate stack by executing docker-compose up -d from the command line.

Do not forget to put your .ovpn file inside ~/LMDS/vpn/ folder, rename it to vpn.conf, then append auth-user-pass line providing path to your credentials file called vpn.auth also created and stored inside this folder.

How to test if VPN is working and if traffick is secured?

In order to test if container hidden behind vpn gateway use VPN network and if is using VPN provider network as its default route we will need to deploy another container. This container has all the testing tools we need to find out the truth. We could use deluge for testing or any other container you stashed behind vpn-gateway but I am sure they do not even have ifconfig command available.

I found below container very useful for checking the network related features, you can use any other if you like.

Copy below container definition in to docker-compose.yml file - it can go at the very bottom of that file, save it and run docker-compose up -d again.

  netmtool:
    container_name: net-tool
    image: praqma/network-multitool
    depends_on:
      - vpn
    network_mode: service:vpn

When net-tool is deploy check if is running docker ps. if you see it running enter the container command line docker exec -it net-tools /bin/bash or docker exec -it net-tools /bin/sh your prompt will change to something like this bash-5.0# or just #

When inside the container run ifconfig and check if you see tun0 interface and notice its IP address - do you see IP that belongs to your VPN provider - you should.

Example:

tun0      Link encap:UNSPEC  HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
          inet addr:10.211.1.149  P-t-P:10.211.1.150  Mask:255.255.255.255
          UP POINTOPOINT RUNNING NOARP MULTICAST  MTU:1500  Metric:1
          RX packets:409055 errors:0 dropped:0 overruns:0 frame:0
          TX packets:7550 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:500
          RX bytes:129422927 (123.4 MiB)  TX bytes:679034 (663.1 KiB)

Next, we can check default route to the Internet - type route First record you see called default should point to IP address that belongs to the VPN provider.

Example:

bash-5.0# route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         10.211.1.150    128.0.0.0       UG    0      0        0 tun0
default         172.18.0.1      0.0.0.0         UG    0      0        0 eth0
10.211.1.150    *               255.255.255.255 UH    0      0        0 tun0
23.170.32.37    172.18.0.1      255.255.255.255 UGH   0      0        0 eth0
128.0.0.0       10.211.1.150    128.0.0.0       UG    0      0        0 tun0
172.18.0.0      *               255.255.0.0     U     0      0        0 eth0
192.168.0.0     172.18.0.1      255.255.0.0     UG    0      0        0 eth0
192.168.100.0   172.18.0.1      255.255.255.0   UG    0      0        0 eth0

As we know IP address of the tun0 and we checked that our default gateway IP belongs to our VPN provider network, we can now verify if traffick actually goes the wey we expect it to go - through the VPN pipe mtr google.com it will show you a traceroute like output but more dynamic, look at the first IP there - is it your VPN provider broadcast IP? In my case it start from 10.211 .... so it is ok and it works. I can reach Google and traffick goes over VPN network.

Example:

c48a046e12c7 (10.211.1.149) 2021-03-22T22:05:20+0000
        Keys: Help Display mode Restart statistics Order of fields quit
        Packets Pings
        Host                                               Loss% Snt Last Avg Best Wrst StDev
        1. 10.211.254.254                                   0.0% 12 309.8 301.2 181.9 388.9 63.9
        2. vl99-dist1-van1.etinw.net                        0.0% 12 215.1 264.9 172.3 418.4 67.8
        3. 1gig-e1-4-fw1-van1.etinw.net                     0.0% 12 317.0 300.5 225.2 332.7 35.4
        4. xe-0-1-3-0-border2-van1.etinw.net                0.0% 12 222.7 266.2 206.5 367.7 62.0
        5. xe-1-0-11.mpr1.yvr3.ca.zip.zayo.com              0.0% 11 286.4 271.6 194.5 328.9 37.0
        6. ae7.cs1.sea1.us.zip.zayo.com                     0.0% 11 200.7 214.7 164.9 301.6 40.3
        7. ae27.mpr1.sea1.us.zip.zayo.com                   0.0% 11 259.2 286.8 197.4 495.7 80.2
        8. 72.14.208.172                                    0.0% 11 170.5 282.1 170.5 392.9 81.9
        9. 74.125.243.193                                   0.0% 11 246.6 286.7 181.3 506.6 81.8
        10. 209.85.254.249                                  0.0% 11 346.5 340.5 222.7 516.5 78.0
        11. sea30s02-in-f14.1e100.net                       0.0% 11 252.8 278.0 187.5 412.8 64.1

Conclusion

Any container having network_mode: service:vpn line in its definition will now work exact the same way. You can also test it by removing that line from "net-tool" container, save updated docker-compose.yml file, running docker-compose up -d again and repeat above tests - this time, without network_mode: service:vpn line present traffick will bypass VPN protection.

Help me make LMDS better

With your support anything is possible