Setting up Traefik with SSL certificates for a Homelab

Search for a command to run...

No comments yet. Be the first to comment.
Due to requirements in the workplace for a specific application where the staging/testing server wasn’t able to be added into the deployment group of a pipeline (security risk for on-premise server access) I had to find a way to have a container auto...
It can be quite tricky to manage multiple services on a single Docker host and may involve a lot of manual intervention for troubleshooting, restarting services and updating images to the latest versions I've built up a list of scripts over time that...
Why Docker? Docker is currently the best tool that I've found for the job of hosting linux containers. It creates a repeatable system that is easy to deploy.These are the steps that the system uses to deploy the software: A "Dockerfile" Instructio...
Introduction As part of my company's migration from Autodesk Vault as an Engineering management system, I was in charge of developing a method for extracting all of the files and metadata to be prepared for importation into the new system. Our reas...

Traefik is a reverse proxy that allows you to have all of your web services behind a single front end. It can be used for easy management of Docker based services (with automatic SSL certificate generation), and also to handle external services (located on another device/IP or hosted in Docker).
The main benefits (and the reasons that I am using Traefik) are:
These are the requirements to establish the setup that I currently have for Traefik:
The dashboard for Traefik is the best way to check how all of the traffic and routing is working and for troubleshooting services or access The documentation for the dashboard can be found here
The main page of the dashboard will show useful information about the services and ports that it is listening on (example below listens on Port 80/443 for web and also 8888 for Metric information):

When setting up a service to be proxied by Traefik, there are a few things to consider that can be illustrated by the below flow diagram:

Inside of the docker-compose.yml file for each container, I will specify the labels that are used to control the different requirements of how it will be handled by Traefik
Here is the docker-compose.yml for my Plex:
version: '3.3'
services:
plex:
image: linuxserver/plex
container_name: plex
ports:
# Plex DLNA Server
- 1900:1900/udp
- 32410:32410/udp
- 32412:32412/udp
- 32413:32413/udp
- 32414:32414/udp
labels:
- plex
- "traefik.enable=true"
- "traefik.http.routers.plex.rule=Host(`plex.domain`)"
- "traefik.http.routers.plex.entrypoints=websecure"
- "traefik.http.routers.plex.tls.certresolver=myresolver"
- "traefik.http.services.plex.loadbalancer.server.port=32400"
- "traefik.frontend.rule=Host:traefik.domain"
- "traefik.http.routers.plex.middlewares=middlewares-rate-limit@file,middlewares-ipwhilelist@file"
networks:
- web
environment:
- PLEX_UID=1000
- PLEX_GID=1000
- UMASK=022 #optional
- PLEX_CLAIM=claim-XXXXXXXXXXXXXXXXXX
restart: unless-stopped
volumes:
- cifs-plex:/config
- cifs-movies:/movies:ro
- cifs-tv:/tv:ro
volumes:
cifs-plex:
external: true
cifs-movies:
external: true
cifs-tv:
external: true
networks:
web:
external: true
There are quite a few sections in here, but the main one I will be looking at is the "labels":
plex Used to label the container name for easy management)
traefik.enable=true Tell Traefik to watch this container
traefik.http.routers.plex.rule=Host('plex.domain') DNS hostname to use
traefik.http.routers.plex.entrypoints=websecure Use the "websecure" entrypoint (HTTPs port as seen in the Dashboard screenshot)
traefik.http.routers.plex.tls.certresolver=myresolver The certificate resolver to use, all of mine use the "certresolver" in the Traefik Docker command for Lets' Encrypt
traefik.http.services.plex.loadbalancer.server.port=32400 The port of the client if multiple or non-standard (Plex web uses 32400 and has multiple other ports available)
traefik.frontend.rule=Host:traefik.domain The name of the Traefik host, same on all sites
traefik.http.routers.plex.middlewares=middlewares-rate-limit@file,middlewares-ipwhilelist@file Middleware to be used, this one will limit requests and also only allow internal IP addresses to access the service (set in my rules.yaml file)
Also note above that this container has the router's specified as traefik.http.routers.plex.* while other containers will need their own name replacing plex to ensure that router's don't overlap
A note to make in the example above is that is a docker container listens on multiple ports (running a docker ps will show the ports that the container is listening on) or a non-standard web port then you will need to specify it in the traefik.http.services.NAME.loadbalancer.server.port label to allow Traefik to correctly point to it
Example here is checking the Service for Plex to ensure that it uses the correct port (32400)

