Navigating Through Traefik

Navigating Through Traefik

When I first set up this website, and finally got the server up and and running google let me know my work was not yet done and I needed to secure it with HTTPS. All my traffic was unencrypted and vulnerable to anyone who cares enough to intercept it. I asked chatgpt and it recommended traefik so I didn’t bother looking for alternatives, with no experience I didn’t know exactly what I was looking for in a HTTPS-doer (I don’t actually know the term for what traefik is).

My setup consists of running three services in a docker-compose communicating with each other via an internal bridged network. These services included my webserver, wordpress used as a headless CMS (content management system) and a mysql database. The first step was to start small and get the website to come up with HTTPS and configure traefik.

So let’s jump in to what I have and what I learned, keeping my fingers crossed sharing this config isn’t going to help it get hacked!

goapp:
...
  networks:
    - internal
    - web
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.goapp.rule=Host(`ryanjames.io`)"
    - "traefik.http.routers.goapp.entrypoints=websecure"
    - "traefik.http.routers.goapp.tls.certresolver=myresolver"
    - "traefik.http.services.goapp.loadbalancer.server.port=80"
    - "traefik.docker.network=web"

Ok so we have two networks, internal is the bridged network for my containers to talk to each other, traefik doesn’t need to know about this one, and web, the external network for HTTPS communication, this allows traefik to run in a container but to network with the outside world.

I am not 100% sure if this is necessary, maybe I could have just put traefik on the internal network and exposed the ports it needed like 80, 443 etc. Let’s break down how I understand this config, I first give traefik a rule for my goapp, this tells traefik “when incoming traffic is making a request for some resource on the host ryanjames.io, it is to be routed to the goapp container”. Then entrypoints=websecure tells it that the goapp receives traffic from the websecure port (443). The loadbalancer tells traefik that the goapp service is running on port 80 inside the container, so that it knows which port to route to within the container.

One line I would like to point out is the following

traefik.docker.network=web

Originally I didn’t have this line and it took me a bit of time to realise I needed it. The goapp is on two networks, but I only added my traefik container to one network (web), I thought this meant traefik would understand what network to communicate with the goapp one, this wasn’t the case. This issue materialised as an intermittent issue where sometimes my goapp would come up, and continue to work fine, other times it would come up, but traefik would receive 503 errors back from goapp. The reason this happens is that unless you explicitly tell traefik what network to use, it will not know, and even though it is not on the internal network, it can still see the ip address from the goapp on this network and may use that instead of web.

Next we have the traefik config

  traefik:
    image: traefik:v3.1
    command:
      - "--api.insecure=false"
      - "--api.dashboard=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entryPoints.web.address=:80"
      - "--entryPoints.websecure.address=:443"
         ...
      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
      - "--entrypoints.web.http.redirections.entryPoint.scheme=https"
      - "--certificatesresolvers.myresolver.acme.httpchallenge=true"
      - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
      - "--certificatesresolvers.myresolver.acme.email=ryanjames.dev@protonmail.com"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
      - "--log.level=DEBUG"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik_https.rule=Host(`ryanjames.io`)"
      - "traefik.http.routers.traefik_https.entrypoints=traefik"
      - "traefik.http.routers.traefik_https.tls=true"
      - "traefik.http.routers.traefik_https.tls.certresolver=myresolver"
      - "traefik.http.routers.traefik_https.service=api@internal"
         ...
      - "traefik.http.routers.http_traefik.entrypoints=web"
      - "traefik.http.routers.http_traefik.rule=Host(`ryanjames.io`) && PathPrefix(`/dashboard`)"
      - "traefik.http.routers.http_traefik.middlewares=https_redirect"

      - "traefik.http.middlewares.https_redirect.redirectscheme.scheme=https"
      - "traefik.http.middlewares.https_redirect.redirectscheme.permanent=true"
      - "traefik.docker.network=web"
    ports:
      - "80:80"
      - "443:443"
        ...
    networks:
      - web

