# DKIM Record Publishing — Cloudflare
## Problem
OpenDKIM on Hetzner was signing outbound mail for all 21 client domains correctly (private keys generated, milter running, signatures added to every message) — but the public keys were never published to DNS. This caused Gmail/other recipients to fail DKIM verification, drop the spam score, and quarantine the mail.
Symptom: Wordfence alerts (and other transactional email from Kaburu sites) consistently flagged as spam in Gmail.
## Root Cause
CyberPanel's email installer generates DKIM keys locally and configures OpenDKIM, but does not publish the public key TXT records to Cloudflare. The records at `default._domainkey.<domain>` were either missing or contained the wrong (empty/stale) value.
## Fix Applied (2026-07-30)
Published `default._domainkey.<domain>` TXT records for all 21 Kaburu domains:
- 24hrresponse.com - aberdeenrda.co.uk - bankhousecatering.co.uk - claybusters.scot - dm-ph.com - doctorfin.co.uk - friendscic.org - ggsgenerators.co.uk - jafricasafari.com - kaburu.cc, kaburu.co, kaburu.co.uk - kaburusvr.uk - l8waterhygiene.co.uk - longniddrybowlingclub.com, .co.uk - myretonmarquees.co.uk - pilates-edinburgh.co.uk - thechippyvan.co.uk - touchwell.co.uk - womenunlimited.africa
## Verification
```bash dig TXT default._domainkey.24hrresponse.com +short # Should return: “v=DKIM1; h=sha256; k=rsa; p=MIIBIjA…”
ssh root@kaburusvr 'tail -20 /var/log/mail.log | grep opendkim' # Should show: “DKIM-Signature field added (s=default, d=24hrresponse.com)” ```
## Re-running the Publish Script
Located at: `/root/publish-dkim.sh` on kaburusvr (Hetzner)
What it does: 1. Reads CF token from `/root/.secrets/cloudflare` 2. Lists all zones in the Cloudflare account 3. For each domain in `/etc/opendkim/keys/`:
- Extracts the public key from `default.txt`
- Looks up the zone ID
- Creates or updates the `default._domainkey.<domain>` TXT record
When to re-run: - A new domain is added to the Hetzner server - DKIM keys are regenerated (e.g. by CyberPanel reinstall) - A new site is migrated to the server - DNS records need rotation (every 6-12 months for hygiene)
## Gotchas
- `grep -F “^domain|”` doesn't work — `^` with `-F` treats `|` as literal. Use `awk -F'|' '$1 == d'` instead. Caught on first run. - The script lives on Hetzner because it needs the CF token from `/root/.secrets/cloudflare` and the OpenDKIM keys from `/etc/opendkim/keys/`. Don't move it elsewhere. - DNS propagation: Cloudflare typically serves new records within 1-5 minutes. No need to wait for TTL. - DNSSEC warning (“key not secure”) from `opendkim-testkey` is informational only, doesn't block signing.
## Related
- opnsense — OPNsense firewall (separate email chain, Brevo SMTP fallback) - Wiki: `wiki/infrastructure/opnsense.md` — Unbound/DNS notes - Wiki: `wiki/web/cyberpanel.md` — CyberPanel-specific email setup
## Verification log (2026-07-30)
Before fix: ``` $ dig TXT default._domainkey.24hrresponse.com +short # (empty) ```
After fix: ``` $ dig TXT default._domainkey.24hrresponse.com +short “v=DKIM1; h=sha256; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi6SuVjoBZaUmtPVD+p6Nel23K5gFpJHfHR5hlM8r73AVjmC7toVZKblrIob0RFhe8xcT1IYS3vE0IECxV8oOlBSEFgKGVPkBvKt9gh3AqvG+LB8yeVOho7lypFScRZiENBE3GO+/Rj3wCjPq19BUn87TsNfB3EHO9IPjUykFACO91nUns0QwJkL…” “W5hu71HjOcExh+qNOumDpE1” ```
Op en DKIM log confirmed signing active throughout: ``` 2026-07-30T18:36:28 opendkim[1538]: 7AE90C5D26: DKIM-Signature field added (s=default, d=24hrresponse.com) … (5 more 24hrresponse.com entries from active brute-force block alerts) ```
