We have been running wildcard SSL certificates on our own projects — and for clients we maintain at /work/ — using Certbot with the dns-cloudflare plugin for years. The initial setup described in our wildcard-ssl-certbot-cloudflare article is straightforward, but the real maintenance burden shows up six to twelve months later when an automated renewal silently fails and we discover it only because a browser starts flashing red. This piece collects the production gotchas we have seen, how we prevent them, and the concrete commands we reach for when something breaks.

When DNS-01 renewal fails quietly

The DNS-01 challenge creates a TXT record via the Cloudflare API, waits for propagation, and removes the record after validation. A dry-run often passes because it skips the actual challenge handshake. In production we have watched real renewals fail with a timeout after the TXT record never propagated past Cloudflare's edge. The symptom is a Certbot error that looks like a generic timeout, but the root cause is usually the API token lacking Zone:Read or the dns-01 propagation delay being shorter than the actual TTL on the zone.

We set our renewal hooks to log explicitly. In /etc/letsencrypt/renewal/example.com.conf we add:

post_hook = /usr/bin/logger -t certbot-renew "Renewal finished for example.com"

And we check the hook status with systemctl status certbot.timer before we assume the timer is active.

Cron timing and overlapping runs

Certbot's systemd timer (or the legacy cron job) runs twice a day. We once saw a server where a previous engineer disabled the timer and added a custom crontab -e entry that ran every hour. Overlapping renewals created conflicting TXT records and caused NXDOMAIN errors in the Certbot log. We now enforce a single renewal mechanism per server. If we rely on cron, our entry is:

0 3,15 * * * root certbot renew --quiet --post-hook "systemctl reload nginx"

We never combine a crontab entry with a running certbot.timer; one must be disabled. We verify with systemctl is-active certbot.timer and systemctl is-enabled certbot.timer.

Cloudflare API token scopes

Using a global API key is a common shortcut, but a scoped token is safer and less likely to trigger rate limits or unexpected permission errors. Our token JSON looks like this:

{
  "Zone:Read": ["all"],
  "Zone:DNS:Edit": ["example.com"]
}

If the token is missing Zone:Read, Certbot can create the TXT record but fails during the cleanup phase. If Zone:DNS:Edit is too broad, Cloudflare may throttle requests during bulk renewals across multiple zones. We store the token in /etc/letsencrypt/cloudflare.ini with chmod 600, exactly as our wildcard article recommends, and we rotate the token every six months.

Propagation delays and dns-01 patience

Cloudflare's default TXT TTL is 120 seconds, but some zones inherit shorter or longer values. Certbot's default propagation wait is 10 seconds. We have seen renewals fail because the TXT record existed in the API but had not reached the authoritative resolver when Certbot queried it. We extend the wait by adding to our renewal config:

dns_cloudflare_propagation_seconds = 60

This is not always enough; some zones need 90 or 120 seconds. We measure with dig +trace TXT _acme-challenge.example.com from a remote server before we commit the configuration change.

Systemctl service failures and silent exits

On systems where systemctl is available, Certbot relies on certbot.service running under a timer. We have seen the timer fail silently because systemd considered the previous certbot process a success even when the renewal itself returned a non-zero exit code. We inspect with:

systemctl status certbot.timer
systemctl status certbot.service
journalctl -u certbot.service -n 50

When the timer is masked or disabled, we restore it:

systemctl unmask certbot.timer
systemctl enable certbot.timer
systemctl start certbot.timer

Without these checks, we assume automation is working and miss the certificate expiry window.

Log rotation and disk exhaustion

Certbot writes renewal logs to /var/log/letsencrypt/. We have watched a server fill its disk because a previous rotation policy kept every log file indefinitely. The logrotate configuration for /etc/logrotate.d/letsencrypt needs to include:

/var/log/letsencrypt/*.log {
    rotate 12
    weekly
    compress
    delaycompress
    missingok
    notifempty
}

When disk space drops below a safe threshold, the dns-cloudflare plugin cannot write temporary challenge files, and the renewal aborts without notifying anyone. We monitor /var/log/letsencrypt/ size with a simple mail alert:

echo "Certbot log size exceeded 500MB on $(hostname)" | mail -s "Certbot log rotation warning" ops@theeyeug.com

We run this from a nightly script that checks du -sh /var/log/letsencrypt/.

Certificate expiry monitoring

Even with automation, we treat monitoring as a separate layer. We query the live certificate directly:

openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -enddate

We parse the output with a Python snippet that compares the date to today plus 14 days. When it falls inside that window, we send an alert regardless of whether Certbot reports success. The alert is our last line of defense against rate limits, NXDOMAIN errors from upstream resolvers, or unexpected API token revocation.

Rate limits from Cloudflare are often temporary but can block renewal for hours. We have seen NXDOMAIN responses when the zone name in our cloudflare.ini did not exactly match the zone configured in Cloudflare — a subtle mismatch that only surfaces during the actual TXT query, not during dry-runs.

If you manage a site through our contact page, we include these checks in our monthly maintenance. For our own work at /work/, we apply the same rules without exception.

What we recommend now

Before you consider the automation complete, run certbot renew --dry-run, confirm systemctl status certbot.timer is active, verify the cloudflare.ini token scopes, check dig propagation timing on the zone, set up logrotate, and configure an expiry alert. We do not rely on any single mechanism; the combination of a scoped API token, a single timer or cron job, propagation patience, log rotation, and an independent expiry monitor is what keeps certificates from expiring unexpectedly.