Making a service only accessible when connected to vpn via wg-easy

How can I simply make a service only accessible when I'm connected to a vpn?

I have:

  • wildcard certs
  • wg-easy running on vpn.mydomain.com (so I can see the interface to add clients)
  • whoami running on whoami.mydomain.com

Problem:

I tried an ipAllowList middleware with the source range set to 10.0.0.0/24. The problem is it seems that the IP is already seen as local, because it's routed through traefik maybe?

From whoami.mydomain.com:

IP: 127.0.0.1
IP: ::1
IP: 10.0.1.127
IP: 172.18.0.4
RemoteAddr: 10.0.1.4:40830
GET / HTTP/1.1

I note that when I connect to the vpn, nothing in this list changes. The IPs stay the same.

I have tried:

  • Setting the IP whitelist middleware to 172.18.0.0/32. Then I get a Forbidden!! But being on the VPN makes no difference and I can't get past the Forbidden then lol.

How can I:

ONLY access whoami, when I'm on the vpn, otherwise receive a Forbidden or whatever.

So, what do I need to change to make it so that I can only access that whoami container when I'm connected to the vpn? Is it that it has to be in another, separate docker swarm network that traefik also need to be in?

--

Relevant configs:

dynamic configuration
http:
  routers:
    vpn:
      rule: Host(`vpn.{{env "DOMAIN"}}`)
      service: vpn
      entryPoints:
        - https
      tls:
        certResolver: lets_encrypt_resolver
        domains:
          - main: {{env "DOMAIN"}}
          - sans: '*.{{env "DOMAIN"}}'
    whoami:
      rule: Host(`whoami.{{env "DOMAIN"}}`)
      service: whoami
      middlewares:
        - ipwhitelist-vpn
      entryPoints:
        - https
      tls:
        certResolver: lets_encrypt_resolver
        domains:
          - main: {{env "DOMAIN"}}
          - sans: '*.{{env "DOMAIN"}}'

  services:
    vpn:
      loadBalancer:
        servers:
          - url: 'http://wg-easy:51821'
    whoami:
      loadBalancer:
        servers:
          - url: 'http://whoami:80'

  middlewares:
    ipwhitelist-vpn:
      ipAllowList:
        sourceRange:
          - "10.0.0.0/24"
docker-compose
  wg-easy:
    image: ghcr.io/wg-easy/wg-easy
    env_file:
      - /prod.env
    networks:
      - app_network
    environment:
      WG_HOST: "vpn.${DOMAIN}"
      WG_DEVICE: eth2
    ports:
      - "51820:51820/udp"
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    sysctls:
      - net.ipv4.ip_forward=1
      - net.ipv4.conf.all.src_valid_mark=1
    deploy:
      placement:
        constraints: [ node.role == manager ]
      replicas: 1
      restart_policy:
        condition: on-failure

  whoami:
    image: "traefik/whoami"
    env_file:
      - /prod.env
    networks:
      - app_network
    deploy:
      placement:
        constraints: [ node.role == worker ]
      replicas: 1
      restart_policy:
        condition: on-failure

networks:
  app_network:
    driver: overlay
    driver_opts:
      encrypted: "true"

--
Example threads where discussions exist, but no config or concrete answer is talked about:

  • Expose Traefik services via VPN only
  • Here's a reddit example where OP is extremely clear in his request, he responds to each person doing exactly what they say with "here is my new config, here is the output, should I have expected a different output?" and... no response there either. Maybe related.

I have read so much here, but it seems like this is an open secret where we only talk in abstractions. I would love to get this working for my mini project.

Alright, I'm very happy that after years, I figured it out and I'd like to share it.

The first clue was: connecting and disconnecting from the vpn showed no change when going to whoami.mydomain.com. So the key here is to get that to change.

Turns out when you're using docker swarm, and traefik is in it, and you're just normally exposing ports 80 and 443, the "real IP" doesn't get forwarded through. So, you need to update your docker-compose file stack to use "host" mode:

ports:
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host

Once that's done and you redeploy, you'll see that connecting and disconnecting from the VPN does indeed change the IP address in the X-Forwarded-For header.

X-Forwarded-For: 172.18.0.1

Next, now that you see a different IP when you connect / disconnect from the VPN, the CIDR for that is used in the whitelist middleware.

I didn't know too much about middleware CIDRs so I did a bit of research. I originally thought putting /32 of an IP meant that it would cater to all 172.x.x.x IPs but no.. actually /32 is MORE specific and would require that EXACT IP to be used.

So, you put /8 on the end to allow all IPs from 172.x.x.x.

The middleware whitelist now looks like this:

  middlewares:
    ipwhitelist-vpn:
      ipAllowList:
        sourceRange:
          - "172.0.0.0/8"

Stick that in your dynamic config and make your whoami router use it and there you go! FINALLY.

--

The caveat here is that it's less secure than using overlay networking modes, because it gives containers direct asccess to the host network stack. This means they bypass Docker's virtual network isolation, so containers can interact with the host network resources directly. But I trust traefik, so hopefully no issue.

But if anyone figures out how to do this without using host network mode, I'd love to hear. Maybe this is a starting point?

1 Like

This topic was automatically closed 3 days after the last reply. New replies are no longer allowed.