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
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"
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"
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:
- 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")
- 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.
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