Skip to content

Traefik Reverse Proxy in Docker with TLS Certs

September 25, 2022·Austin Lynn Huffman
Austin Lynn Huffman

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:

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:

image

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.

image

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.yml
      • acme.json

    traefik.yml

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

    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

    image

    Creating the Container

    Deploy Traefik using Docker Compose

    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 are instructions for pfSense & Pihole

    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"

    This config will match *.example.com to 192.168.1.54

    Pihole

    Wildcard entries aren’t supported in Pihole’s UI. To add one, you’ll have to ssh in and edit some files.

    We’re looking for the /etc/dnsmasq.d directory which holds all of our custom DNS configuration. Create a new file in this directory and it will be automatically loaded. We’ll call ours 05-wildcards.conf:

    # Pi-hole dnsmasq configuration
    # Wildcard entries for reverse proxies
    
    address=/example.com/192.168.1.54

    This config will match *.example.com to 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!

    image

    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
    ---
    ###############################################################
    #                   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: admin@example.com
    ...

    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
      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
    ---
    ###############################################################
    #                         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: authelia@authelia.com
        groups:
          - admins
          - dev
    ...

    Create your first users 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: example@example.com

    Creating the Container

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

    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.