Local Network SSL with Caddy and Cloudflare

Since I’ve been using OrangePi Zero as my current server for my homelab, I was so curious regarding the PKI or TLS/SSL world that can be implementation in the local area network (LAN). I’ve ever to try create raw/natively PKI server to using default OpenSSL then a bit doing automation it using Ansible or with a fresh tech stack using smallstep (might will write it out someday 🙄).

But yeah after that, just found some interesting things when googling like is it possible if using the ACME concept? Like dealing with Let’s Encrypt but for local network and a bit just thinking then looks like i need some public IP to do that. But I found this great write up from SamEdwardes: Automatic Homelab HTTPS with Caddy and Cloudflare .

Suddenly after quickly reading that write up, was remembered that DNS it’s just a common DNS like. Probably Domain or DNS that we usually registering nameserver publicly via common DNS provider like Cloudflare or Route53, similiarities with the “zone” concept with bind9 or dnsmasq, perhaps something like “DNS table” like under /etc/hosts 🤷.

How about making it work with Let’s Encrypt ACME? It’s simple, As long as my domain was publicly registered under those providers. Technically, the challenge that I used was DNS-01 . Why? Because if I had public IP I don’t need to install it in local network or it will be need another requirement like adding a tunnel to obtain some public IP a.k.a straightforward with HTTP-01 challenge.

High Level Diagram

The Workflows:

  1. Client/User/Devices try to access bump.riskiwah.xyz .
  2. As long as all my clients resolve to my CoreDNS, it will be answered/translating to IP addresses. Let’s say it will be resolved like below.     bump.riskiwah.xyz #it also my Caddy server IP
  1. Make sure Caddy can resolve the challenge ACME from Cloudflare records. Then if the challenge was successful, it will return the SSL verification done, and Let’s Encrypt will trust the server. This process usually was done when the Caddy server is up and running first.
  2. After trusting it, Caddy will continue the request to upstream. Which is it can be another server/service/any applications.

Quick Setup

For the quick proof of concept (POC) I’ll using docker compose. Since it will use Cloudflare as a public nameserver resolver, we need to install this module caddy-dns/cloudflare to make it working.

Also, make sure the DNS record has already been created on the Cloudflare side with a specific IP address, such as, which may involve some magic tricks. If you are interested in learning more about IP addresses like 100.x.x.x, you can read more here .

Example the Dockerfile.

FROM caddy:2-builder-alpine as build
RUN xcaddy build \
    --with github.com/caddy-dns/cloudflare

FROM caddy:2-alpine as runtime
COPY --from=build /usr/bin/caddy /usr/bin/caddy

After that, prepare docker-compose.yaml file like below.

version: "3.9"
    build: .
    container_name: caddy-dns
    hostname: caddy
      - majumundur-dev
      - caddy.env
      - "80:80"
      - "443:443"
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
    image: nginx:alpine
    container_name: nginx
    hostname: nginx
      - majumundur-dev
      - 80

    external: true

Next step, create Caddyfile for Caddy configuration and some environment variable file (for this case using caddy.env).

# Caddyfile
https://bump.riskiwah.xyz {
    reverse_proxy nginx:80
    tls {
        dns cloudflare {$CF_TOKEN}
# caddy.env

Then execute it with docker compose up and you will see stdout logs that Caddy will request the certificate to Let’s Encrypt with verifying DNS-01 challenge to Cloudflare then order the certificate.

{"level":"info","ts":1710602408.8543596,"logger":"tls.issuance.acme","msg":"waiting on internal rate limiter","identifiers":["bump.riskiwah.xyz"],"ca":"https://acme-v02.api.letsencrypt.org/directory","account":""}
{"level":"info","ts":1710602408.854439,"logger":"tls.issuance.acme","msg":"done waiting on internal rate limiter","identifiers":["bump.riskiwah.xyz"],"ca":"https://acme-v02.api.letsencrypt.org/directory","account":""}
{"level":"info","ts":1710602409.6186998,"logger":"tls.issuance.acme.acme_client","msg":"trying to solve challenge","identifier":"bump.riskiwah.xyz","challenge_type":"dns-01","ca":"https://acme-v02.api.letsencrypt.org/directory"}
{"level":"info","ts":1710602416.1446314,"logger":"tls.issuance.acme.acme_client","msg":"authorization finalized","identifier":"bump.riskiwah.xyz","authz_status":"valid"}
{"level":"info","ts":1710602416.1446817,"logger":"tls.issuance.acme.acme_client","msg":"validations succeeded; finalizing order","order":"https://acme-v02.api.letsencrypt.org/acme/order/xxxx/xxxx"}
{"level":"info","ts":1710602418.1942368,"logger":"tls.obtain","msg":"certificate obtained successfully","identifier":"bump.riskiwah.xyz"}

Bump and it’s locked!.

Closing Thought

Well, this case is quite interesting and it motivates me to refresh my knowledge about DNS zones, ACME, DNS-01 challenges and Caddy server itself. Something that’s made me a bit feel redundant was having to create two name records in two zone which is in Cloudflare side and in my local DNS server (CoreDNS). But I guess if there were no local DNS server, it could be resolved directly in Cloudflare side without the need for those magic IP addresses (the 100.x.x.x).

Furthermore, by introducing two DNS records on two DNS servers, it’s possible to reduce some actors being able to reconnaissance your local IP Addresses from outside, eg: using nslookup or dig commands. Just my two cents 🤷.

Wondering to buy some public domain again…