Adding Basic Auth to secure Traefik dashboard fails - otherwise working docker compose

Hello!

I use docker compose, dns validation through cloudflare, and wildcard DNS. I try to do all configuration through docker-compose.yml. Everything works correctly. I can access the traefik dashboard, and all docker services with labels configured are properly redirected. I am now trying to add basic auth to protect access to the dashboard.

I generated a user account and hashed password using

htpasswd -nBC 10 admin mypassword

I then created a /credentials.txt file and put the info in there.

admin:mylonghashedpassword

I then appended the following lines to the labels section of my traefik container:

- 'traefik.http.middlewares.auth.basicauth.usersfile=/credentials.txt'
- 'traefik.http.middlewares.traefik-https-chain.chain.middlewares=auth'
- 'traefik.http.routers.traefik-https.middlewares=traefik-https-chain'

Here is my docker compose file:

version: '3.7'
services:
  traefik:
    image: traefik:latest
    container_name: traefik
    ports:
      - "80:80"
      - "443:443"
    networks:
      - proxy
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./letsencrypt:/letsencrypt
      - "/configs/traefik/credentials.txt:/credentials.txt:ro"

    command:
      - --api.insecure=false
      - --api.dashboard=true
      - --serversTransport.insecureSkipVerify=true
      - --api.debug=true
      - --providers.docker=true
      - --providers.docker.swarmMode=false
      - --providers.docker.network=proxy
      - --providers.docker.exposedByDefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecured.address=:443
      - --entrypoints.web.http.redirections.entryPoint.to=websecured
      - --entrypoints.web.http.redirections.entryPoint.scheme=https
      - "--certificatesresolvers.le.acme.dnschallenge=true"
      - "--certificatesresolvers.le.acme.httpChallenge=false"
      - "--certificatesresolvers.le.acme.tlsChallenge=false"
      - "--certificatesresolvers.le.acme.dnschallenge.provider=cloudflare"
      - "--certificatesresolvers.le.acme.email=MYEMAIL"
      - "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.le.acme.httpChallenge.entryPoint=web"

    environment:
      - CF_API_EMAIL=MYEMAIL
      - CF_API_KEY=MYAPIKEY

    labels:
        - 'traefik.enable=true'
        - 'traefik.http.routers.traefik.rule=Host(`traefik.mydomain.com`)'
        - 'traefik.http.middlewares.auth.basicauth.usersfile=/credentials.txt'
        - 'traefik.http.routers.traefik.tls=true'
        - 'traefik.http.routers.traefik.tls.certresolver=le'
        - 'traefik.http.routers.traefik.service=api@internal'
        - 'traefik.http.services.api.loadbalancer.server.port=8080'
        - 'traefik.http.routers.traefik.tls.domains[0].main=mydomain.com'
        - 'traefik.http.routers.traefik.tls.domains[0].sans=*.mydomain.com'
        - 'traefik.http.middlewares.traefik-https-chain.chain.middlewares=auth'
        - 'traefik.http.routers.traefik-https.middlewares=traefik-https-chain'

I recreate traefik container, but I can still access the dashboard without a prompt for user/password. Super frustrating! Any advice would be very appreciated.

Solved! This post was extremely helpful in figuring this out.

Here is how to recreate my setup for anyone in a similar boat later!

Features of my setup:

Docker-Compose.yml based configuration (no separate files!)
Cloudflare DNS Validation
Wildcard Cert Generation for subdomains
Redirect to HTTPS
Basic Auth via declared credentials file (this avoids the $$ vs $ drama)

Folder Structure:

/traefik
---credentials.txt
---docker-compose.yml
---/letsencrypt
---------acme.json

mkdir traefik
cd traefik
touch credentials.txt
touch docker-compose.yml
mkdir letsencrypt
cd letsencrypt
touch acme.json
chmod 600 acme.json

Generate your user/pass for basic auth:

htpasswd -nBC 10 <myusername> <mypassword>

Paste the results in your credentials file.

Final working docker-compose.yml:

version: '3.7'
services:
  traefik:
    image: traefik:latest
    container_name: traefik
    ports:
      - "80:80"
      - "443:443"
    networks:
      - proxy
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./letsencrypt:/letsencrypt
      - "/traefik/credentials.txt:/credentials.txt:ro" #Update to your path

    command:
      - --api.insecure=false
      - --api.dashboard=true
      - --serversTransport.insecureSkipVerify=true
      - --api.debug=true
      - --providers.docker=true
      - --api
      - --providers.docker.swarmMode=false
      - --providers.docker.network=proxy
      - --providers.docker.exposedByDefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecured.address=:443
      - --entrypoints.web.http.redirections.entryPoint.to=websecured
      - --entrypoints.web.http.redirections.entryPoint.scheme=https
      - "--certificatesresolvers.le.acme.dnschallenge=true"
      - "--certificatesresolvers.le.acme.httpChallenge=false"
      - "--certificatesresolvers.le.acme.tlsChallenge=false"
      - "--certificatesresolvers.le.acme.dnschallenge.provider=cloudflare"
      - "--certificatesresolvers.le.acme.email=myemail@emailprovider.com" #Update to your email
      - "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.le.acme.httpChallenge.entryPoint=web"

    environment:
      - CF_API_EMAIL=myemail@emailprovider.com #Update to your cloudflare email
      - CF_API_KEY=MYAPI #Update to your cloudflare API key

    labels:
        - 'traefik.enable=true'
        - 'traefik.http.routers.traefik.rule=Host(`traefik.mydomain.com`)' #Update to your domain
        - 'traefik.http.middlewares.auth.basicauth.usersfile=/credentials.txt'
        - 'traefik.http.routers.traefik.tls=true'
        - 'traefik.http.routers.traefik.tls.certresolver=le'
        - 'traefik.http.routers.traefik.service=api@internal'
        - 'traefik.http.services.api.loadbalancer.server.port=8080'
        - 'traefik.http.routers.traefik.tls.domains[0].main=mydomain.com' #Update to your domain
        - 'traefik.http.routers.traefik.tls.domains[0].sans=*.mydomain.com' #Update to your domain
        - "traefik.http.routers.traefik.middlewares=auth"
        - 'traefik.http.middlewares.traefik-https-chain.chain.middlewares=auth' #add future additional middlewares here with comma separated values
        - 'traefik.http.routers.traefik-https.middlewares=traefik-https-chain'
		
networks:
  proxy:
    external: true

Example exposing Tautulli container to web at tautulli.mydomain.com:

tautulli:
    image: ghcr.io/linuxserver/tautulli:latest
    container_name: tautulli
    networks:
      - proxy
    restart: unless-stopped
    ports:
      - 8181:8181
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.tautulli.rule=Host(`tautulli.mydomain.com`)" #update to your domain
      - "traefik.http.routers.tautulli.entrypoints=websecured"
      - "traefik.http.routers.tautulli.tls.certresolver=le"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=America/New York
    volumes:
      - /configs/tautulli:/config

Just thinking out loud --

volume declaration -- I dont think you need quotes around:
"/configs/traefik/credentials.txt:/credentials.txt:ro" -- I think it can be:
/configs/traefik/credentials.txt:/credentials.txt:ro

Next your labels are kind of distracting however I think its something like:

      - "traefik.enable=true"
      - "traefik.docker.network=docker-net"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.mydomain.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
      - "traefik.http.routers.dashboard.tls=true"
      - "traefik.http.routers.dashboard.tls.options=modern@file"
      - "traefik.http.routers.dashboard.tls.certresolver=le"
      - "traefik.http.routers.dashboard.tls.domains[0].main=traefik.mydomain.com"
      - "traefik.http.routers.dashboard.tls.domains[0].sans=traefik.mydomain.com"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.middlewares=auth"
      - "traefik.http.middlewares.auth.basicauth.usersfile=/etc/traefik/basicauth.pass"
      - "traefik.http.routers.dashboard.entrypoints=web,websecure"

You can change some of my entries to match your configuration

Finally within the traefik.yml static configuration file:

api:
  insecure: false
  debug: true
  dashboard: true

Appreciate the help! I actually figured it out. I did a full write up, and posted it as a reply. It's still being reviewed by mods though. Maybe because it was pretty lengthy and I'm new to the forums.

1 Like

Lets hope they give it a green light soon!

This inspired me to kick start a blog to document projects like this, and share them with others.

I’ll refine the website, but the basic write up is available here:

https://www.noblanks.com/blog/traefik-reverse-proxy-setup-guide

1 Like

Good work! I still want to point you to two great Traefik posts that really made me click:

The biggest difference is that he uses the static file, Traefik.yml for the stuff that should reside there, IMHO.
That makes the other files so much easier to read, and thus understand.

1 Like

Cool thanks! I tried the Traefik.yml route, but found it less convenient personally. With this approach I can integrate the traefik compose commands straight into my portainer stack. Makes it easy for me to make adjustments on the fly, instead of manually editing files.

What you are missing is that the changes in the traefik.yml file is automatically picked up by Traefik. Having them in the docker-compose file means you have to restart the container to pickup the changes.

Traefik has both a static and dynamic configuration setup. Changes made within the static configuration file are not dynamically picked up, where as the changes made in the dynamic configuration file are automatically picked up without have to restart containers. I'm aware this "split" of configuration files seems like a pain, however it just fits into the traefik definition of things.