Traefik Reverse Proxy in Docker with TLS Certs

Credits

Reverse Proxies and setting up authentication on them can be very hard to wrap your head around and get working at first. While I will do my best to continue to update this guide to be as comprehensive as possible, I would like to point out the articles and resources that really helped me to learn this:

Joshua Avalon – Setup Traefik v2 Step by Step

Techno Tim – 2 Factor Auth and Single Sign on with Authelia

Prerequisites

All files used in this guide can be found here.

This guide will be assuming you are deploying the services you want to reverse proxy in Docker and are provisioning them with Docker Compose.

To receive TLS Certs you must own and control the domain you are deploying to. This does not mean you have to expose these services to the internet or even add any public records to your domain, you can have certs issued for domains that are used exclusively locally.

Configuring Your Domain

Cloudflare DNS

This setup will be based on using the Cloudflare API to manage DNS on your domain. We will be using DNS challenges to issue certs with LetsEncrypt.

Import your domain as a Cloudflare site (Optional)

If you purchased your domain through cloudflare, feel free to skip this step. If you got your domain through another registrar, you can register your site through cloudflare using the steps below.

Change your DNS servers to Cloudflare’s

If you haven’t purchased your domain through Cloudflare, you can still continue with this guide by changing the DNS servers your domain uses over to Cloudflare’s. Change your nameservers to: connie.ns.cloudflare.com and cris.ns.cloudflare.com

Here’s an example using Hostinger’s dashboard:

Add your site to Cloudflare

After changing your DNS servers, you can add your site to Cloudflare under Websites>Add a Site. For our purposes, the free plan works fine.

Create an API Key

Head over to the Cloudflare API Tokens Page and create a new API Token with Zone.Zone and Zone.DNS permissions on either just the sites you want or all of your sites.

Deploying Traefik

File Structure

Deploying Traefik isn’t completely plug-and-play and needs some files to get started.

Wherever you wish to place your Traefik configuration files, create the following file structure. You’ll need to create all files listed, traefik does not create them itself and will yell at you if you don’t. Create acme.json as an empty file for traefik to fill, we will configure traefik.yml in the next step.

traefik
│   traefik.yml   
│
└───acme
│   │   acme.json
│   
└───dynamic

Traefik also requires strict permissions on the acme.json file (600)

traefik.yml

Below is an example basic configuration of traefik.yml that will serve our purposes for now

Github: traefik.yml

global:
  checkNewVersion: true
  sendAnonymousUsage: false  # true by default

# (Optional) Log information
log:
   level: INFO  # DEBUG, INFO, WARNING, ERROR, CRITICAL
   format: common  # common, json, logfmt
   filePath: /var/log/traefik/traefik.log

# (Optional) Enable API and Dashboard
api:
  dashboard: true  # true by default
  insecure: false  # Don't do this in production!

# Entry Points configuration
entryPoints:
  http:
    address: :80
    http:
      redirections:
        entryPoint:
          to: https
          scheme: https

  https:
    address: :443

# Configure your CertificateResolver here...
certificatesResolvers:
   letsEncrypt:
     acme:
       email: YOUR_EMAIL
       storage: /etc/traefik/acme/acme.json
       dnsChallenge:
         provider: cloudflare
         resolvers:
           - 1.1.1.1:53
           - 8.8.8.8:53
         delayBeforeCheck: 0

providers:
  docker:
    endpoint: unix:///var/run/docker.sock
    exposedByDefault: false  # Default is true
  file:
    # watch for dynamic configuration changes
    directory: /etc/traefik/dynamic
    watch: true

Docker

Proxy Network

Create a new attachable bridge network in Docker for Traefik to use. This can be done in the docker CLI or through a managment portal such as Portainer. We will attach our Traefik container and all other proxied containers to this network.

docker network create -d bridge --attachable proxy

Creating the Container

Deploy Traefik using Docker Compose

Github: docker-compose.yml

version: '3'

services:
  traefik:
    image: "traefik:v2.5"
    container_name: "traefik"
    environment:
      - CF_API_EMAIL=YOUR_EMAIL
      - CF_DNS_API_TOKEN=YOUR_API_KEY
      - CF_ZONE_API_TOKEN=YOUR_API_KEY
    networks:
      - proxy
    ports:
      - "80:80"
      - "443:443"
      # (Optional) Expose Dashboard
      - "42069:8080"  # Don't do this in production!
    volumes:
      - /config/docker/traefik-logs:/var/log/traefik
      - /config/docker/traefik:/etc/traefik
      - /var/run/docker.sock:/var/run/docker.sock:ro

