Shore Up
A worn wax seal stamp being retired to a drawer while a freshly engraved stamp takes its place beside a stack of outgoing letters, a calendar on the wall marking the changeover date.
LinuxMail

Automate DKIM Key Checks and Rotation on OpenDKIM

Ketan Aagja9 min read
No ratings yet

Before you run this

This guide has two parts. The check part is a read-only script that queries DNS for your published DKIM record and confirms it still matches the private key OpenDKIM signs with — safe to run any time, and safe to put on cron. The renewal part generates a new key under a new selector, has you publish a DNS record, and then switches signing over to it.

  • Privileges: the check script only needs to read your key files and query DNS. If your keys are in /etc/opendkim/keys (mode 0700, owned by opendkim), you'll run it with sudo or as opendkim. The renewal steps write into /etc/opendkim and restart a service, so they need root/sudo.
  • Test first: read every line before running it, and run the renewal on a test domain or a staging mail server before your live one. Generating a key is harmless and additive; the dangerous moment is switching the active selector before the new DNS record has propagated, or removing the old selector too soon — either one makes freshly sent mail fail DKIM verification and land in spam.
  • What changes, and reversibility: generating a key adds files, changes nothing live. Editing the KeyTable/SigningTable and restarting OpenDKIM is the live change. It is reversible: point the tables back at the old selector and restart, as long as the old key's DNS record is still published. That is exactly why you keep the old DNS record for a grace period rather than deleting it immediately.

Assumed environment: Debian 12, Postfix, OpenDKIM installed from the distro package, keys under /etc/opendkim/keys/<domain>/, and /etc/opendkim.conf referencing a KeyTable and SigningTable. If you run rspamd or amavisd-new for DKIM signing instead, the concepts (selector, key pair, DNS TXT record, grace period) are identical but the file layout and reload command differ — check that project's docs. Throughout, replace example.com with your mail domain.

The read-only check script

opendkim-testkey is the right tool: it resolves the DKIM DNS record for a selector and compares it against the local private key, returning a non-zero exit code on mismatch. That's everything a health check needs.

#!/usr/bin/env bash
# dkim-check.sh — verify the published DKIM record still matches the local key.
# Read-only. Exit non-zero (and mail root) on any problem.
set -euo pipefail

DOMAIN="example.com"                              # your mail domain
SELECTOR="202501"                                 # the ACTIVE selector
KEY="/etc/opendkim/keys/${DOMAIN}/${SELECTOR}.private"
FQDN="${SELECTOR}._domainkey.${DOMAIN}"

# 1. Is the record even published?
if [ -z "$(dig +short TXT "${FQDN}")" ]; then
    echo "FAIL: no TXT record at ${FQDN}" >&2
    exit 1
fi

# 2. Does the published public key match our private key?
#    -k gives the private-key path; testkey resolves DNS and compares.
if opendkim-testkey -d "${DOMAIN}" -s "${SELECTOR}" -k "${KEY}" -vvv; then
    echo "OK: ${FQDN} matches local key"
else
    echo "FAIL: DNS record does not match ${KEY}" >&2
    exit 1
fi

Run it once by hand first:

sudo bash dkim-check.sh

opendkim-testkey prints its verdict on the last line (a successful run reports the key verified). Confirm the flags against man opendkim-testkey on your box — the -d/-s/-k form is standard, but options do occasionally vary by package version.

To run it daily and get an email only when something breaks, add a cron entry (cron mails you any output on a non-zero exit if you keep the >&2 failures):

# /etc/cron.d/dkim-check — run 07:15 daily, mail root on failure
15 7 * * * root /usr/bin/env bash /opt/scripts/dkim-check.sh > /dev/null

Because the script exits non-zero and writes to stderr on failure, cron delivers only the failure text to root's mail. This catches the two things that actually go wrong in practice: someone edited the zone and dropped the record, or the key files drifted from what's published.

Generating a new key (renewal, phase 1)

