Block Paths With Traefik

By hernil

Note: This post was updated 2024-09-24T13:00:00+02:00 to Traefik v3 syntax.

Limit access to certain paths with traefik routing rules

Jump straight to the solution

Traefik is a reverse proxy and load balancer - most often found in front of a container-based application infrastructure. Most often either straight Docker or using Kubernestes. I started using it as I was under the impression that it was a more modern alternative to using Nginx for the same purpose but after some time I’ve landed on them each having strength and weeknesses.

  • Traefik is somewhat cumbersome to initially configure, and using it to proxy to “external sources” with config files is a bit of a pain. However it shines when using it together with containers - in my case Docker containers and setting labels in their individual docker-compose.yml files that Traefik picks up automatically
  • Nginx is still a superb web server and is the base of quite a few om my containers serving web content. It’s also a powerful proxy but needs explicit interaction and modification of config to pick up and handle new services

The use case

I have some services that are reachable and openly accessible on from the Internet - like this very website. And I have other services that I only want accessible from within an internal network (via VPN). These services are still behind authentication layers, but it’s one additional layer of protection. Although serving content on the Internet is not too hard, and very useful - a good rule of thumb is not exposing a service to the Internet unless you need to.

The best and most effective way of doing that is just not physically connecting that server to the outside Internet. However that is usually quite the hassle and you will at least want a way of connecting with a VPN (if the server is in the cloud) - or having it on your LAN but firewalled off (and usually behind NAT) if hosted at home.

Now there’s another caveat and that is accessing services over https. With Let’s Encrypt this is now orders of magnitude easier than a decade ago. It’s even baked right into traefik. Now having both https with valid certificates and avoiding exposing the service to the Internet is a bit more tricky. Either you need to get get a wildcard certificate on your domain and manage that somehow, or you can let traefik manage it and use a middleware to avoid the service being reachable from the Internet. Note that this is inherently easier to mess up than just never connecting traefik to it in the first place. But the end result is clean and convenient.

This is a matter of defining a middleware in your traefik configuration:

[http.middlewares]
  [http.middlewares.lan-only.ipAllowList]
    sourceRange = ["192.168.2.1/24"]

and applying it to a container like so

labels:
      - traefik.http.routers.my-service.rule=Host(`service.mydomain.com`)
      - traefik.http.routers.my-service.tls=true
      - traefik.http.routers.my-service.tls.certresolver=lets-encrypt
      - traefik.http.routers.my-service.middlewares=lan-only@file # <- this line here 

This will tell traefik to only allow trafic originating from the 192.168.2.1/24 address range to access the my-service service. Any IP-address outside this range will be denied access with a http 403 return code. Obviously your client should have an IP in this range if you want to access the service.

The challenge

This setup is just fine and I’m using this for everything that shouldn’t be accessible to others. But recently I spun up a container that needs to be partially available publicly. But the service also has a dashboard and API that has no reason to be available online as I’m the only one that need access.

The solution

Traefik let’s us define multiple routers per container in our setup. And the more spesific one takes precedence over the more general one so simply specifying an additional router for the endpoints you want protected let’s us shield it a bit more from the Internet. It simply looks like this.

labels:
      # the main 'my-service' router that handles the trafic from the Internet
      - traefik.http.routers.my-service.rule=Host(`service.mydomain.com`) 
      # the 'my-service-admin' router catching trafic to the '/dashboard', '/login' and '/api/auth' paths
      - traefik.http.routers.my-service-admin.rule=(Host(`service.mydomain.com`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api/auth`) ||  PathPrefix(`/login`)))
      # applying the lan-only middleware to the 'my-service-admin' router
      - traefik.http.routers.my-service-admin.middlewares=lan-only@file
      - traefik.http.routers.my-service.tls=true
      - traefik.http.routers.my-service.tls.certresolver=lets-encrypt
      - traefik.http.routers.my-service-admin.tls=true
      - traefik.http.routers.my-service-admin.tls.certresolver=lets-encrypt

Note that you could also define the middleware directly in the Docker labels (see documentation) - but as I apply it to many containers across many docker-compose files I prefer to define it in a central config.

Disclaimer

This should only be one layer in your Swiss cheese security model. I wouldn’t go ahead and disable authentication from your service even if it’s now “LAN-only”. Any number of things could go wrong, like changes or bugs in how Traefik handles these configs after an update, or some misconfiguration either in software or in physical wiring that could let other clients in the building be on the “secure” network. So again, this is not your only - or even main - layer of security. Just an additional one you can add.


Input or feedback to this content?
Reply via email!
Related Articles