networks:
  proxy:
    external: true

Leaving your API keys exposed in docker-compose like this is not recommended. Environment variables in docker containers also aren’t particularly secure. Running a secrets agent like the one built in to docker-swarm is recommended. Will I be covering that here? Absolutely not.

Configuring local DNS

If you’re using Traefik as a reverse proxy for exclusively local services, you will need to configure a wildcard DNS entry on your network’s DNS server to point at Traefik, be that the DNS resolver on your router, or something more dedicated like a Pihole. Below I will include instructions for pfSense.

pfSense

In pfSense you can create a wildcard entry in the Custom Options section of Unbound at Services>DNS Resolver>General Settings:

server:
local-zone: "example.com" redirect
local-data: "example.com 3600 IN A 192.168.1.54"

Adding Docker Services to Traefik

Thankfully, once Traefik is configured, adding new services is a simple as adding some extra configuration to your containers. Here’s a docker-compose example of deploying Homer, a static homepage service.

---
version: "2"
services:
  homer:
    image: b4bz/homer
    container_name: homer
    networks:
      - proxy # Required for Traefik
    volumes:
      - /homer_assets:/www/assets
    user: 1000:1000
    labels:
      - "traefik.docker.network=proxy" # Connect to traefik's docker network
      - "traefik.enable=true" # Enable traefik
      - "traefik.http.routers.homer.rule=Host(`example.com`)" # Configure 'homer' router host rule
      - "traefik.http.routers.homer.entrypoints=https" # Enable https entrypoint on 'homer' router
      - "traefik.http.routers.homer.tls=true" # Enable tls on 'homer' router
      - "traefik.http.services.homer.loadbalancer.server.port=8080" # Port to redirect to in container
      - "traefik.http.routers.homer.tls.certresolver=letsEncrypt" # Use letsEncrypt certresolver defined in traefik.yml
    restart: unless-stopped

networks:
  proxy: # Add a refrence to traefik's docker network here
    external: true

If everything went well, your definied address should route to your service and your cert will be issued!

Authelia

Deploying Authelia

File Structure

Like Traefik, theres a bit of configuration we have to do before deploying Authelia. The file structure for Authelia is very simple:

authelia
│   configuration.yml 
│   users_database.yml 
configuration.yml

Github: configuration.yml

---
###############################################################
#                   Authelia configuration                    #
###############################################################

jwt_secret: a_very_important_secret
default_redirection_url: https://public.example.com

server:
  host: 0.0.0.0
  port: 9091

log:
  level: debug
# This secret can also be set using the env variables AUTHELIA_JWT_SECRET_FILE

totp:
  issuer: authelia.com

# duo_api:
#  hostname: api-123456789.example.com
#  integration_key: ABCDEF
#  # This secret can also be set using the env variables AUTHELIA_DUO_API_SECRET_KEY_FILE
#  secret_key: 1234567890abcdefghifjkl

authentication_backend:
  file:
    path: /config/users_database.yml
    password:
      algorithm: argon2id
      iterations: 1
      salt_length: 16
      parallelism: 8
      memory: 64

access_control:
  default_policy: deny
  rules:
    # Rules applied to everyone
    - domain: public.example.com
      policy: bypass
    - domain: traefik.example.com
      policy: one_factor
    - domain: secure.example.com
      policy: two_factor

session:
  name: authelia_session
  # This secret can also be set using the env variables AUTHELIA_SESSION_SECRET_FILE
  secret: unsecure_session_secret
  expiration: 3600  # 1 hour
  inactivity: 300  # 5 minutes
  domain: example.com  # Should match whatever your root protected domain is

  # redis:
  #   host: redis
  #   port: 6379
  #   # This secret can also be set using the env variables AUTHELIA_SESSION_REDIS_PASSWORD_FILE
  #   # password: authelia

regulation:
  max_retries: 3
  find_time: 120
  ban_time: 300

storage:
  encryption_key: you_must_generate_a_random_string_of_more_than_twenty_chars_and_configure_this
  local:
    path: /config/db.sqlite3

notifier:
  smtp:
    username: test
    # This secret can also be set using the env variables AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE
    password: password
    host: mail.example.com
    port: 25
    sender: [email protected]
...

There’s a lot of stuff to configure in here, but to get started there’s only a few things we need to do

  1. Create a jwt_secret, an alphanumeric key of 64 characters:
