How to Secure Your Web Containers using a Traefik DNS Challenge

Hello! Today, I am going to demonstrate how to use a traefik DNS challenge to enable SSL for your docker web containers. I have included a working configuration file for proxying traffic to your web containers, and for obtaining SSL certificates from Let’s Encrypt using a traefik DNS Challenge. Let’s begin!

How to Secure Web Containers Traefik DNS Challenge Cover Image

What is a Traefik DNS Challenge?

A DNS challenge is a method used by Let’s Encrypt, the CA we want to get a certificate from, to verify that the owner of our certificate requester (our server) also owns the domain they are requesting a certificate for. Without goin into too much detail, the CA checks for a specific DNS record on the domain. If the server is properly configured, it will set that record appropriately (usually using the domain registrar’s api), and, upon seeing that, the CA can be sure the server owner also owns the domain. This means the CA can safely issue a certificate to the server.

There are benefits and drawbacks to using a traefik DNS challenge to get certificates. A DNS challenge does not need to use the common http port (port 80). So, if your ISP blocks traffic to port 80, using traefik DNS challenge will likely serve you better than a traefik HTTP challenge. On the other hand, the configuration of your docker setup will vary based on which domain registrar you register your domains with, whereas with an HTTP challenge, the configuration will be registrar agnostic.

The Configuration

Below, you will find a docker-compose set up to run a simple web container with SSL. Traefik will use a DNS challenge to get an SSL certificate for the container. Note that this compose is tailored to work with a domain registered through Namecheap. Details for using other registrars will be given later.

version: '3.7'

services:
  traefik:
    image: traefik:v3.1
    container_name: traefik_router
    command:
      #routers and api
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.websecure.address=:443"
      #debug
      - "--log.level=DEBUG"    #uncomment while debugging
      #acme automatic ssl dns challenge resolver
      - "--certificatesresolvers.le_dns.acme.dnsChallenge=true"
      - "--certificatesresolvers.le_dns.acme.email=your@email.com"
      - "--certificatesresolvers.le_dns.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.le_dns.acme.dnsChallenge.provider=namecheap"
      - "--certificatesresolvers.le_dns.acme.dnsChallenge.delayBeforeCheck=30"  #needs to be set high enough to allow propagation
      - "--certificatesresolvers.le_dns.acme.dnsChallenge.resolvers=9.9.9.9:53,1.1.1.1:53"
      - "--certificatesresolvers.le_dns.acme.dnschallenge.disablepropagationcheck=true"
      - "--serversTransport.insecureSkipVerify=true"
      - "--certificatesresolvers.le_dns.acme.caServer=https://acme-v02.api.letsencrypt.org/directory"
      #- "--certificatesresolvers.le_dns.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory"
    environment:
      - NAMECHEAP_API_USER=YOUR_USERNAME_HERE
      - NAMECHEAP_API_KEY=YOUR_API_KEY_HERE
    ports:
      - "443:443" #host:container
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./letsencrypt:/letsencrypt
    restart: unless-stopped
    networks:
      - websites_net
  web:
    image: httpd
    container_name: web-1
    volumes:
      - ./html:/usr/local/apache2/htdocs/
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      #https config
      - "traefik.http.routers.web_1.entrypoints=websecure"
      - "traefik.http.routers.web_1.rule=Host(`yourdomain.com`)"
      #tls config
      - "traefik.http.routers.web_1.tls=true"
      - "traefik.http.routers.web_1.tls.certresolver=le_dns"
      - "traefik.docker.network=websites"
    networks:
      - websites_net

networks:
  websites_net:  #define the network
    name: websites
    driver: bridge

Note: if you use the docker-compose above, be sure to test using the staging server to avoid being rate-limited.

Domain Registrar API

Do you recall how I said that the configuration will need to be changed based on which domain registrar you use? Well luckily, making those changes is easy. Notice how I have the environment field defined under my traefik container?

environment:
    - NAMECHEAP_API_USER=YOUR_USERNAME_HERE
    - NAMECHEAP_API_KEY=YOUR_API_KEY_HERE

These values aren’t magic, they must be defined to interact with the Namecheap API. Most Domain Registrars have a similar api that traefik can make use of. To tailor the docker-compose to your registrar, follow these steps:

  • Find the providers table in the Traefik docs.
  • Find your provider in the table (ex. Namecheap, Google Domains, etc.)
  • Define all values in the “Environment Variables” column for that entry under the traefik environment: key.
  • Use the “Additional Configuration” link in the table for more advanced config needs.

Here is a quick example of what it would look like to configure the Google Domains provider based on that table:

environment:
    GOOGLE_DOMAINS_ACCESS_TOKEN=YOUR_ACCESS_TOKEN_HERE

Summary

The docker-compose above may look a bit complex, but I assure you that it is not too bad once you get a feel for it. We define one router for handling HTTPS traffic. It forwards requests to the appropriate container based on the domain labels we applied to the containers, such as the web container. To obtain the certificates, we define a traefik DNS challenge certificate resolver using these lines:

- "--certificatesresolvers.le_dns.acme.dnsChallenge=true"
- "--certificatesresolvers.le_dns.acme.email=your@email.com"
- "--certificatesresolvers.le_dns.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.le_dns.acme.dnsChallenge.provider=namecheap"
- "--certificatesresolvers.le_dns.acme.dnsChallenge.delayBeforeCheck=30"  
- "--certificatesresolvers.le_dns.acme.dnsChallenge.resolvers=9.9.9.9:53,1.1.1.1:53"
- "--certificatesresolvers.le_dns.acme.dnschallenge.disablepropagationcheck=true"
- "--serversTransport.insecureSkipVerify=true"
- "--certificatesresolvers.le_dns.acme.caServer=https://acme-v02.api.letsencrypt.org/directory"
#- "--certificatesresolvers.le_dns.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory"

These lines create and configure a traefik DNS challenge resolver called le_dns (name chose by me), ensuring it has proper email, certificate storage location, DNS provider, CA server, and DNS propagation check attributes set. For more information on these values, see the Traefik documentation. A few things to note about the lines above:

- "--certificatesresolvers.le_dns.acme.dnsChallenge.delayBeforeCheck=30"

Instructs Traefik to delay the process of gettting a certificate so the TXT record set on your domain can propagate across DNS servers.

- "--serversTransport.insecureSkipVerify=true"

You may need this to make your config work properly with certain DNS providers.

Once this docker-compose is set up and ready to go, all there is left to do is test. Spin the project up and visit the domain your configured, and if all goes well, your traefik container should use a DNS challenge to get a Let’s Encrypt certificate for your site, allowing you to connect to your website over HTTPS!

If you are interested in another method for obtaining certificates for your containers, be sure to read my post on using a Traefik HTTP Challenge to obtain certificates.

Comments

Leave a Reply

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