When using Traefik, you don’t need to expose ports via Docker explicitly for your application containers. The reason is that any containers you want to secure with HTTPS will be routed through traefik, which handles encryption, certificates (with the help of Let’s Encrypt), and port exposure for us, and traefik handles exposing these ports for us. This brought me to a point of confusion that I find a little clearer now. In regular docker we are mapping two ports, the external ports we ask docker to expose, and the ports we run inside the container.

With traefik we add another layer so we have the external ports, the port traefik is listening on and then the port our service runs on inside the container. In the above config these ports don’t say anything about our docker container ports

    ports:
      - "80:80"
      - "443:443"

The left ports are the host ports, for example HTTPS will communicate on 443, and the right hand port will be the port traefik is listening on inside its container. This simply maps external traffic to a port listening within traefik, however it doesn’t hook it together to a specific container. To do this we use entrypoints such as in the goapp

    - "traefik.http.routers.goapp.entrypoints=websecure"

This tells traefik that external traffic on websecure (port 443) should be routed to the goapp container (if it meets the routing rules). It will generally forward it to the equivalent port on the container unless otherwise specific like we do with loadbalancer

    - "traefik.http.services.goapp.loadbalancer.server.port=80"

We also have a traefik dashboard to view our config and set insecure to false to ensure it can’t be accessed via HTTP, I wanted to enable this route via HTTPS with some authorization (not shown here). Most of the setup you will see is similar to the goapp, with the exception that it doesn’t have its own docker-compose service, instead it seems traefik already have it as part of the traefik container and we just need to configure it with labels. This means it doesn’t have a name like `goapp` in this docker-compose.yml, we need to refer to it using

      - "traefik.http.routers.traefik_https.service=api@internal"

Finally we add some config to route from http to HTTPS to prevent any http communication and also make it more convenient if someone accidentally goes to HTTPS. We do this for both the goapp and traefik. Here is the traefik example below

      - "traefik.http.routers.http_traefik.middlewares=https_redirect"

      - "traefik.http.middlewares.https_redirect.redirectscheme.scheme=https"
      - "traefik.http.middlewares.https_redirect.redirectscheme.permanent=true"

After this the goapp is running and we have a working traefik dashboard, all using HTTPS. Feeling more confident I wanted to next add wordpress into this traefik config and have HTTPS enforced on the admin panel too, there was some config required on the wordpress side we will ignore. On the docker-compose.yaml we have the following for wordpress

  wordpress:
    image: wordpress:latest
      ...
    networks:
      - internal
      - web
      ...
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.wordpress.rule=Host(`ryanjames.io`)"
      - "traefik.http.routers.wordpress.entrypoints=wpentry"
      - "traefik.http.routers.wordpress.tls.certresolver=myresolver"
      - "traefik.http.services.wordpress.loadbalancer.server.port=80"
      - "traefik.docker.network=web"

Almost the same as the goapp config, except we use a different entrypoint instead of websecure, as I wanted my wordpress admin panel on another port. We just needed to configure this entrypoint in traefik too

  traefik:
    image: traefik:v3.1
    command:
      - "--api.insecure=false"
      - "--api.dashboard=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entryPoints.web.address=:80"
      - "--entryPoints.websecure.address=:443"
      - "--entryPoints.wpentry.address=:8070"
      - "--entryPoints.traefik.address=:8080"
        ...
    ports:
      - "80:80"
      - "443:443"
      - "8080:8070"
      - "8090:8080"

We created wpentry to listen on the traefik container’s 8070 port, but notice the port mappings, I wanted wordpress on port 8080 (its default), but the traefik dashboard runs on 8080. As traefik and wordpress are on different containers all we need to do is to router the host port 8080 -> 8070 in the wordpress container and the host port 8090 -> 8080 in the traefik container and everything can run on its default port while making them accessible on different ports on the host machine.

Overall this was an interesting learning experience, take my explanations with a pinch of salt as its my first time doing it and by the end of writing this I only now learnt that the term for what traefik is – a reverse proxy.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *