Home Self Signed Certificates and DNS Setup using Traefik
Post
Cancel

Self Signed Certificates and DNS Setup using Traefik

Introduction

We are going to install traefik proxy in a docker container. The traefik proxy is going to use Let’s encrypt to get certificates(using cloudflare for the domain verifications). We are also going to setup DNS records to point to our traefik proxy which will inturn re-route the traffic to its respective domain.

With this setup, I will be able to get free wild card certificates for all our self hosted services while using cloudflare as our local DNS to route to our services that are going to protected with let’s encrypts secure certificates!!!

The following is the file structure that I have used, we can modify the structure, but doing so, we would also need to specify the said changes in the config and the docker compose file where we are mounting these changes.

1
2
3
4
5
6
7
./traefik
├── data
│   ├── acme.json
│   ├── config.yml
│   └── traefik.yml
└── cf_api_token.txt
└── docker-compose.yml

Setting up my environment.

Started by creating the folders above for the docker compose and the files that needs to be mounted to the docker container.

1
2
3
4
5
6
7
8
9
10
11
## For the docker compose. I have all the docker container files in a docker-volumes folder. 
mkdir docker_volumes
cd docker_volumes

## To hole the traefik files
mkdir traefik
cd traefik

## Create a compose file for creating the container
touch docker-compose.yaml
nano docker-compose.yaml

My docker compose files looks something like (traefik using .yml syntax for its config files)

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
version: "3.8"

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - proxy
    ports:
      - 80:80
      - 443:443
      # - 443:443/tcp # Uncomment if you want HTTP3
      # - 443:443/udp # Uncomment if you want HTTP3
    environment:
      CF_DNS_API_TOKEN_FILE: /run/secrets/cf_api_token # note using _FILE for docker secrets
      # CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN} # if using .env
      TRAEFIK_DASHBOARD_CREDENTIALS: ${TRAEFIK_DASHBOARD_CREDENTIALS}
    secrets:
      - cf_api_token
    env_file: .env # use .env
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/traefik.yml:/traefik.yml:ro # traefik config file
      - ./data/acme.json:/acme.json # this is needed with write permissions to update or write the certificate
      # - ./data/config.yml:/config.yml:ro  # This is to use traefik for any exiting services we have already deployed. Detailed info below.
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik.entrypoints=http"
      - "traefik.http.routers.traefik.rule=Host(`mysubdomain.domain.example.com`)"
      - "traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_DASHBOARD_CREDENTIALS}"
      - "traefik.http.middlewares.traefik-https-redirect.redirectscheme.scheme=https"
      - "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https"
      - "traefik.http.routers.traefik.middlewares=traefik-https-redirect"
      - "traefik.http.routers.traefik-secure.entrypoints=https"
      - "traefik.http.routers.traefik-secure.rule=Host(`mysubdomain.domain.example.com`)"
      - "traefik.http.routers.traefik-secure.middlewares=traefik-auth"
      - "traefik.http.routers.traefik-secure.tls=true"
      - "traefik.http.routers.traefik-secure.tls.certresolver=cloudflare"
      - "traefik.http.routers.traefik-secure.tls.domains[0].main=subdomain.example.com"
      - "traefik.http.routers.traefik-secure.tls.domains[0].sans=*.subdomain.example.com"
      - "traefik.http.routers.traefik-secure.service=api@internal"

secrets:
  cf_api_token:
    file: ./cf_api_token.txt

networks:
  proxy:
    external: true

Now, I am going to create an acme.json file where traffic can write or update the certificates. Had to make sure that the acme file is editable.

1
2
3
4
5
6
7
mkdir data
cd data
touch acme.json
chmod 600 acme.json

## Create a traefik file for its configuration/. By default, traefik will look for this file. 
nano traefik.yml

The above created traefik file contains the configuration for installing and configuring traefik, Which in my case looks something like,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
api:
  dashboard: true
  debug: true
entryPoints:
  http:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: https
          scheme: https
  https:
    address: ":443"
serversTransport:
  insecureSkipVerify: true
providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  # file: ## Commented this out initially to first have the treafik up and running. Once the traefik instance is up, we can add more services to it using this config.
  #   filename: /config.yml