Rotate by introducing a new dated selector rather than overwriting the current one. That way both keys can coexist while DNS propagates.

#!/usr/bin/env bash
# dkim-genkey.sh — create a new DKIM key under a dated selector.
# Additive: writes a new key pair and prints the DNS record. Nothing goes live.
set -euo pipefail

DOMAIN="example.com"
SELECTOR="$(date +%Y%m)"                          # e.g. 202506
KEYDIR="/etc/opendkim/keys/${DOMAIN}"
BITS=2048

install -d -m 0700 -o opendkim -g opendkim "${KEYDIR}"

# -r restricts the key to e-mail use in the generated record.
opendkim-genkey -b "${BITS}" -r -d "${DOMAIN}" -s "${SELECTOR}" -D "${KEYDIR}"

chown opendkim:opendkim "${KEYDIR}/${SELECTOR}.private"
chmod 600 "${KEYDIR}/${SELECTOR}.private"

echo "Private key: ${KEYDIR}/${SELECTOR}.private"
echo "--- Publish this DNS TXT record, then wait for propagation ---"
cat "${KEYDIR}/${SELECTOR}.txt"
sudo bash dkim-genkey.sh

The .txt file contains the record in BIND format, e.g.:

202506._domainkey  IN  TXT ( "v=DKIM1; h=sha256; k=rsa; s=email; "
          "p=MIIBIjANBgkqhkiG9w0BAQEF..." )

Publish that as a TXT record at 202506._domainkey.example.com in your DNS (concatenate the quoted strings into one value — many providers' web forms want the p= value as a single string). Leave the old selector's record in place.

Verify propagation before switching

Do not touch signing until the new record actually resolves and matches. Reuse the check logic against the new selector:

dig +short TXT 202506._domainkey.example.com
sudo opendkim-testkey -d example.com -s 202506 \
     -k /etc/opendkim/keys/example.com/202506.private -vvv

When opendkim-testkey reports the new selector verified, you're ready. Give public DNS a little longer than your record's TTL to be safe.

Switch signing to the new selector (phase 2)

Back up the tables first:

sudo cp /etc/opendkim/KeyTable     /etc/opendkim/KeyTable.bak
sudo cp /etc/opendkim/SigningTable /etc/opendkim/SigningTable.bak

Update the entries to point at the new selector. Confirm the exact paths in your /etc/opendkim.conf (KeyTable and SigningTable directives) — mine reference these files:

# /etc/opendkim/KeyTable
202506._domainkey.example.com example.com:202506:/etc/opendkim/keys/example.com/202506.private
# /etc/opendkim/SigningTable
*@example.com 202506._domainkey.example.com

Then restart OpenDKIM so it reloads the tables:

sudo systemctl restart opendkim
sudo systemctl status opendkim --no-pager

Verify the switch worked

Send a message from the domain to an address you control and inspect the received headers — the DKIM-Signature should now show s=202506, and the receiving side should record dkim=pass. Gmail's "Show original" view reports the DKIM result plainly; so do most mail-test services. You can also watch your own logs:

journalctl -u opendkim -n 50 --no-pager

Confirm the check script now passes against the new selector by updating SELECTOR in dkim-check.sh to 202506 and running it.

Grace period and cleanup

Keep the old selector's DNS record and private key for at least a week (a few days beyond your longest expected mail-in-transit / retry window) so any messages signed before the switch still verify at the receiver. After that, delete the old TXT record from DNS and remove the stale key files.

How to roll back

If mail starts failing DKIM right after the switch, restore the previous tables and restart — this works only while the old DNS record is still published, which is why you kept it:

sudo cp /etc/opendkim/KeyTable.bak     /etc/opendkim/KeyTable
sudo cp /etc/opendkim/SigningTable.bak /etc/opendkim/SigningTable
sudo systemctl restart opendkim

For the authoritative options on the generator and test tools, see man opendkim-genkey and man opendkim-testkey, and the OpenDKIM documentation for the KeyTable/SigningTable formats.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.