2023-10-02 — 3 min read

Caddy Wildcard Certificates with Cloudflare DNS Challenge

Wildcard certificates have their advantages and drawbacks. In this note, we will set them up with Caddy, and use a “variation” of Docker secrets to store and securely share the Cloudflare API token with the Caddy executable.

Benefits and drawbacks

Firstly, we should talk about the drawbacks. As the Information Sheet linked from this press release of the NSA details, there are several downsides of wildcard certificates. The main risk vector doesn’t concern my use case as I only have a single server, not multiple ones. Nevertheless, we should keep the disadvantages in mind before implementing this technique.

However, there are many benefits to wildcard certificates as well. For me, the main one is to keep the protected subdomains completely unknown to the public. Whenever we request an SSL certificate for a new subdomain, we effectively broadcast that “Hey, I created my.secret.subdomain.example.com, you should check it out!” to everybody. How? With Certificate Transparency. Every issued certificate is added to a secure and publicly available list of certificates that can be queried by anyone, for example at crt.sh. Looking up any domain, we can see every certificate ever issued for any subdomains in the last ~decade (the first CT log was launched in March 2013 by Google).

Create the Cloudflare API token

Let’s start with the API token: we’ll need this token for Caddy to be able to add or modify the required record for the DNS challenge. Since our DNS is managed by Cloudflare, we should open our Cloudflare profile, select API tokens, and create a new token with the permissions Zone.Zone and Zone.DNS. Save the generated token since it’s only shown this one time.

Build Caddy with the Cloudflare module

Next, we’ll need to build Caddy with the Cloudflare module included. The caddy service in our current docker-compose.yml begins like this:

# docker-compose.yml

service:
  caddy:
    image: caddy:2

We will need to replace the image: caddy:2 line with the following (replace the path as necessary):

    build:
      context: .
      dockerfile: ./caddy/Dockerfile

Then, create a Dockerfile at the above location, with the following content:

# Dockerfile

FROM caddy:2-builder AS builder
RUN xcaddy build --with github.com/caddy-dns/cloudflare
FROM caddy:2
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

This will build Caddy with the required module for our setup.

Securely share the API key with the Caddy executable

We will need to share the Cloudflare API token with the Caddy executable in a secure manner. In most tutorials, it’s handled with Docker environment variables even though the Docker documentation clearly discourages this:

Don’t use environment variables to pass sensitive information, such as passwords, in to your containers. Use secrets instead.

Unfortunately, Docker secrets are only accessible as files under /run/secrets, but we need the content of the file to use in our Caddyfile. There are workarounds like this to modify the container entrypoint to a script that converts secrets to environment variables, but with this method, we would need to add a new entrypoint script to our container.

Instead, we will use Caddy’s command line switch --entrypoint to provide our secret to the Caddy executable. In our docker-compose.yml, we define the secret, make it available to the Caddy service, then modify the container’s entrypoint to load the token from the secret file. If we define an entrypoint for a service in our docker-compose.yml, Docker won’t use the CMD or ENTRYPOINT instructions in the Dockerfile.

# docker-compose.yml
services:
  caddy:
    secrets:
      - cloudflare_api_token
    entrypoint: ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile", "--envfile", "/run/secrets/cloudflare_api_token"]

secrets:
  cloudflare_api_token:
    file: ./caddy/.cloudflare_api_token

We save our token in the above-mentioned file (in my case ./caddy/.cloudflare_api_token):

CF_API_TOKEN=<our secret token>

The file’s permissions should be 600 – only the file owner should be able to read and write it.

Lastly, we modify our Caddyfile:

*.example.com {
  tls {
    dns cloudflare {$CF_API_TOKEN}
  }
}

The rest of the Caddyfile follows this example pattern for wildcard certificates.

We can list the environment variables of the container with

# sudo docker compose exec [container name] /usr/bin/env

and check whether our API token is among them – it won’t be there. In conclusion, this is a safe way to share a secret with our containers.

Thanks for reading! If you have any comments, additions, or corrections, feel free to reach me via e-mail.

Copyright © 2023 csm.hu
Contact