Let's Encrypt wildcard certificates with DNS-01 and PowerDNS on AlmaLinux and Rocky Linux
Issue and renew a *.example.com certificate with certbot's DNS-01 challenge, publishing the proof in your own PowerDNS server with two small hook scripts.
A wildcard certificate for *.example.com covers every direct subdomain at once:
www, mail, shop, and any name you add next year. Let’s Encrypt issues wildcards
only through the DNS-01 challenge. Instead of serving a file over HTTP, you prove you
control the domain by publishing a TXT record at _acme-challenge.example.com.
That makes DNS-01 a natural fit when the DNS for the domain runs on your own server.
This tutorial sets up PowerDNS as the authoritative server for example.com, then has
certbot publish and remove the challenge record through two small hook scripts. Renewals
then happen on their own, with nobody editing DNS by hand.
DNS-01 has a second advantage: it works for names that are not websites. A mail hostname with no document root can still get a certificate.
Install PowerDNS and create the zone
PowerDNS keeps zones in a database; SQLite is plenty for one server.
dnf -y install dnf-plugins-corednf config-manager --set-enabled crbdnf -y install epel-releasednf -y install pdns pdns-backend-sqlite sqlite bind-utilssqlite3 /var/lib/pdns/pdns.sqlite3 < /usr/share/doc/pdns/schema.sqlite3.sqlchown -R pdns:pdns /var/lib/pdns
cat > /etc/pdns/pdns.conf <<'EOF'launch=gsqlite3gsqlite3-database=/var/lib/pdns/pdns.sqlite3local-address=0.0.0.0, ::setuid=pdnssetgid=pdnszone-cache-refresh-interval=0default-soa-content=ns1.@ hostmaster.@ 0 10800 3600 604800 3600EOF
systemctl enable --now pdnsfirewall-cmd --permanent --add-service=dnsfirewall-cmd --reloadzone-cache-refresh-interval=0 matters here more than anywhere. Without it, PowerDNS
only notices new zones every five minutes, and answers REFUSED in between.
Create the zone. 203.0.113.10 stands in for your server’s public IP:
pdnsutil zone create example.compdnsutil rrset add example.com example.com NS ns1.example.com.pdnsutil rrset add example.com ns1.example.com A 203.0.113.10pdnsutil rrset add example.com example.com A 203.0.113.10dig +short @127.0.0.1 NS example.com | grep -x 'ns1.example.com.'Write the two hook scripts
certbot’s --manual mode calls a script before validation, with the domain in
CERTBOT_DOMAIN and the value to publish in CERTBOT_VALIDATION, and calls a second
script afterwards to clean up. The first adds the TXT record:
cat > /usr/local/bin/certbot-pdns-auth <<'EOF'#!/bin/bash# certbot DNS-01 auth hook: publish _acme-challenge in the local PowerDNS.set -euo pipefailname="_acme-challenge.${CERTBOT_DOMAIN}"# The zone is the longest zone PowerDNS serves that the domain ends with.zone=$(pdnsutil zone list-all | awk -v d="$CERTBOT_DOMAIN" \ 'd == $0 || substr(d, length(d) - length($0)) == "." $0 { print length($0), $0 }' | sort -n | tail -1 | cut -d' ' -f2)[ -n "$zone" ] || { echo "no PowerDNS zone for $CERTBOT_DOMAIN" >&2; exit 1; }pdnsutil rrset add "$zone" "$name" TXT 60 "\"${CERTBOT_VALIDATION}\""EOF
cat > /usr/local/bin/certbot-pdns-cleanup <<'EOF'#!/bin/bash# certbot DNS-01 cleanup hook: remove _acme-challenge from the local PowerDNS.set -euo pipefailname="_acme-challenge.${CERTBOT_DOMAIN}"zone=$(pdnsutil zone list-all | awk -v d="$CERTBOT_DOMAIN" \ 'd == $0 || substr(d, length(d) - length($0)) == "." $0 { print length($0), $0 }' | sort -n | tail -1 | cut -d' ' -f2)# A wildcard plus its apex validate the same name twice; the first cleanup already# removed both values, so a missing record is not an error.pdnsutil rrset delete "$zone" "$name" TXT 2>/dev/null || trueEOF
chmod 700 /usr/local/bin/certbot-pdns-auth /usr/local/bin/certbot-pdns-cleanupA certificate for example.com plus *.example.com validates the same name,
_acme-challenge.example.com, twice with two different values. rrset add appends,
so both values are published together, which is what Let’s Encrypt expects.
Issue the wildcard certificate
dnf -y install certbotcertbot certonly --manual --preferred-challenges dns \ --manual-auth-hook /usr/local/bin/certbot-pdns-auth \ --manual-cleanup-hook /usr/local/bin/certbot-pdns-cleanup \ -d example.com -d '*.example.com' \ --non-interactive --agree-tos -m postmaster@example.comsystemctl enable --now certbot-renew.timerThe certificate and key are in /etc/letsencrypt/live/example.com/, ready to use in
Apache, Nginx, Postfix or Dovecot. certbot saves the hooks with the certificate, so the
renewal timer runs them again every time, without any manual step.
Check it
The certificate must name both the wildcard and the apex, the challenge record must be gone from the zone, and a renewal must work end to end:
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -ext subjectAltName | grep -F 'DNS:*.example.com'openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -ext subjectAltName | grep -F 'DNS:example.com'pdnsutil zone list example.com > /tmp/zone.txttest "$(grep -c _acme-challenge /tmp/zone.txt)" = 0certbot renew --cert-name example.com --dry-runThe check reads the zone with pdnsutil rather than asking with dig. PowerDNS caches
its answers for a few seconds, so a dig right after the cleanup can still show the
challenge record that was just removed. That is harmless, but it makes a confusing test.
The CorePanel way
This is how CorePanel issues every certificate. Its certificate manager uses the DNS-01
challenge only, against the local PowerDNS, and requests one certificate per domain
covering *.example.com and example.com. That single certificate covers the website,
mail., webmail. and ftp. at once. It is issued as soon as the domain is added and
renewed in the background, with no hooks to write.
See SSL certificates and DNS.