Adding HTTP basic authentication using traefik middlewares

On last post I added traefik to my monitoring lab in order to use it as a reverse proxy. Some of the tools used on the monitoring lab lack of any authentication mechanism, so is a bit risky exposing them, including on your own infrastructure, for instance, cadvisor.

Traefik has some pieces called middlewares to provide some extra functionality, like http authentication among others.

What are traefik’s middlewares

According to it’s own definition in traefik’s docs

traefik middlewares schema Attached to the routers, pieces of middleware are a means of tweaking the requests before they are sent to your service (or before the answer from the services are sent to the clients).

But, from my point of view as an experienced web server admin: middlewares are those lines of configuration copied from one vhost to another.

Yes, they are rewriting rules, redirects, auth, rate limiters, etc.

Adding basic auth to containers

It took me longer than expected to understand the different labels components available, but I found a very good configuration example on teslamate’s docs.

The first step is to create an apache style user-password pair:

$ htpasswd -nB traefik 
New password: 
Re-type new password: 
admin:$2y$05$lcbgFtKew0U57IFKa77AButqDLWqHCfc2m1HfvtJP.5SIg9iChs0C

Note: if you don’t have any host with apache-tools installed, you can go to Htpasswd generator.

Once I got the chain, the first step was to escape the dollar signs, so $2y$05$... became $$2y$$05$$....

Then I put it on every service on my docker-compose.yml I wanted to protect.

  whoami:
    # A container that exposes an API to show its IP address
    image: traefik/whoami
    labels:
      - "traefik.http.routers.whoami.rule=Host(\"whoami.docker.garmo\")"
      - "traefik.http.routers.whoami.middlewares=whoami-auth"
      - "traefik.http.middlewares.whoami-auth.basicauth.users=admin:$$2y$$05$$lcbgFtKew0U57IFKa77AButqDLWqHCfc2m1HfvtJP.5SIg9iChs0C"
      - "traefik.http.middlewares.whoami-auth.basicauth.realm=traefik at Juanjo's"

I had to add two lables, one for the declaring the user on the middelware:

- "traefik.http.middlewares.whoami-auth.basicauth.users=admin:$$2y$$05$$lcbgFtKew0U57IFKa77AButqDLWqHCfc2m1HfvtJP.5SIg9iChs0C"

And other to tell the existing router definition to use the middleware:

- "traefik.http.routers.whoami.middlewares=whoami-auth"

whoami-auth is the name I chose for the middleware instance, and can be almost anything you like if you keep it in the alpha-plus-dash scheme.

There is a an optional property for the basicauth middleware called real and you guess it, it’s function is to define the authentication realm.

- "traefik.http.middlewares.whoami-auth.basicauth.realm=traefik at Juanjo's"

authentication realm

Putting traefik dashboard behind trafik’s authentication is a bit more tricky, but is a good example to show how to protect services running in other port than the exposed by the image.

  traefik:
    ...
    labels:
      - "traefik.http.routers.traefik.rule=Host(`traefik.docker.garmo`)"
      - "traefik.http.routers.traefik.middlewares=traefik-auth"
      - "traefik.http.routers.traefik.service=traefik-svc"
      - "traefik.http.middlewares.traefik-auth.basicauth.users=admin:$$2y$$05$$lcbgFtKew0U57IFKa77AButqDLWqHCfc2m1HfvtJP.5SIg9iChs0C"
      - "traefik.http.services.traefik-svc.loadbalancer.server.port=8080"

In this case, I had to define a service and specify in which port is the container listening for connections, 8080 in my case (default traefik’s API port):

- "traefik.http.services.traefik-svc.loadbalancer.server.port=8080"

And then I told the router instance to use the service. Again traefik-svc is the name I chose, not something pre-established:

- "traefik.http.routers.traefik.service=traefik-svc"

traefik screenshot

Having the user’s password on the compose file doesn’t look clean, so I tested the usersFile option on basicauth middleware. This is a two step change:

  1. Adding the label on the target container:
  whoami:
    image: traefik/whoami
    labels:
      traefik.http.middlewares.whoami-auth.basicauth.realm: traefik at Juanjo's
      traefik.http.middlewares.whoami-auth.basicauth.usersFile: /htusers/master.htpasswd
      traefik.http.routers.whoami.middlewares: whoami-auth
      traefik.http.routers.whoami.rule: Host("whoami.docker.garmo")
  1. Make the file available on traefik container:
  traefik:
  ...
    volumes:
    ...
    - ./traefik/htusers:/htusers:z

I decided to mount a folder instead of a file to get two benefits:

  • Binded files are not updateable from outside the container.
  • Binding a directory allows having several files, for instance, one for each protected service.

Then I created the directory hierarchy and used htpassword to create the master.htpasswd file.