certificatesResolvers:
  cloudflare:  ## I am using cloudflare as my local DNS and for DNS challange and verification, 
    acme:
      email: <example.email.com>
      storage: acme.json
      caServer: https://acme-v02.api.letsencrypt.org/directory # prod (default) has a rate limit. So, for testing and intial setup, i have used the below staging cert. 
      # caServer: https://acme-staging-v02.api.letsencrypt.org/directory # staging
      dnsChallenge:
        provider: cloudflare
        #disablePropagationCheck: true # uncomment this if you have issues pulling certificates through cloudflare, By setting this flag to true disables the need to wait for the propagation of the TXT record to all authoritative name servers.
        #delayBeforeCheck: 60s # uncomment along with disablePropagationCheck if needed to ensure the TXT record is ready before verification is attempted 
        resolvers:
          - "1.1.1.1:53"
          - "1.0.0.1:53"

We are also going to create a network called proxy for the container to use, as we have indicated so in the compose file.

1
docker create network proxy

Now, to store the cloudflare API Token, which will be used to verify our domain, I have created a folder cf_api_token.txt

Traefik Login Credentials

To generate the traefik login credentials, I have utilized, htpasswd, which can be installed by installing apache2-utils. To generate a password using this htpasswd

1
echo $(htpasswd -nB <userName>) | sed -e s/\\$/\\$\\$/g

Now, I have copied the password along with the username in a .env file, which will also be mounted along with all the other files to the docker container. While mounting, the credentials should resolve to TRAEFIK_DASHBOARD_CREDENTIALS variable, which is what we have specified in the compose file.

Now that I have all the configurations, I have created the docker container using docker compose

1
docker compose up -d ## For the initial creation of the container

Troubleshooting.

I have faced some issues, initially or after changing the cert url of cloudflare from staging to production. The follwoing steps are some steps I used to resolve the issues,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
docker ps ## To check if the container has been spinned up properly without any issues.
docker logs <container_name> ## Usually, if traefik is running successfully, we should not see any logs. if you have any logs, then something went wrong
docker exec -it <container_name> /bin/sh ## ssh into the container to check if all the files and secrets are mounter correctly

## In the docker container
top ## To see the services running
ls ## Check for the higher level files mounted
cat acme.json ## Should have a certificate same as in the server
cat traefik.yml ## Should have the same configuration
cat /run/secrets/cf_api_token ## Should echo out cloudflare's api token we created
echo env | grep -i TRAEFIK

## While changing out the staging certificate to production, clear out the acme file 
rm acme.json
touch acme.json
chmod 600 acme.json

## Recreating docker container
docker compose up -d --force-recreate

DNS Records

I have created a central A record that is pointing to my docker container and using this A record, I have created CNAME records for traefik that are mentioned in the compose file. This allows to change the IP of traefik whithout changing all the IPs of services that are associated to traefik.

Once the changes have been made, I have verified the name resolution

1
nslookup <subdomain.domain.example.com>

Adding External Routes

As mentioned above, I already have a few service up and running that i want to resolve using traefik. For this, I have to uncommented the config file in compose and traefik file. Now, I have also create this config file which looks something like

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
http:
 #region routers 
  routers:
    proxmox:
      entryPoints:
        - "https"
      rule: "Host(`proxmox.subdomain.example.com`)"
      middlewares:
        - default-headers
        - https-redirectscheme
      tls: {}
      service: proxmox
    pihole:
  
#endregion
#region services
  services:
    proxmox:
      loadBalancer:
        servers:
          - url: "<proxmox_url>"
        passHostHeader: true
#endregion
  middlewares:
    https-redirectscheme:
      redirectScheme:
        scheme: https
        permanent: true
    default-headers:
      headers:
        frameDeny: true
        browserXssFilter: true
        contentTypeNosniff: true
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 15552000
        customFrameOptionsValue: SAMEORIGIN
        customRequestHeaders:
          X-Forwarded-Proto: https

    default-whitelist:
      ipAllowList:
        sourceRange: # IPs that we want to allow
        - "10.0.0.0/8" 

    secured:
      chain:
        middlewares:
        - default-whitelist
        - default-headers

recreated the container again, one last time.

This post is licensed under CC BY 4.0 by the author.