Fixing ACME DNS‑01 on a Home Lab Using Pi‑hole and Cloudflare

Running a single DNS server on a Raspberry Pi or a tiny VM is a common pattern in home labs.
When Let’s Encrypt asks you to prove domain ownership via a DNS‑01 challenge, the classic “edit a TXT record” dance becomes a chore if you’re juggling Cloudflare’s web UI.
Below is a quick, repeatable recipe that plugs a Pi‑hole into Cloudflare’s API, lets Cert‑bot finish the challenge automatically, and keeps the whole thing lean and secure.

1. Prerequisites

Item Why it matters
Pi‑hole (v5.x) Acts as the local DNS server and a convenient spot to run scripts.
Cloudflare API token Needed to modify DNS records. Create a token with Zone.Zone and Zone.DNS read/write permissions only.
Cert‑bot (v2.10+) The ACME client that will request certificates.
Python 3.8+ Required for the cloudflare library used in the helper script.

Tip: Keep the API token in /etc/cloudflare/token and chmod 600 it. Avoid hard‑coding it in scripts.

2. Install the Cloudflare Python client

sudo apt-get update
sudo apt-get install -y python3-pip
sudo pip3 install cloudflare

3. Create the DNS‑01 helper

Save the following as /usr/local/bin/certbot-cloudflare.py and make it executable.

#!/usr/bin/env python3
import sys, os
from cloudflare import CloudFlare

cf = CloudFlare(token=os.environ["CF_API_TOKEN"])
zone_name = sys.argv[1]
record_name = sys.argv[2]
record_type = sys.argv[3]
record_content = sys.argv[4]
record_ttl = 120

zone_id = cf.zones.get(name=zone_name)[0]["id"]
records = cf.zones.dns_records.get(zone_id, params={"name": record_name, "type": record_type})

if record_content == "DELETE":
    for rec in records:
        cf.zones.dns_records.delete(zone_id, rec["id"])
else:
    if records:
        rec_id = records[0]["id"]
        cf.zones.dns_records.put(zone_id, rec_id, data={
            "type": record_type,
            "name": record_name,
            "content": record_content,
            "ttl": record_ttl
        })
    else:
        cf.zones.dns_records.post(zone_id, data={
            "type": record_type,
            "name": record_name,
            "content": record_content,
            "ttl": record_ttl
        })

Make it executable:

sudo chmod +x /usr/local/bin/certbot-cloudflare.py

4. Tell Cert‑bot to use the helper

Create a file /etc/letsencrypt/cli.ini with:

dns-cloudflare = /usr/local/bin/certbot-cloudflare.py
dns-cloudflare-credentials = /etc/cloudflare/token

Now request a certificate:

sudo certbot certonly --dns-cloudflare -d example.com -d *.example.com

Cert‑bot will call the helper to add a TXT record, wait for propagation, then delete it automatically.

5. Security & maintenance

  • Least‑privilege token – the token only has DNS read/write rights for the zone in question.
  • Pi‑hole isolation – keep the Pi‑hole subnet separate from the public internet; use iptables or ufw to block inbound traffic except for DNS (port 53).
  • Automated renewal – add a cron job that runs certbot renew nightly. The helper will again touch Cloudflare, so keep the token file secure.
  • Audit logs – Cloudflare keeps a record of API calls. Periodically review them for unexpected changes.

6. Common hiccups

Symptom Fix
“Zone not found” Verify the zone name matches exactly (case‑sensitive).
“Permission denied” Double‑check the token scopes.
TXT record not visible after creation Cloudflare may cache DNS. Use dig +noall +answer _acme-challenge.example.com TXT from an external host to confirm.

The helper script is intentionally minimal; you can extend it to support other record types or to log actions.
With this setup, your home lab can automatically renew Let’s Encrypt certificates without manual DNS edits, while keeping the Pi‑hole as a single point of control.


See also