Traefik + Docker + OAuth: a free reverse proxy with TSL and Google OAuth2

Advantages of the solution 🕵️‍♂️

  • A single server for all your domains and applications, thanks to the Traefik reverse proxy and its Docker integration
  • HTTPS thanks to the free TLS certificates provided by Let’s Encrypt
  • Private resources protection with Google’s OAuth2
  • Everything automated

What you need 🧰

  • A domain: you need at least a domain for the OAuth service, but you will need one more for each app you want to deploy. Subdomains are fine, most of my services (OAuth included) are on subdomains. Freenom provides free domains if you need a testing one (again: I don’t receive money for them, I just enjoyed the service in the past).

That’s it! ✅

💡PRO TIP: When you see the ☝️emoji, it’s your turn! Follow the instructions.

Prepare the server 🏗

Install docker

Install docker-compose

#!/bin/zsh# get latest docker compose released tag
COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d\" -f4)
# Install docker-compose
sh -c "curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose"
chmod +x /usr/local/bin/docker-compose
sh -c "curl -L https://raw.githubusercontent.com/docker/compose/${COMPOSE_VERSION}/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose"
# Output compose version
docker-compose -v

Configure the domain

💡PRO TIP: if you own your-domain.com, you can set configure srv.our-domain.com and *.srv.your-domain.com to point to your server, without buying a new domain.

OAuth screen consent 🚧

During the OAuth screen consent, setup choose whatever App name you like (it will be visible during the login in your services) and add your-domain.com to the Authorized domains.

When generating a new OAuth 2.0 Credential pick again the name you want but set https://oauth.your-domain.com/_oauth as Authorized redirect URIs. This will provide OAuth for *.your-domain.com. If you have multiple domains just add them in the same way. We will see later how to handle them on the server-side.

Save the Client ID and Client secret for later.

The Traefik stack 🦦

version: "3.9"services:
traefik:
container_name: traefik
image: traefik
restart: always
stdin_open: true
tty: true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./traefik.yml:/etc/traefik/traefik.yml
- ./acme:/etc/traefik/acme
ports:
- "80:80"
- "443:443"
oauth:
container_name: oauth
image: thomseddon/traefik-forward-auth
restart: always
stdin_open: true
tty: true
environment:
PROVIDERS_GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
PROVIDERS_GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
SECRET: ${SECRET}
COOKIE_DOMAIN: ${DOMAIN}
AUTH_HOST: oauth.${DOMAIN}
labels:
traefik.http.middlewares.oauth.forwardauth.address: http://oauth:4181
traefik.http.middlewares.oauth.forwardauth.authResponseHeaders: X-Forwarded-User
traefik.http.middlewares.oauth.forwardauth.trustForwardHeader: "true"
traefik.http.routers.oauth.middlewares: oauth
traefik.enable: "true"
traefik.http.routers.oauth.rule: Host(`oauth.${DOMAIN}`)
traefik.http.routers.oauth.tls: "true"
traefik.http.routers.oauth.tls.certresolver: letsencrypt
traefik.http.services.oauth.loadbalancer.server.port: 4181
networks:
default:
name: traefik

This docker-compose will create a stack of two services: traefik and oauth.

Traefik

We mount the Docker socket, needed to discover the containers and read the labels, the acme directory that we use to store Let’s Encrypt certificates, and the traefik.yml configuration file.

Let’s see how to configure Traefik.
☝️Create a new file traefik.yml with the following content.

entryPoints:
# http redirect to https
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
# https
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt
# letsencrypt TLS
certificatesResolvers:
letsencrypt:
acme:
email: <YOUR-EMAIL_HERE>
storage: /etc/traefik/acme/acme.json
tlsChallenge: {}
httpChallenge:
entryPoint: web
# listen docker
providers:
docker:
exposedByDefault: false
network: traefik

First, we create two entrypoints, one for the HTTP (web) that is configured to redirect to the secure entrypoint, and the other for the HTTPS (websecure) configured to use Let’s Encrypt for generating certificates.

Then, we set up Let’s Encrypt as a certificate resolver. It will store certificates in the acme.json file we have mounted. It can use both TLS and HTTP challenge using the web entrypoint.

Finally, we configure Traefik to watch Docker containers. Traefik will connect to containers using the traefik network that we create at the end of the docker-compose.yml file. We will attach this network to each container we want to expose.

OAuth2 with Traefik Forward Auth

We will see the environment variables later on. The OAuth service will run on https://oauth.your-domain.com.