htpasswd -Bbc ./traefik/htusers/master.htpasswd sysop apassword

It worked but I wasn’t able to update the password or add users. I found some users on forums having the same issue, but no answers. But there is and old trick all seasoned linux administrators are aware, is not written anywhere, but most programs reload their configuration on SIGHUP signal. Guess what? traefik also reloads the usersFile.

reload configuration screenshot

Conclusion

Traefik give’s you a simply and clean way to forget about web authentication. It’s basic, but it’s way better than no authentication at all. Its a shame LDAP support is only available on Traefik Enterprise, but I understand if someone is doing the hard work, and is doing it well, they should be paid.

The complete docker-compose.yml

version: '3'
services:
  prometheus:
    image: prom/prometheus:latest
    labels:
      - "traefik.http.routers.prometheus.rule=Host(`prometheus.docker.garmo`)"
      - "traefik.http.routers.prometheus.middlewares=prometheus-auth"
      - "traefik.http.middlewares.prometheus-auth.basicauth.usersFile=/htusers/master.htpasswd"
      - "traefik.http.middlewares.prometheus-auth.basicauth.realm=traefik at Juanjo's"
    volumes:
      - ./prometheus/etc/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro,z
      - ./prometheus/prometheus:/prometheus:Z
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--web.enable-lifecycle'
    restart: unless-stopped
  cadvisor:
    image: google/cadvisor:latest
    labels:
      - "traefik.http.routers.cadvisor.rule=Host(`cadvisor.docker.garmo`)"
      - "traefik.http.routers.cadvisor.middlewares=cadvisor-auth"
      - "traefik.http.middlewares.cadvisor-auth.basicauth.usersFile=/htusers/master.htpasswd"
      - "traefik.http.middlewares.cadvisor-auth.basicauth.realm=traefik at Juanjo's"
    privileged: true
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:rw
      - /sys/fs/cgroup/:/sys/fs/cgroup/
      - /var/lib/docker/:/var/lib/docker:ro
    restart: unless-stopped
  grafana:
    image: grafana/grafana
    labels:
      - "traefik.http.routers.grafana.rule=Host(\"grafana.docker.garmo\")"
    user: "1000"
    environment:
      - "GF_SECURITY_ADMIN_PASSWORD=grafana"
    volumes:
      - ./grafana/var/lib/grafana:/var/lib/grafana:Z
    depends_on:
      - prometheus
    restart: unless-stopped
  influxdb:
    image: influxdb:1.8-alpine
    labels:
      - "traefik.http.routers.influxdb.rule=Host(`influx.docker.garmo`)"
      - "traefik.http.routers.influxdb.middlewares=influx-auth"
      - "traefik.http.middlewares.influx-auth.basicauth.usersFile=/htusers/master.htpasswd"
      - "traefik.http.middlewares.influx-auth.basicauth.realm=traefik at Juanjo's"
    volumes:
      - ./influxdb/etc/influxdb/influxdb.conf:/etc/influxdb/influxdb.conf:ro,z
      - ./influxdb/var/lib/influxdb:/var/lib/influxdb:z
    restart: unless-stopped
  traefik:
    # The official v2 Traefik docker image
    image: traefik:v2.4
    labels:
      - "traefik.http.routers.traefik.rule=Host(`traefik.docker.garmo`)"
      - "traefik.http.routers.traefik.middlewares=traefik-auth"
      - "traefik.http.routers.traefik.service=traefik-svc"
      - "traefik.http.middlewares.traefik-auth.basicauth.usersFile=/htusers/master.htpasswd"
      - "traefik.http.middlewares.traefik-auth.basicauth.realm=traefik at Juanjo's"
      - "traefik.http.services.traefik-svc.loadbalancer.server.port=8080"
    # Enables the web UI and tells Traefik to listen to docker
    command: --api.insecure=true --providers.docker
    restart: unless-stopped
    ports:
      # The HTTP port
      - "80:80"
      # The Web UI (enabled by --api.insecure=true)
      - "8080:8080"
    volumes:
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock:z
      - ./traefik/htusers/:/htusers:z
    networks:
      - default
      - es-docker_default
  whoami:
    # A container that exposes an API to show its IP address
    image: traefik/whoami
    restart: unless-stopped
    labels:
      - "traefik.http.routers.whoami.rule=Host(\"whoami.docker.garmo\")"
      - "traefik.http.routers.whoami.middlewares=whoami-auth"
      - "traefik.http.middlewares.whoami-auth.basicauth.usersFile=/htusers/master.htpasswd"
      - "traefik.http.middlewares.whoami-auth.basicauth.realm=traefik at Juanjo's"
 
networks:
  es-docker_default:
     external: true