jwt_secret: DHUAov7n9VTchKhgP5XRjraZdD6qJq2ck8SeMWjy2J5memk7JUwZEuZAoxKHkC7i

I know what the top of the site says, but please make your own and don’t copy paste this

2. Change the provided access_control policy. Authelia Access-Control Docs

access_control:
  default_policy: deny
  rules:
    # Rules applied to everyone
    - domain: service.example.com
      policy: two_factor

3. Update the domain in the session section to the domain you’re protecting

domain: example.com  # Should match whatever your root protected domain is

4. Generate an encryption key for sqlite storage

storage:
  encryption_key: you_must_generate_a_random_string_of_more_than_twenty_chars_and_configure_this

5. Setup email notifier for forgotten passwords.

I won’t be covering how to get this setup with SMTP. There are simple ways to do this through your gmail account or other providers. You can also write these notifications to a local file, which isn’t recommended for any amount of users over one.

notifier:
  filesystem:
    filename: /config/notification.txt
users_database.yml

Github: users_database.yml

---
###############################################################
#                         Users Database                      #
###############################################################

# This file can be used if you do not have an LDAP set up.

# List of users
users:
  authelia:
    displayname: "Authelia User"
    # Password is authelia
    password: "$6$rounds=50000$BpLnfgDsc2WD8F2q$Zis.ixdg9s/UOJYrs56b5QEZFiZECu0qZVNsIYxBaNJ7ucIL.nlxVCT5tqh8KHG8X4tlwCFm5r6NTOZZ5qRFN/"  # yamllint disable-line rule:line-length
    email: [email protected]
    groups:
      - admins
      - dev
...

Create your first user with the steps below:

  1. Change your displayname:
users:
  example:
    displayname: "Example"

2. Change your hashed password

password: "$argon2id$v=19$m=65536,t=3,p=4$d3hKRUpCSUhLMEhxVXdoZg$eChQ4l0qnt4oCE7Yaw+bVp1wi7/CO3PqlqEY+udUkYc"  # yamllint disable-line rule:line-length

Authelia provides a tool for creating hashed passwords using the proper hashing algorithm. Generate a password by running docker run authelia/authelia:latest authelia hash-password 'YOUR_PASSWORD'

3. Change your email address:

email: [email protected]

Creating the Container

After configuring your file structure, deploy Authelia with the compose below:

Github: docker-compose.yml

version: '3'

services:
  authelia:
    image: authelia/authelia
    container_name: authelia
    volumes:
      - /config/docker/authelia:/config
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.authelia.rule=Host(`auth.example.com`)" # Change to your domain
      - "traefik.http.routers.authelia.entrypoints=https"
      - "traefik.http.routers.authelia.tls=true"
      - "traefik.http.routers.authelia.tls.certresolver=letsEncrypt"
      - "traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://auth.example.com" # Change to your domain
      - "traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true"
      - "traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email"
    expose:
      - 9091
    restart: unless-stopped
    environment:
      - TZ=America/Phoenix
    healthcheck:
      disable: true

networks:
  proxy:
    external: true

Adding Docker Services to Authelia

Much like with Traefik, it is very easy to add containers to Authelia. Here’s our same Homer example from before, but this time with Authelia:

---
version: "2"
services:
  homer:
    image: b4bz/homer
    container_name: homer
    networks:
      - proxy # Required for Traefik
    volumes:
      - /homer_assets:/www/assets
    user: 1000:1000
    labels:
      - "traefik.docker.network=proxy"
      - "traefik.enable=true"
      - "traefik.http.routers.homer.rule=Host(`example.com`)"
      - "traefik.http.routers.homer.entrypoints=https"
      - "traefik.http.routers.homer.tls=true"
      - "traefik.http.services.homer.loadbalancer.server.port=8080"
      - "traefik.http.routers.homer.tls.certresolver=letsEncrypt"
      - "traefik.http.routers.homer.middlewares=authelia@docker" # This line adds the Authelia middleware to Homer
    restart: unless-stopped

networks:
  proxy: # Add a refrence to traefik's docker network here
    external: true

With version 2.5 of traefik, you’ll get Traefik logs saying middleware \"authelia@docker\" does not exist. These logs are a lie. This is apparently a known issue and Authelia is working correctly if you see these. I have not tested to see if this is fixed in newer versions.

Updating access_control

It is likely that as you add services to Authelia, you may need to update the access_control section of your configuration.yml. By default, the config is set to deny access to undefined addresses, resulting in a 403: forbidden when you visit the site.

Leave a Reply

Your email address will not be published. Required fields are marked *