I could have titled this post as “The evolution of a website”. This blog was started back in 2015 and at that time was hosted on a virtual Ubuntu 14.04 instance. I also had my personal site on yet another instance. Of course it worked well but was a waste of resources for a lightly used blog mainly by me! There were upgrades to newer versions of OS along the way but the big move was to Docker. I finally moved both sites to a single virtual instance. What a superb piece of software Docker really is. Of course the whole containerisation phenomenon continues to gather pace. My first implementation of websites on Docker was not using Traefik but an Nginx proxy as the ingress point which was trivial to implement. Traefik 1.7 was also fairly easy as there are a lot of examples out on the web. The guys that built Traefik have had a major redesign though on moving to version 2.0 and the documentation can only be described as perhaps lacking in ‘example based documentation.’ It certainly took some time to understand so I will hopefully save others some time with this post.
The first thing to understand is a key difference in the Traefik config implementation. There is ‘static’ configuration and ‘dynamic’ configuration. To add to the confusion there is also multiple methods you can use. Bear in mind they are mutually exclusive, e.g. you may only use one.
For the static configuration, you may use either: TOML or YAML as a file type provider. You may also use CLI which is still technically within a YAML but as part of the Traefik configuration file.
For the dynamic configuration, you have even more options, but if we are talking about a simple blog on a single server using a file type then it is either: TOML or YAML. This way you can save repeating the same code endlessly in CLI config commands.
I went for CLI and TOML configuration respectively. Now for the bit that most people are looking for! The following is the code for the Traefik 2 container:
Note, I’m using docker-compose rather than the incredibly long winded docker run commands.
~/traefik/docker-compose.yml
version: "3.3"
services:
traefik:
image: "traefik:latest"
container_name: "traefik"
restart: "always"
command:
- "--global.sendAnonymousUsage"
#- "--log.level=DEBUG"
- "--log.filePath=/var/log/traefik.log"
- "--accesslog=true"
- "--accesslog.filePath=/var/log/access.log"
- "--accesslog.bufferingsize=100"
#- "--api.insecure=true"
#- "--api=true"
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.file.directory=/configuration"
- "--providers.file.filename=dynamic_conf.toml"
- "--providers.file.watch=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true"
#- "--certificatesresolvers.mytlschallenge.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
- "--certificatesresolvers.mytlschallenge.acme.email=postmaster@yourdomain.com"
- "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"
network_mode: "host"
volumes:
- logs:/var/log
- "./letsencrypt:/letsencrypt"
- "./configuration:/configuration"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
volumes:
logs: {}
Note the commented commands. The api should be secured as per the dashboard if it is used, if it isn’t though disable it. The letsencrypt staging provider should be used while you are testing. You can then comment out once you are up and running.
This will get you a functioning traefik container with a directory named ‘configuration’ to store your dynamic configuration file. There is also a ‘letsencrypt’ directory to store your certificates file. I’ve also added a mounted logs directory to write logs to.
The next file is the dynamic TOML configuration file. This file is dynamic because it may be changed while the reverse proxy is running and the software will monitor the changes made and react accordingly.
~/traefik/configuration/dynamic_conf.toml
[http]
[http.routers.my-api]
entryPoints = ["websecure"]
rule = "Host(`traefik.yourdomain.com`)"
service = "api@internal"
middlewares = ["secured"]
[http.routers.my-api.tls]
options = "default"
certResolver = "mytlschallenge"
[http.middlewares]
[http.middlewares.secured.chain]
middlewares = ["safe-ipwhitelist", "auth"]
[http.middlewares.secureHeader.headers]
frameDeny = true
sslRedirect = true
stsSeconds = 31536000
stsPreload = true
stsIncludeSubdomains = true
[http.middlewares.auth.basicAuth]
users = [
"secure_user:$2y$05$AM4KZ7OspJ./M7zV6zmLkevRVekRvnaZUcaSMFOVXfT5.ugcQyFVq"
]
[http.middlewares.safe-ipwhitelist.ipWhiteList]
sourceRange = ["192.168.1.0/24"]
[tls]
[tls.options]
[tls.options.default]
minVersion = "VersionTLS12"
cipherSuites = [
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
]
sniStrict = true
There is a fair amount going on this file. The routers section defines the dashboard and the associated configuration. It also introduces chaining of middleware to secure access to the dashboard. The middlewares section defines both the chain and the individual middlewares. I will warn you against using the secureHeader middleware until you are sure that you have implemented your site correctly. Note also, the whitelist currently contains a single RFC 1918 prefix. This will not allow you access to the dashboard on an internet address as it is unroutable address space. You should replace this with an address range you would actually like to access the dashboard from if you wish to restrict access this way. You should also make your own user and password. Use htpasswd for this, e.g:
htpasswd -nb -B secure_user dontusethispassword
The final section is the TLS section. I have made this deliberately secure keeping TLS 1.2 as the minimum version with the included cipher suites. This will help ensure you can achieve an A+ score on:
The Traefik container uses host docker networking. I have implemented it this way to ensure that I see the actual addresses of users who access this site. This does mean however you should be in full control of the firewall on your instance. The default way would be via bridge but then you would see the bridge address as accessing your site.
Now that you have a functioning Traefik 2 container. You should be able to access the dashboard. (Don’t forget your DNS updates) Now we can look at the really great thing that Traefik does. Automatic routing of web services to the correct service. Next I’ll provide a working configuration of a docker WordPress site.
~/yourdomain.com/docker-compose.yml
version: '3.3'
services:
db:
image: mariadb
volumes:
- db-data:/var/lib/mysql
networks:
- default
restart: always
environment:
MYSQL_ROOT_PASSWORD: supersecretpassword
MYSQL_DATABASE: db
MYSQL_USER: dbuser
MYSQL_PASSWORD: dbpassword
wordpress:
depends_on:
- db
image: wordpress:latest
volumes:
- wordpress:/var/www/html
networks:
- traefik_default
- default
restart: always
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik_default"
- "traefik.http.middlewares.yourdomain-https.redirectscheme.scheme=https"
- "traefik.http.routers.yourdomain-http.entrypoints=web"
- "traefik.http.routers.yourdomain-http.rule=Host(`yourdomain.com`, `www.yourdomain.com`)"
- "traefik.http.routers.yourdomain-http.middlewares=yourdomain-https@docker"
- "traefik.http.routers.yourdomain.middlewares=secureHeader@file"
- "traefik.http.routers.yourdomain.entrypoints=websecure"
- "traefik.http.routers.yourdomain.rule=Host(`yourdomain.com`, `www.yourdomain.com`)"
- "traefik.http.routers.yourdomain.tls=true"
- "traefik.http.routers.yourdomain.tls.options=default"
- "traefik.http.routers.yourdomain.tls.certresolver=mytlschallenge"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: db
WORDPRESS_DB_USER: dbuser
WORDPRESS_DB_PASSWORD: dbpassword
networks:
traefik_default:
external: true
volumes:
db-data: {}
wordpress: {}
Note, I do stress, please god use your own passwords!
It is the docker labels that are the real magic here. The configuration will ensure redirection of HTTP to HTTPS, letsencrypt certificate with the www version as an alternate name and will apply the default super secure TLS config from the dynamic TOML file.
Not to mention that Traefik 2 also does routing of TCP as well using services. That however will be for another day.
https://chriswiegman.com/2019/10/serving-your-docker-apps-with-https-and-traefik-2/
Why network_mode : “host” ? Isn’t the host network handled by the web and websecure entrypoints?
Hi Joseph,
As mentioned in the post, using bridged networking meant instead of seeing the IP addresses of users who were accessing the site, I was instead seeing the address of the bridge itself, not what I wanted. Of course in host network mode you are entirely responsible for firewalling the instance. This behaviour however may have changed, for someone setting something similar up, I would suggest they try bridged networking over host. The added advantage host networking provides though is it requires no additional effort to make the docker container available on IPv6.
I have two WordPress sites configured exactly as you have shown above albeit with separate domains. I have added both domains to my /etc/hosts file. Both sites installed with no problem however, the Site Health shows issues.
The first site shows ‘The REST API did not behave correctly’ as it could not process the context parameter but there is no problem with loopback test.
The second site has errors both with loopback request and REST API. In both cases it cannot resolve the domain name.
I am running on Linux Mint 19 – no firewall is enabled. I have been bashing my head against this setup for ages with no luck. I really want to use the FPM version with Alpine and Nginx but have had even less success getting that to work.
Any help you can give would be much appreciated.
Hi Alan,
The configuration I included is an exact representation of the docker-compose configuration this site uses plus others which are also on another completely different domain. Not sure which host files you are referring to but this kind of setup requires a real server on a public address. Traefik needs to fetch the actual certificates and so needs to be accessible publicly using publicly known DNS records for the domain in questions. This should always be performed first using the letsencrypt staging account first.
Thank you for the detail! I’ve been struggling with Traefikv2 and wordpress within docker and your configuration was rock solid. It was also my first experience with security headers so thank you for that. My sites are getting a “A” rating now. I do have a question – my dashboard (https://traefik.example.com) is seen publically and it seems the whitelisting isn’t working even though it is shown in the dashboard as being setup. And since it’s on the interwebs, I did a security header check and it’s failing with a F rating. Do you have any advise on how to get the traefik dashboard behind the security headers? I tried adding it to the chain but it doesn’t seem to make a difference.
[http.middlewares.secured-chain.chain]
# middlewares = [“safe-ipwhitelist”, “auth”]
middlewares = [“safe-ipwhitelist”, “auth”, “secureHeader”]
Again – Thank you so much for the information! You saved me from a good couple of weeks of struggling with traefikv2 issues, and kept me from giving up and staying on 1.7
No worries Sean. It certainly took me some time to figure out but once up and running is so easy to throw a new site behind.
The middlewares are ‘chainable’ in that multiple middlewares can be linked into a single referenced middleware but you still need to apply that chain to the dashboard. This is the documented preferred way to implement the dashboard.
https://docs.traefik.io/operations/dashboard/
From the dynamic_conf.toml:
You define an entrypoint, host, service and apply the chained middleware named “secured”
The “secured” chain consists of a “safe-ipwhitelist” and “auth”. Obviously in this example the source range is invalid to use on the web as it is RFC 1918 address space. Your assumption should be correct in that adding the security headers to the same chain should do what you need but of course if the auth and whitelisting are not even working then something is clearly not quite right in your configuration. Good luck I’m sure you’ll crack it!
[http]
[http.routers.my-api]
entryPoints = [“websecure”]
rule = “Host(`traefik.yourdomain.com`)”
service = “api@internal”
middlewares = [“secured”]
[http.routers.my-api.tls]
options = “default”
certResolver = “mytlschallenge”
[http.middlewares]
[http.middlewares.secured.chain]
middlewares = [“safe-ipwhitelist”, “auth”]
[http.middlewares.secureHeader.headers]
frameDeny = true
sslRedirect = true
stsSeconds = 31536000
stsPreload = true
stsIncludeSubdomains = true
[http.middlewares.auth.basicAuth]
users = [
“secure_user:$2y$05$AM4KZ7OspJ./M7zV6zmLkevRVekRvnaZUcaSMFOVXfT5.ugcQyFVq”
]
[http.middlewares.safe-ipwhitelist.ipWhiteList]
sourceRange = [“192.168.1.0/24”]