The labels configure Traefik to work with the OAuth middleware. In particular:

  • traefik.http.middlewares.oauth sets up a new forwardauth middleware called oauth and configure the headers.
  • traefik.http.routers.oauth.middlewares: oauth activates the Auth Host Mode, which is helpful when dealing with a large number of subdomains.
  • All the other labels are used to proxy oauth.your-domain.com to this container. You will use them on each container you want to proxy, but we will see them later.

Note that you will need an OAuth service per each domain (subdomains excluded). For example, the deployment we have seen covers your-domain.com, sub1.your-domain.com, and sub2.sub1.your-domain.com. It doesn’t cover your-other-domain.com nor sub.your-other-domain.com.

This is due to the COOKIE_DOMAIN env variable. The service will cover that domain and all the subdomains of any label. For an additional domain, you need an additional service with its domain specified in COOKIE_DOMAIN.

You can use also subdomain: that is, COOKIE_DOMAIN value can be server.your-domain.com. In this case, it will cover server.your-domain.com and all the sub-sub-domains (like sub.server.your-domain.com) but not a different subdomain (sub.your-domain.com is not covered).

Additional configs

☝️Create a .env file from which Docker Compose will load your environment variables.

DOMAIN=<YOUR-DOMAIN>
GOOGLE_CLIENT_ID=<YOUR-CLIENT-ID>
GOOGLE_CLIENT_SECRET=<YOUR-CLIENT-SECRET>
SECRET=<A-RANDOM-STRING-HERE>
  • Set DOMAIN to your domain. I assume you have all the subdomain pointing to this server or at least the subdomain called oauth.
  • Insert the Client ID and Secret obtained from Google.
  • SECRET is set to 16 randomly generated digits, but I suggest you put here a randomly generated string with upper and lower case letters, digits, and symbols.

☝️Now you can run the Trefik stack with the following command.

docker-compose --env-file .env up -d

Set up your application 🚀

version: "3.9"services:
# a not exposed service
db:
image: mongo
# a publicly exposed app, using https
app:
image: myapp
network:
- default # to reeach the db
- traefik # to be reached from traefik
labels:
# enable traefik and tls using letsencrypt
traefik.enable: "true"
traefik.http.routers.app.tls: "true"
traefik.http.routers.app.tls.certresolver: letsencrypt
# serve it on https://app.your-domain.com
traefik.http.routers.app.rule: Host(`app.your-domain.com`)
# a protected admin panel, accessible using https
admin:
image: myadminpanel
network:
- default # to reeach the db
- traefik # to be reached from traefik
labels:
# enable traefik and tls using letsencrypt
traefik.enable: "true"
traefik.http.routers.admin.tls: "true"
traefik.http.routers.admin.tls.certresolver: letsencrypt
# serve it on https://admin.your-domain.com
traefik.http.routers.admin.rule: Host(`admin.your-domain.com`)
# protect with oauth
traefik.http.routers.admin.middlewares: oauth
# make the traefik network available for these containers
networks:
traefik:
external:
name: traefik
  1. You need to make the traefik network available to the containers in this file. See the last section of the file.
  2. You need to attach the traefik network to all the containers you want to expose. Docker Compose creates also a default network to connect all the containers inside the docker-compose.yml file. When you attach the traefik network remember to keep the default network too if you need to reach the other containers of the stack.
  3. All the magic is made with labels! traefik.enable: "true" enables Trafik for this container. traefik.http.routers.appname.tls: "true" enables the HTTPS. traefik.http.routers.appname.tls.certresolver: letsencrypt tells Traefik to use Let’s Encrypt for generating TLS certificates.
  4. Use traefik.http.routers.appname.rule: Host(`app.your-domain.com`) to tell Traefik on which domain you want to expose this container. You can specify more than one domain dividing them with a comma: Host(`app.your-domain.com,theapp.your-domain.com`).
  5. If your app exposes only one port, you are good to go. If the app exposes multiple ports you can tell Traefik which use by adding a label: traefik.http.services.appname.loadbalancer.server.port: portnumber.
  6. If you want to enable OAuth on that container just add the label traefik.http.routers.appname.middlewares: oauth.

Conclusions 👋

Hey, it doesn’t work!
Don’t be shy!
If you need some help, you find some errors in the code or the instructions, or you have some suggestions, just drop me a message. You can find my contacts on my website.

It works, thanks!
Nice! Would you mind giving me some claps? 👏

Do you want to get in touch? Feel free to contact me. 😉

🔗 https://ald.ooo — 👨‍💻 Infrastructure Engineer @ BendingSpoons

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store