Setting up global HTTPS and SSO with Traefik v2
1 October 2019
tl;dr: A working Traefik v2 config implementing everything in the post can be found here.
Migration from Traefik v1 → v2
The official documentation on the differences between Traefik v1 and v2 can be found here, but I’ll go through the major points that’ll apply in this post.
Routers, Services and Middleware are the new black
Traefik v1 used the concepts of frontends and backends to represent how containers should be routed. Traefik v2 does away with this, and utilises routers, services and middleware to conduct routing. Entrypoints still exist in v2 configurations but they’re severely limited; you can no longer attach properties such as redirects or tls restrictions anymore. The official documentation describes the paradigm shift thusly:
Typically, a router replaces a frontend, and a service assumes the role of a backend, with each router referring to a service. However, even though a backend was in charge of applying any desired modification on the fly to the incoming request, the router defers that responsibility to another component. Instead, a dedicated middleware is now defined for each kind of such modification. Then any router can refer to an instance of the wanted middleware.
TLS configuration is now per router
Traefik v1 used to have a dedicated config section for ACME-based certificate acquisition and automagically applied these certificates to TLS entrypoints as required. With the shift to Traefik v2, this section is obselete and we now have certificateResolvers
.
These resolvers can’t be applied to entryPoints
like we’re used to in Traefik v1, we now need to apply these to routers instead. The official documentation on certificateResolvers
can be found here.
Redirections are now per router
Traefik v1 allowed us to apply a blanket redirect upon an entrypoint to redirect all traffic somewhere else, i.e. redirecting all HTTP to HTTPS. Traefik v2 no longer allows this and instead requires us to specify any redirections we want as middleware upon routers. This gives us greater control on when we want to apply redirects but is pretty confusing when coming across from a Traefik v1 mindset.
Setting up Traefik
The examples in this post will operate on a docker-compose setup like the one below.
traefik2-demo
|── .env
├── README.md
├── config
│ └── traefik
│ ├── dynamic_conf.toml
│ ├── forward.ini
│ └── traefik.toml
└── docker-compose.yml
2 directories, 5 files
Our Traefik service is defined within our docker-compose.yml
as such:
version: "3.7"
services:
traefik:
image: traefik
ports:
- "0.0.0.0:80:80"
- "0.0.0.0:443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./config/traefik:/etc/traefik
environment:
- TZ=Australia/Sydney
- DO_AUTH_TOKEN=${DO_AUTH_TOKEN}
networks:
- traefik
labels:
- traefik.enable=true
- traefik.backend=traefik-api
# Which network traefik should listen on
- traefik.docker.network=traefik2_demo_traefik
# Reverse proxy configuration - exposes the Traefik dashboard under traefik.${TRAEFIK_DOMAIN}
- traefik.http.services.traefik.loadbalancer.server.port=8080
# SSL configuration
- traefik.http.routers.traefik-ssl.entryPoints=https
- traefik.http.routers.traefik-ssl.rule=host(`traefik.${TRAEFIK_DOMAIN}`)
- traefik.http.routers.traefik-ssl.tls=true
- traefik.http.routers.traefik-ssl.tls.certResolver=le
# Single Sign On middleware
- traefik.http.routers.traefik-ssl.middlewares=sso@file
Note: docker-compose reads ${VARIABLE}
substitutions from the .env file.
The important things to note from this definition are the volume mounts and labels. The volume mounts are pretty self explainatory, but the labels can be quite confusing – we’ll be going over them more later on.
Static vs dynamic configuration
We have two sources of config: static and dynamic. The official docs explain the difference between them pretty well so I’ll copy-paste that here:
Elements in the static configuration set up connections to providers and define the entrypoints Traefik will listen to (these elements don’t change often).
The dynamic configuration contains everything that defines how the requests are handled by your system. This configuration can change and is seamlessly hot-reloaded, without any request interruption or connection loss.
Static configuration - with comments
[log]
level = "INFO"
[entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.https]
address = ":443"
[api]
dashboard = true
# Exposes the api without TLS, fine for our setup with TLS termination
insecure = true
[providers]
[providers.file]
filename = "/etc/traefik/dynamic_conf.toml"
[providers.docker]
endpoint = "unix:///var/run/docker.sock"
watch = true
exposedbydefault = false
# Adds a default routing rule for new containers
defaultrule = "Host(`.demo.carey.li`)"
# Used for certificate acquisition - explained later
[certificatesResolvers.le.acme]
email = "hello@carey.li"
storage = "/etc/traefik/acme.json"
[certificatesResolvers.le.acme.dnsChallenge]
provider = "digitalocean"
Dynamic configuration - with comments
[http.routers]
# HTTP catchall router, matches all HTTP traffic and applies the httpsredirect middleware
[http.routers.https-only]
entryPoints = ["http"]
middlewares = ["httpsredirect"]
rule = "HostRegexp(`{host:.+}`)"
# Only here so the router is valid, is never actually used
service = "noop"
[http.services]
[http.services.noop.loadBalancer]
[[http.services.noop.loadBalancer.servers]]
url = "http://127.0.0.1"
[http.middlewares]
# Used for SSO - explained later
[http.middlewares.sso.forwardAuth]
address = "http://traefik-fa:4181"
authResponseHeaders = ["X-Forwarded-User"]
# Does as it says on the tin
[http.middlewares.httpsredirect.redirectScheme]
scheme = "https"
Setting up HTTPS
Setting up your certificateResolver
If you want to acquire certificates via Lets Encrypt/ACME, you’ll need to setup a certificate resolver. Certificate resolvers support multiple ways of verifying whether a certificate should be issued for a given domain (tlsChallenge
, httpChallenge
, dnsChallenge
). If wildcard certificates are required you’ll have to use dnsChallenge
, but otherwise all three are equivalent. For the purposes of this post, we’ll be using dnsChallenge
.
[certificatesResolvers.le.acme]
email = "hello@carey.li"
storage = "/etc/traefik/acme.json"
[certificatesResolvers.le.acme.dnsChallenge]
provider = "digitalocean"
Providing a DigitalOcean DNS token
The api key for the dnsChallenge
is provided through the DO_AUTH_TOKEN
environment within docker-compose.yml
, which in turn is populated by the .env
file:
DO_AUTH_TOKEN=do-api-token
Making a host HTTPS
This section is ugly, you’ll pretty much have to copy the following labels to all the services you want to enable SSL on:
labels:
# Creates a router that listens on the https entrypoint
- traefik.http.routers.service-ssl.entryPoints=https
- traefik.http.routers.service-ssl.rule=host(`service.${TRAEFIK_DOMAIN}`)
# Enables TLS on this router
- traefik.http.routers.service-ssl.tls=true
- traefik.http.routers.service-ssl.tls.certResolver=le
Enforcing HTTPS
Note that we didn’t attach to the http entrypoint, this was intentional! Without the attached http entrypoint, when a HTTP request comes in it’ll match against the HTTP catch-all router we defined earlier in our dynamic file config.
This’ll apply the httpsredirect
middleware we created in our dynamic config and enforce https everywhere!
Single Sign On
Using thomseddon/traefik-forward-auth, we can add authentication via Google in front of all our services. While this isn’t strictly necessary, I find it convenient and using this method shares authentication across all my services, reducing the need to sign on to every single service.
Traefik configuration
We define a forwardAuth middleware within our dynamic configuration like so:
[http.middlewares]
# Used for SSO - explained later
[http.middlewares.sso.forwardAuth]
# Where to forward the requests credentials to, in order to verify the request.
address = "http://traefik-fa:4181"
# What header to inspect for the authenticated user, if any.
authResponseHeaders = ["X-Forwarded-User"]
Traefik Forward Auth container
Deploying the forward auth container is pretty straight forward, we expose it over HTTPS, bind it to auth.${TRAEFIK_DOMAIN}
and mount in our config.
version: "3.7"
services:
traefik-fa:
image: thomseddon/traefik-forward-auth
container_name: traefik-fa
volumes:
- ./config/traefik/forward.ini:/forward.ini
environment:
- CONFIG=/forward.ini
networks:
- traefik
labels:
- traefik.enable=true
- traefik.backend=traefik-fa
- traefik.http.services.traefik-fa.loadBalancer.server.port=4181
# SSL configuration
- traefik.http.routers.traefik-fa-ssl.entryPoints=https
- traefik.http.routers.traefik-fa-ssl.rule=host(`auth.${TRAEFIK_DOMAIN}`)
- traefik.http.routers.traefik-fa-ssl.middlewares=sso@file
- traefik.http.routers.traefik-fa-ssl.tls=true
- traefik.http.routers.traefik-fa-ssl.tls.certResolver=le
Forward Auth config
# Cookie signing nonce, replace this with something random
secret = secret-nonce
# Google oAuth application values - you can follow https://rclone.org/drive/#making-your-own-client-id to make your own
providers.google.client-id = google-client-id
providers.google.client-secret = google-client-secret
log-level = debug
# Replace demo.carey.li with your own ${TRAEFIK_DOMAIN}
cookie-domain = demo.carey.li
auth-host = auth.demo.carey.li
# Add authorized users here
whitelist = hello@carey.li
whitelist = another@carey.li
Conclusion
After configuring everything here, your Traefik instance should be up and running!