Once we have the services setup correctly, we would like a valid certificate to appear in the title bar to ensure extra security is applied to the site. For each of the services that you want to have a certificate generated for, you will specify in the labels which certificate resolver to use For Traefik I use the "myresolver" as the name, and pass the parameters to the Traefik docker-compose.yml (full config file can be seen at the bottom of this page)
services:
traefik:
labels:
- "--certificatesresolvers.myresolver.acme.dnschallenge=true"
- "--certificatesresolvers.myresolver.acme.dnschallenge.provider=dreamhost"
- "--certificatesresolvers.myresolver.acme.email=email@domain.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
environment:
- "DREAMHOST_API_KEY=XXXXXXXXXXXX"
Once Traefik is aware of the certificate resolver to use, you can simply add the traefik.http.routers.plex.tls.certresolver=myresolverlabel to the container as in the Plex example above and it should place the certificate in the letsencrypt/acme.json file automatically
Once you have the certificates applied correctly to the connected containers, you can look into adding extra security to the TLS connections used. A good site to test with (if your domains are publicly resolvable) is SSL Labs
With the following configuration I am currently receiving an "A" score for SSL (as of 29th March 2022)
Traefik docker-compose.yml:
labels:
- "traefik.http.middlewares.traefik-headers.headers.accesscontrolallowmethods=GET, OPTIONS, PUT"
- "traefik.http.middlewares.traefik-headers.headers.accesscontrolalloworiginlist=https://abowden.net"
- "traefik.http.middlewares.traefik-headers.headers.accesscontrolmaxage=100"
- "traefik.http.middlewares.traefik-headers.headers.addvaryheader=true"
- "traefik.http.middlewares.traefik-headers.headers.allowedhosts=traefik.domain"
- "traefik.http.middlewares.traefik-headers.headers.hostsproxyheaders=X-Forwarded-Host"
- "traefik.http.middlewares.traefik-headers.headers.sslredirect=true"
- "traefik.http.middlewares.traefik-headers.headers.sslhost=traefik.domain"
- "traefik.http.middlewares.traefik-headers.headers.sslforcehost=true"
- "traefik.http.middlewares.traefik-headers.headers.sslproxyheaders.X-Forwarded-Proto=https"
- "traefik.http.middlewares.traefik-headers.headers.stsseconds=63072000"
- "traefik.http.middlewares.traefik-headers.headers.stsincludesubdomains=true"
- "traefik.http.middlewares.traefik-headers.headers.stspreload=true"
- "traefik.http.middlewares.traefik-headers.headers.forcestsheader=true"
- "traefik.http.middlewares.traefik-headers.headers.framedeny=true"
- "traefik.http.middlewares.traefik-headers.headers.contenttypenosniff=true"
- "traefik.http.middlewares.traefik-headers.headers.browserxssfilter=true"
- "traefik.http.middlewares.traefik-headers.headers.referrerpolicy=same-origin"
- "traefik.http.middlewares.traefik-headers.headers.featurepolicy=camera 'none'; geolocation 'none'; microphone 'none'; payment 'none'; usb 'none'; vr 'none';"
- "traefik.http.middlewares.traefik-headers.headers.customresponseheaders.X-Robots-Tag=none,noarchive,nosnippet,notranslate,noimageindex,"
Traefik rules.yaml:
tls:
options:
default:
minVersion: VersionTLS12
cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 # TLS 1.2
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 # TLS 1.2
- TLS_AES_256_GCM_SHA384 # TLS 1.3
- TLS_CHACHA20_POLY1305_SHA256 # TLS 1.3
curvePreferences:
- CurveP521
- CurveP384
sniStrict: true
Full copies of the config files are available at the bottom of this page
Middleware is used in the middle of the request process, to make a change or add extra controls to the request as it is passing through
In my case I am using the following middlewares to add extra security:

version: "3.3"
networks:
web:
external: true
services:
traefik:
image: "traefik:v2.5.3"
container_name: "traefik"
restart: unless-stopped
command:
- "--api=true"
- "--providers.docker=true"
# Do not expose containers unless explicitly told so
- "--providers.docker.exposedbydefault=false"
- "--providers.file=true"
- "--providers.file.filename=/etc/traefik/rules.yaml"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
- "--entrypoints.web.http.redirections.entrypoint.permanent=true"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.websecure.http.middlewares=middlewares-basic-auth@file"
- "--entrypoints.metrics.address=:8888"
- "--metrics.prometheus=true"
- "--metrics.prometheus.entryPoint=metrics"
- "--certificatesresolvers.myresolver.acme.dnschallenge=true"
- "--certificatesresolvers.myresolver.acme.dnschallenge.provider=dreamhost"
- "--certificatesresolvers.myresolver.acme.email=email@domain.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
- "--serverstransport.insecureskipverify=true"
- "--accessLog.filePath=/logs/access.log"
security_opt:
- "no-new-privileges:true"
labels:
- "traefik=name"
- "traefik.enable=true"
# Middleware
- "traefik.http.routers.traefik-rtr.middlewares=traefik-headers,middlewares-rate-limit@file,middlewares-basic-auth@file"
- "traefik.http.middlewares.traefik-headers.headers.accesscontrolallowmethods=GET, OPTIONS, PUT"
- "traefik.http.middlewares.traefik-headers.headers.accesscontrolalloworiginlist=https://abowden.net"
- "traefik.http.middlewares.traefik-headers.headers.accesscontrolmaxage=100"
- "traefik.http.middlewares.traefik-headers.headers.addvaryheader=true"
- "traefik.http.middlewares.traefik-headers.headers.allowedhosts=traefik.domain"
- "traefik.http.middlewares.traefik-headers.headers.hostsproxyheaders=X-Forwarded-Host"
- "traefik.http.middlewares.traefik-headers.headers.sslredirect=true"
- "traefik.http.middlewares.traefik-headers.headers.sslhost=traefik.domain"
- "traefik.http.middlewares.traefik-headers.headers.sslforcehost=true"
- "traefik.http.middlewares.traefik-headers.headers.sslproxyheaders.X-Forwarded-Proto=https"
- "traefik.http.middlewares.traefik-headers.headers.stsseconds=63072000"
- "traefik.http.middlewares.traefik-headers.headers.stsincludesubdomains=true"
- "traefik.http.middlewares.traefik-headers.headers.stspreload=true"
- "traefik.http.middlewares.traefik-headers.headers.forcestsheader=true"
- "traefik.http.middlewares.traefik-headers.headers.framedeny=true"
- "traefik.http.middlewares.traefik-headers.headers.contenttypenosniff=true"
- "traefik.http.middlewares.traefik-headers.headers.browserxssfilter=true"
- "traefik.http.middlewares.traefik-headers.headers.referrerpolicy=same-origin"
- "traefik.http.middlewares.traefik-headers.headers.featurepolicy=camera 'none'; geolocation 'none'; microphone 'none'; payment 'none'; usb 'none'; vr 'none';"
- "traefik.http.middlewares.traefik-headers.headers.customresponseheaders.X-Robots-Tag=none,noarchive,nosnippet,notranslate,noimageindex,"
ports:
- "80:80"
- "443:443"
# Metrics
- "8888:8888"
networks:
- web
environment:
- "DREAMHOST_API_KEY=XXXXXXXXXXXX"
volumes:
- "./letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./rules.yaml:/etc/traefik/rules.yaml:ro"
- "./logs:/logs"
http:
routers:
router-traefik:
entryPoints:
- websecure
rule: Host(`dashboard.domain`)
service: api@internal
middlewares:
- "middlewares-rate-limit@file"
- "middlewares-basic-auth@file"
- "middlewares-ipwhilelist@file"
tls:
certResolver: myresolver
middlewares:
middlewares-basic-auth:
basicAuth:
users:
- "username:(encrypted password)"
headerField: X-WebAuth-User
middlewares-rate-limit:
rateLimit:
average: 100
period: 1m
burst: 100
middlewares-ipwhilelist:
ipWhiteList:
sourceRange:
- "127.0.0.1/32"
- "192.168.0.0/24"
tls:
options:
default:
minVersion: VersionTLS12
cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 # TLS 1.2
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 # TLS 1.2
- TLS_AES_256_GCM_SHA384 # TLS 1.3
- TLS_CHACHA20_POLY1305_SHA256 # TLS 1.3
curvePreferences:
- CurveP521
- CurveP384
sniStrict: true
It's taken me quite a while to get to this setup with Traefik, and I've tried to boil down the troubleshooting steps to following the items in the "Setup Overview" section. This is the order that I will troubleshoot a container if it's not resolving the way that I planned:
This is still a work in progress, and extra labels and configuration are being added as I discover them or through extra security requirements
There are still some improvements that I'd like to make to the setup: