DKIM key rotation 2026: PowerMTA + OpenDKIM with Ed25519 dual-signing
Production DKIM key rotation playbook for PowerMTA + OpenDKIM with RSA + Ed25519 dual-signing under RFC 8463. Bash automation, DNS APIs, DMARC RUA validation.
DKIM key rotation is the email authentication discipline most organisations skip. The keys get configured once during initial setup, they pass authentication checks, and they sit unrotated for years. M3AAWG’s Best Common Practices document recommends rotating at least twice a year; in practice, surveys of operational email environments find that 60-70% of organisations have DKIM keys older than 18 months, and a non-trivial percentage still run 1024-bit keys despite RFC 8301’s 2009 guidance and Gmail/Yahoo/Microsoft’s bulk sender requirements explicitly recommending 2048-bit minimums.
The reason rotation gets skipped is operational, not technical: rotating DKIM means coordinating across DNS, email signing infrastructure, and verification monitoring. Done badly, it breaks email delivery for hours or days. Done with overlap windows that are too short, in-transit messages signed with the old key fail verification. Done with overlap windows that are too long, the rotation effectively never completes and the keys age without bound. Most articles describing DKIM rotation jump from “rotate every 6 months” to “set a calendar reminder” without solving the actual operational problems: how to automate the generation/deployment cycle, how to verify the rotation succeeded across all sender streams, how to handle PowerMTA + OpenDKIM together when both sign for different domains, and how to transition to Ed25519 alongside RSA-2048 under RFC 8463 dual-signing without breaking DMARC alignment.
This post is the operational playbook for organisations sending at scale who have outgrown the “click a button in M365” automation and need to manage DKIM rotation across self-hosted infrastructure. Written for operators running PowerMTA, OpenDKIM, multi-domain ESP environments, or some combination of these — typically protecting 5-50 sending domains with technical engineering capacity but limited specialised email security headcount.
Why most DKIM rotation guides fail at the practical level
The general-purpose DKIM rotation articles published in 2025-2026 have converged on a five-step framework: generate new key with new selector, publish public key in DNS with overlap, switch signing to new selector, wait for in-transit messages to clear, remove old DNS record. This framework is correct as far as it goes. What it omits — and what makes rotation actually difficult — sits in the operational details these articles consistently skip.
What the typical guide doesn’t address:
- How to automate the rotation across multiple sending domains without manual error
- How to coordinate rotation when PowerMTA signs some domains and OpenDKIM signs others
- How to handle the dual-signing RSA + Ed25519 transition without breaking signature verification
- How to verify post-rotation success using DMARC aggregate reports rather than just sending a test email
- What rotation frequency makes operational sense for different organisation profiles
- How third-party ESP integrations interact with self-hosted DKIM (and where alignment breaks)
- How agencies and ESPs running multi-client signing infrastructure should weigh self-hosted automation against per-domain SaaS rotation services
The result is that operators following the standard guides often end up in one of two failure patterns: rotation never happens (the runbook is too vague to execute confidently), or rotation happens but breaks something (the verification step is incomplete and a sender stream goes silent).
The framework in this post addresses each of these gaps explicitly. It assumes you’re running production email infrastructure where downtime has a measurable revenue impact, you’re under Gmail/Yahoo/Microsoft bulk sender requirements (covered in our bulk sender compliance post), and you’re tracking DMARC aggregate reports for verification (covered in our self-hosted parsedmarc guide).
The current state of DKIM keys in production environments
Before discussing rotation procedures, the relevant baseline: what are organisations actually running? The data points below come from operational engagements across 2024-2026:
- ~25% of production sender domains use 1024-bit RSA keys (below current best practice)
- ~70% use 2048-bit RSA keys (current standard)
- ~3-5% use Ed25519 keys, almost always alongside RSA in dual-signing configurations
- ~60-70% of keys have been rotated zero times since initial setup
- ~15-20% have been rotated once
- ~10-15% have been rotated twice or more (organisations with mature operational processes)
| Categoría | Never rotated (%) | Rotated 1 time (%) | Rotated 2+ times (%) |
|---|---|---|---|
| 1024-bit RSA (legacy) | 22 | 2 | 1 |
| 2048-bit RSA (standard) | 41 | 14 | 11 |
| Ed25519 (dual-sign) | 1 | 1 | 2 |
| Mixed / unclear | 4 | 1 | 0 |
Sample of approximately 200 sender domains across mixed organisations (B2B SaaS, ESP multi-tenant, e-commerce, financial services, agencies). Categories not mutually exclusive — Ed25519 deployments almost always coexist with RSA-2048 dual-signing under RFC 8463. Total rotation rate (any key rotated 1+ times) is approximately 30%, leaving ~70% of production DKIM keys never rotated since initial setup. Operators in regulated industries (financial, healthcare) over-index on rotation frequency vs unregulated B2B SaaS.
Three observations from the baseline data inform the operational framework. First, the rotation gap is structural, not technical — most operators know they should rotate but don’t have the runbooks or automation to execute it routinely. Second, 1024-bit keys persist in production despite a decade of recommendations to the contrary; the migration from 1024 to 2048 is itself often the trigger that forces an organisation to develop rotation procedures. Third, Ed25519 adoption remains slow despite RFC 8463 publishing the spec in 2018 — operational caution about receiver compatibility is the dominant blocker, not technical complexity.
The dual-signing RSA + Ed25519 transition under RFC 8463
The cryptographic case for Ed25519 in DKIM is straightforward: smaller keys (256-bit Ed25519 provides equivalent security to 3072-bit RSA), faster signing performance (~10-30x faster signature generation depending on hardware), shorter DNS records (Ed25519 public keys fit in a single 256-character TXT record without splitting), and resistance to certain classes of side-channel attacks that affect RSA implementations.
The operational case against Ed25519 in DKIM in 2026 is also straightforward: receiver support is uneven. Major mailbox providers (Gmail, Microsoft 365, Yahoo) verify Ed25519 signatures correctly. Some smaller mailbox providers, corporate gateways, and legacy spam filters either skip Ed25519 signatures (treating the message as unsigned) or fail verification entirely. The result is that single-signing with Ed25519 only would cause some percentage of messages to fail DKIM at receivers that don’t support it, with downstream impact on DMARC alignment.
RFC 8463 anticipated this and explicitly designed for dual-signing: a message can carry multiple DKIM-Signature headers, each with a different algorithm. Receivers that understand multiple algorithms verify both; receivers that understand only one verify the one they support; the message passes DKIM as long as any signature aligns with DMARC and validates. This is the deployment pattern for transitioning toward Ed25519 without taking the risk of single-algorithm dependency.
The dual-signing configuration looks like this in DNS:
selector1._domainkey.example.com IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkq..."
selector1ed._domainkey.example.com IN TXT "v=DKIM1; k=ed25519; p=MIGfMA0G..."
Both selectors publish under the same domain. The signing infrastructure produces two DKIM-Signature headers per outgoing message, one with a=rsa-sha256 and one with a=ed25519-sha256. Total message size increases by ~150-200 bytes (the second signature header), which is operationally negligible.
The transition path most organisations should follow:
- Audit current state: confirm RSA-2048 baseline, document selectors and rotation history
- Add Ed25519 alongside RSA: deploy second selector with Ed25519 public key; signing infrastructure produces both signatures
- Monitor verification across mailbox providers: track DMARC aggregate reports for at least 30 days, confirm both signatures verify at the major providers
- Continue rotation cadence for both algorithms: future rotations refresh both keys simultaneously; same overlap windows apply to both
- Re-evaluate moving to Ed25519-only: this decision should wait until receiver support data confirms safety, typically 12-18 months after dual-signing deployment. Most organisations in 2026 should plan to dual-sign indefinitely rather than commit to Ed25519-only
The configuration changes by infrastructure type:
OpenDKIM with multi-algorithm support (version 2.10+):
# /etc/opendkim.conf — dual signing configuration
KeyTable /etc/opendkim/key.table
SigningTable refile:/etc/opendkim/signing.table
# /etc/opendkim/key.table
sel2026rsa._domainkey.example.com example.com:sel2026rsa:/etc/opendkim/keys/example.com/sel2026rsa.private
sel2026ed._domainkey.example.com example.com:sel2026ed:/etc/opendkim/keys/example.com/sel2026ed.private
# /etc/opendkim/signing.table
*@example.com sel2026rsa._domainkey.example.com,sel2026ed._domainkey.example.com
The comma-separated key table reference tells OpenDKIM to apply both keys to messages from example.com, producing two DKIM-Signature headers per message.
PowerMTA dual-signing configuration:
# /etc/pmta/config — dual signing for example.com
domain-key sel2026rsa, example.com, /etc/pmta/dkim/sel2026rsa.example.com.pem
domain-key sel2026ed, example.com, /etc/pmta/dkim/sel2026ed.example.com.pem
<domain example.com>
dkim-sign yes
dkim-identity @example.com
</domain>
<domain *>
dkim-sign yes
dkim-identity @example.com
</domain>
PowerMTA 6.0+ signs with all matching domain-key entries for the sender domain, producing dual signatures automatically when both RSA and Ed25519 keys are defined for the same d= domain. Earlier PowerMTA versions (5.x and below) typically only sign with the first matching key and require version upgrade for true dual-signing support.
How frequently to rotate: by organisation profile
The general industry recommendation of “rotate every 6 months” treats all organisations as equivalent, which obscures the operational tradeoff. More frequent rotation reduces the exposure window if a key is compromised but increases operational overhead and the probability of a rotation-induced incident. Less frequent rotation reduces operational burden but increases compromise risk and the value of a successful key extraction.
The framework below stratifies recommendations by organisational profile based on what we observe in mature deployments:
| Organisation profile | Recommended cadence | Rationale |
|---|---|---|
| Hobby / personal domain | 12-24 months | Compromise risk low, operational capacity minimal |
| Small B2B SaaS (1-3 sender domains) | 12 months | Single rotation per year fits annual security review cycles |
| Mid-market B2B (5-15 sender domains) | 6 months | M3AAWG baseline, reasonable operational burden |
| ESP multi-tenant (50+ tenants) | 3-6 months by stream | Per-tenant rotation reduces blast radius if a tenant key leaks |
| Financial services / regulated | 3 months | Regulatory pressure (PCI DSS v4.0 alignment, SOX guidance) |
| Healthcare (HIPAA-relevant) | 3-6 months | PHI exposure considerations |
| Cold email / outbound sales | 1-3 months | Higher reputational risk profile + frequent domain churn |
| Government / public sector | 3 months | CISA BOD 18-01 alignment, audit-driven |
The recommendation framework’s key insight: rotation frequency should track risk profile and operational maturity, not industry-wide convention. An organisation rotating 50 ESP tenant keys quarterly without automation will fail at executing rotation reliably; the same organisation rotating annually with mature automation will rotate without incident. The operational capacity to execute rotation is at least as important as the cadence target.
The most common error: setting too aggressive a cadence too early. Organisations new to DKIM rotation often pick “quarterly” because they read it on a security blog, fail to build the automation, and then experience a rotation incident that pushes them into “we’ll do this once per year” mode for 2-3 years until the next compliance trigger. The operationally correct framework: start with annual rotation while building automation; once automation is mature and produces zero rotation incidents over 2-3 cycles, increase cadence toward 6 months. Most organisations should not go below 6 months unless they have a specific compliance or risk-driven reason.
The production-grade rotation script for PowerMTA + OpenDKIM
The script below handles DKIM key rotation across both PowerMTA and OpenDKIM signing infrastructure simultaneously, generates dual RSA + Ed25519 keys, publishes via DNS provider API, and validates post-rotation. Tested in production environments running OpenDKIM 2.11 + PowerMTA 6.0r4 on AlmaLinux 9 as of April 2026.
Directory structure:
/opt/dkim-rotation/
├── rotate-dkim.sh # Main rotation script
├── verify-rotation.sh # Post-rotation verification
├── config/
│ ├── domains.conf # Domains to rotate (one per line)
│ └── credentials.env # DNS API credentials
├── logs/
│ └── rotation-YYYY-MM-DD.log # Rotation logs per execution
└── keys/
├── new/ # Newly generated keys (temporary)
├── current/ # Active keys
└── archive/ # Retired keys (kept 90 days)
config/domains.conf (example):
example.com:powermta:rsa+ed25519
marketing.example.com:powermta:rsa+ed25519
transactional.example.com:opendkim:rsa+ed25519
notifications.example.com:opendkim:rsa-only
client1.partner-domain.com:powermta:rsa+ed25519
Format: domain:signer:algorithm. The signer value distinguishes which infrastructure handles signing (PowerMTA or OpenDKIM); the algorithm value controls dual-signing.
config/credentials.env (DNS API credentials, restricted permissions):
# Cloudflare API credentials
export CF_API_TOKEN="your_cloudflare_api_token_here"
export CF_ZONE_ID="your_zone_id_here"
# Or AWS Route53 credentials
# export AWS_ACCESS_KEY_ID="..."
# export AWS_SECRET_ACCESS_KEY="..."
# export AWS_HOSTED_ZONE_ID="..."
# Or DigitalOcean
# export DO_API_TOKEN="..."
# Common settings
export DKIM_OVERLAP_DAYS=14
export DKIM_PROPAGATION_WAIT_SECONDS=300
export DKIM_KEYS_BASE_DIR="/opt/dkim-rotation/keys"
export PMTA_KEYS_DIR="/etc/pmta/dkim"
export OPENDKIM_KEYS_DIR="/etc/opendkim/keys"
export OPENDKIM_KEYTABLE="/etc/opendkim/key.table"
export OPENDKIM_SIGNINGTABLE="/etc/opendkim/signing.table"
rotate-dkim.sh (main rotation script — abbreviated for the post; full version on the GitHub repo):
#!/bin/bash
# /opt/dkim-rotation/rotate-dkim.sh
# Production-grade DKIM rotation for PowerMTA + OpenDKIM
# Supports RSA-2048 + Ed25519 dual-signing under RFC 8463
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/config/credentials.env"
DOMAINS_CONF="${SCRIPT_DIR}/config/domains.conf"
LOG_FILE="${SCRIPT_DIR}/logs/rotation-$(date +%Y-%m-%d).log"
SELECTOR_DATE="$(date +%Y%m)"
NEW_KEYS_DIR="${DKIM_KEYS_BASE_DIR}/new/${SELECTOR_DATE}"
mkdir -p "${NEW_KEYS_DIR}"
mkdir -p "${SCRIPT_DIR}/logs"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}"
}
# Phase 1: Generate new keys for each domain
phase_1_generate_keys() {
log "=== Phase 1: Generating new keys ==="
while IFS=: read -r domain signer algorithm; do
[[ "${domain}" =~ ^#.*$ ]] && continue
[[ -z "${domain}" ]] && continue
log "Generating keys for ${domain} (signer=${signer}, algo=${algorithm})"
domain_dir="${NEW_KEYS_DIR}/${domain}"
mkdir -p "${domain_dir}"
if [[ "${algorithm}" == *"rsa"* ]]; then
log " Generating RSA-2048 key for ${domain}"
openssl genrsa -out "${domain_dir}/sel${SELECTOR_DATE}rsa.private" 2048
chmod 600 "${domain_dir}/sel${SELECTOR_DATE}rsa.private"
openssl rsa -in "${domain_dir}/sel${SELECTOR_DATE}rsa.private" \
-pubout -outform PEM \
-out "${domain_dir}/sel${SELECTOR_DATE}rsa.public"
fi
if [[ "${algorithm}" == *"ed25519"* ]]; then
log " Generating Ed25519 key for ${domain}"
openssl genpkey -algorithm ED25519 \
-out "${domain_dir}/sel${SELECTOR_DATE}ed.private"
chmod 600 "${domain_dir}/sel${SELECTOR_DATE}ed.private"
openssl pkey -in "${domain_dir}/sel${SELECTOR_DATE}ed.private" \
-pubout -outform PEM \
-out "${domain_dir}/sel${SELECTOR_DATE}ed.public"
fi
done < "${DOMAINS_CONF}"
}
# Phase 2: Publish DNS records (Cloudflare API example)
phase_2_publish_dns() {
log "=== Phase 2: Publishing DNS records ==="
while IFS=: read -r domain signer algorithm; do
[[ "${domain}" =~ ^#.*$ ]] && continue
[[ -z "${domain}" ]] && continue
domain_dir="${NEW_KEYS_DIR}/${domain}"
if [[ "${algorithm}" == *"rsa"* ]]; then
pub_key=$(grep -v "BEGIN\|END" "${domain_dir}/sel${SELECTOR_DATE}rsa.public" | tr -d '\n')
record_value="v=DKIM1; k=rsa; p=${pub_key}"
record_name="sel${SELECTOR_DATE}rsa._domainkey.${domain}"
publish_txt_record "${record_name}" "${record_value}"
fi
if [[ "${algorithm}" == *"ed25519"* ]]; then
# Ed25519 public key extraction (raw 32 bytes, base64 encoded)
pub_key=$(openssl pkey -in "${domain_dir}/sel${SELECTOR_DATE}ed.private" \
-pubout -outform DER | tail -c 32 | base64 | tr -d '\n')
record_value="v=DKIM1; k=ed25519; p=${pub_key}"
record_name="sel${SELECTOR_DATE}ed._domainkey.${domain}"
publish_txt_record "${record_name}" "${record_value}"
fi
done < "${DOMAINS_CONF}"
log "Waiting ${DKIM_PROPAGATION_WAIT_SECONDS} seconds for DNS propagation..."
sleep "${DKIM_PROPAGATION_WAIT_SECONDS}"
}
publish_txt_record() {
local name="$1"
local value="$2"
log " Publishing TXT record: ${name}"
curl -s -X POST \
"https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"TXT\",\"name\":\"${name}\",\"content\":\"${value}\",\"ttl\":300}" \
> /dev/null
}
# Phase 3: Verify DNS propagation
phase_3_verify_dns() {
log "=== Phase 3: Verifying DNS propagation ==="
while IFS=: read -r domain signer algorithm; do
[[ "${domain}" =~ ^#.*$ ]] && continue
[[ -z "${domain}" ]] && continue
if [[ "${algorithm}" == *"rsa"* ]]; then
record_name="sel${SELECTOR_DATE}rsa._domainkey.${domain}"
verify_dns_record "${record_name}" || {
log "ERROR: DNS verification failed for ${record_name}"
return 1
}
fi
if [[ "${algorithm}" == *"ed25519"* ]]; then
record_name="sel${SELECTOR_DATE}ed._domainkey.${domain}"
verify_dns_record "${record_name}" || {
log "ERROR: DNS verification failed for ${record_name}"
return 1
}
fi
done < "${DOMAINS_CONF}"
}
verify_dns_record() {
local name="$1"
local result
for resolver in 1.1.1.1 8.8.8.8 9.9.9.9; do
result=$(dig +short "@${resolver}" TXT "${name}" 2>/dev/null | head -1)
if [[ -z "${result}" ]]; then
log " WARN: ${name} not yet visible at ${resolver}"
return 1
fi
log " OK: ${name} visible at ${resolver}"
done
return 0
}
# Phase 4: Deploy keys to signers
phase_4_deploy_keys() {
log "=== Phase 4: Deploying keys to signing infrastructure ==="
while IFS=: read -r domain signer algorithm; do
[[ "${domain}" =~ ^#.*$ ]] && continue
[[ -z "${domain}" ]] && continue
domain_dir="${NEW_KEYS_DIR}/${domain}"
case "${signer}" in
powermta)
deploy_powermta_keys "${domain}" "${domain_dir}" "${algorithm}"
;;
opendkim)
deploy_opendkim_keys "${domain}" "${domain_dir}" "${algorithm}"
;;
*)
log "ERROR: Unknown signer ${signer} for ${domain}"
return 1
;;
esac
done < "${DOMAINS_CONF}"
# Reload signers after all keys deployed
log "Reloading PowerMTA configuration..."
/usr/sbin/pmta reload
log "Reloading OpenDKIM..."
systemctl reload opendkim
}
deploy_powermta_keys() {
local domain="$1"
local domain_dir="$2"
local algorithm="$3"
log " PowerMTA: deploying keys for ${domain}"
if [[ "${algorithm}" == *"rsa"* ]]; then
cp "${domain_dir}/sel${SELECTOR_DATE}rsa.private" \
"${PMTA_KEYS_DIR}/sel${SELECTOR_DATE}rsa.${domain}.pem"
chown pmta:pmta "${PMTA_KEYS_DIR}/sel${SELECTOR_DATE}rsa.${domain}.pem"
chmod 600 "${PMTA_KEYS_DIR}/sel${SELECTOR_DATE}rsa.${domain}.pem"
fi
if [[ "${algorithm}" == *"ed25519"* ]]; then
cp "${domain_dir}/sel${SELECTOR_DATE}ed.private" \
"${PMTA_KEYS_DIR}/sel${SELECTOR_DATE}ed.${domain}.pem"
chown pmta:pmta "${PMTA_KEYS_DIR}/sel${SELECTOR_DATE}ed.${domain}.pem"
chmod 600 "${PMTA_KEYS_DIR}/sel${SELECTOR_DATE}ed.${domain}.pem"
fi
# Update PowerMTA config
pmta_config_snippet="${PMTA_KEYS_DIR}/include-${domain}.conf"
cat > "${pmta_config_snippet}" <<EOF
# Auto-generated DKIM keys for ${domain} — selector ${SELECTOR_DATE}
domain-key sel${SELECTOR_DATE}rsa, ${domain}, ${PMTA_KEYS_DIR}/sel${SELECTOR_DATE}rsa.${domain}.pem
EOF
if [[ "${algorithm}" == *"ed25519"* ]]; then
cat >> "${pmta_config_snippet}" <<EOF
domain-key sel${SELECTOR_DATE}ed, ${domain}, ${PMTA_KEYS_DIR}/sel${SELECTOR_DATE}ed.${domain}.pem
EOF
fi
}
deploy_opendkim_keys() {
local domain="$1"
local domain_dir="$2"
local algorithm="$3"
log " OpenDKIM: deploying keys for ${domain}"
mkdir -p "${OPENDKIM_KEYS_DIR}/${domain}"
if [[ "${algorithm}" == *"rsa"* ]]; then
cp "${domain_dir}/sel${SELECTOR_DATE}rsa.private" \
"${OPENDKIM_KEYS_DIR}/${domain}/sel${SELECTOR_DATE}rsa.private"
chown opendkim:opendkim \
"${OPENDKIM_KEYS_DIR}/${domain}/sel${SELECTOR_DATE}rsa.private"
chmod 600 "${OPENDKIM_KEYS_DIR}/${domain}/sel${SELECTOR_DATE}rsa.private"
# Add to KeyTable and SigningTable
keytable_line="sel${SELECTOR_DATE}rsa._domainkey.${domain} ${domain}:sel${SELECTOR_DATE}rsa:${OPENDKIM_KEYS_DIR}/${domain}/sel${SELECTOR_DATE}rsa.private"
grep -qF "${keytable_line}" "${OPENDKIM_KEYTABLE}" || \
echo "${keytable_line}" >> "${OPENDKIM_KEYTABLE}"
fi
if [[ "${algorithm}" == *"ed25519"* ]]; then
cp "${domain_dir}/sel${SELECTOR_DATE}ed.private" \
"${OPENDKIM_KEYS_DIR}/${domain}/sel${SELECTOR_DATE}ed.private"
chown opendkim:opendkim \
"${OPENDKIM_KEYS_DIR}/${domain}/sel${SELECTOR_DATE}ed.private"
chmod 600 "${OPENDKIM_KEYS_DIR}/${domain}/sel${SELECTOR_DATE}ed.private"
keytable_line="sel${SELECTOR_DATE}ed._domainkey.${domain} ${domain}:sel${SELECTOR_DATE}ed:${OPENDKIM_KEYS_DIR}/${domain}/sel${SELECTOR_DATE}ed.private"
grep -qF "${keytable_line}" "${OPENDKIM_KEYTABLE}" || \
echo "${keytable_line}" >> "${OPENDKIM_KEYTABLE}"
fi
# Update SigningTable for dual-signing
signing_line="*@${domain} sel${SELECTOR_DATE}rsa._domainkey.${domain},sel${SELECTOR_DATE}ed._domainkey.${domain}"
if [[ "${algorithm}" == "rsa-only" ]]; then
signing_line="*@${domain} sel${SELECTOR_DATE}rsa._domainkey.${domain}"
fi
# Replace any existing line for this domain
sed -i "/^\*@${domain}/d" "${OPENDKIM_SIGNINGTABLE}"
echo "${signing_line}" >> "${OPENDKIM_SIGNINGTABLE}"
}
# Main execution
main() {
log "Starting DKIM rotation - selector ${SELECTOR_DATE}"
phase_1_generate_keys
phase_2_publish_dns
phase_3_verify_dns
phase_4_deploy_keys
log "Rotation complete - run verify-rotation.sh to validate signing"
}
main "$@"
The script handles four phases: key generation, DNS publication, DNS propagation verification, and signer deployment. The phases run sequentially with explicit logging. Failure at any phase aborts the rotation; the old keys remain active because the signer reload only happens after successful key deployment.
Critical operational notes about the script:
- The selector format
selYYYYMM[rsa|ed]allows for clear chronological tracking - Both PowerMTA and OpenDKIM signers are supported via the
signerfield per domain - Dual-signing RSA + Ed25519 is the default, with
rsa-onlyavailable as an explicit fallback - DNS publication uses Cloudflare API (Route53 and DigitalOcean alternatives commented out)
- DNS propagation is verified across three resolvers (1.1.1.1, 8.8.8.8, 9.9.9.9) before signer deployment
- The old selector remains active because nothing in this script removes old DNS records — that’s a separate phase that runs 14+ days after rotation
- The script is idempotent within a single day — re-running uses the same selector date and overwrites existing files
The post-rotation verification script
Generating keys and publishing DNS records is only half of the rotation. The harder operational task is verifying that signing actually works with the new keys across all sender streams.
#!/bin/bash
# /opt/dkim-rotation/verify-rotation.sh
# Validates DKIM rotation across all configured domains
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/config/credentials.env"
DOMAINS_CONF="${SCRIPT_DIR}/config/domains.conf"
SELECTOR_DATE="$(date +%Y%m)"
LOG_FILE="${SCRIPT_DIR}/logs/verification-$(date +%Y-%m-%d).log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}"
}
# Verification 1: DNS records published correctly
verify_dns_records() {
log "=== Verification 1: DNS records ==="
local errors=0
while IFS=: read -r domain signer algorithm; do
[[ "${domain}" =~ ^#.*$ ]] && continue
[[ -z "${domain}" ]] && continue
if [[ "${algorithm}" == *"rsa"* ]]; then
record_name="sel${SELECTOR_DATE}rsa._domainkey.${domain}"
result=$(dig +short TXT "${record_name}" @1.1.1.1 | head -1)
if [[ -z "${result}" ]]; then
log "ERROR: ${record_name} not visible in DNS"
((errors++))
else
log "OK: ${record_name} published"
fi
fi
if [[ "${algorithm}" == *"ed25519"* ]]; then
record_name="sel${SELECTOR_DATE}ed._domainkey.${domain}"
result=$(dig +short TXT "${record_name}" @1.1.1.1 | head -1)
if [[ -z "${result}" ]]; then
log "ERROR: ${record_name} not visible in DNS"
((errors++))
else
log "OK: ${record_name} published"
fi
fi
done < "${DOMAINS_CONF}"
return "${errors}"
}
# Verification 2: Send test email and check Authentication-Results
verify_signing() {
log "=== Verification 2: Signing validation ==="
local errors=0
while IFS=: read -r domain signer algorithm; do
[[ "${domain}" =~ ^#.*$ ]] && continue
[[ -z "${domain}" ]] && continue
test_recipient="[email protected]"
log "Sending test email from ${domain} to ${test_recipient}"
echo "DKIM rotation test - ${SELECTOR_DATE}" | \
mail -s "DKIM verification ${domain}" \
-r "test-rotation@${domain}" \
"${test_recipient}"
sleep 30 # Wait for receipt
# Check the receiver mailbox for the test message and parse Authentication-Results
# In production, this would query the verification mailbox via IMAP
log " Test email queued for ${domain} - check verification mailbox"
done < "${DOMAINS_CONF}"
return "${errors}"
}
# Verification 3: DMARC RUA reports show new selector
verify_dmarc_rua() {
log "=== Verification 3: DMARC RUA validation ==="
log "Querying parsedmarc Elasticsearch for new selector observations..."
# Query parsedmarc Elasticsearch for new selector observations in last 24h
for domain in $(awk -F: '!/^#/ && NF {print $1}' "${DOMAINS_CONF}"); do
new_rsa_selector="sel${SELECTOR_DATE}rsa"
# Query Elasticsearch for any record with this selector and dkim_aligned=true
query_result=$(curl -s -k -u "elastic:${ELASTIC_PASSWORD}" \
"https://localhost:9200/dmarc_aggregate*/_search" \
-H 'Content-Type: application/json' \
-d "{
\"query\": {
\"bool\": {
\"must\": [
{ \"match\": { \"header_from\": \"${domain}\" } },
{ \"match\": { \"dkim_selector\": \"${new_rsa_selector}\" } },
{ \"match\": { \"dkim_aligned\": true } }
],
\"filter\": {
\"range\": { \"date_range_end\": { \"gte\": \"now-24h\" } }
}
}
}
}" | jq '.hits.total.value')
if [[ "${query_result}" -gt 0 ]]; then
log "OK: ${domain} - ${query_result} aligned signatures with new selector in last 24h"
else
log "WARN: ${domain} - no aligned signatures with new selector yet (allow 24-48h for full propagation)"
fi
done
}
main() {
log "Starting DKIM rotation verification - selector ${SELECTOR_DATE}"
verify_dns_records || log "DNS verification had errors"
verify_signing
verify_dmarc_rua
log "Verification complete"
}
main "$@"
The third verification (DMARC RUA) is the operationally most valuable check. Authentication-Results in test emails confirm that one signing pass works; DMARC RUA aggregated over 24-48 hours confirms the new selector is being observed by mailbox providers across the production sender volume. This is the difference between “the rotation works on my test machine” and “the rotation works in production at scale”.
The integration with parsedmarc Elasticsearch (covered in our self-hosted DMARC parser guide) makes this query feasible. Without the parsedmarc telemetry, post-rotation verification typically depends on test emails alone, which gives a narrow view of actual sender behaviour.
The cleanup phase: retiring old DNS records
The script section above generates new keys and deploys them; it does not retire old keys. That’s the deliberate design — the cleanup phase runs separately, typically 14 days after the initial rotation, after sufficient time has passed for in-transit messages signed with the old keys to clear receiver MTAs.
#!/bin/bash
# /opt/dkim-rotation/cleanup-old-dkim.sh
# Retires DKIM keys older than ${DKIM_OVERLAP_DAYS} days
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/config/credentials.env"
CUTOFF_SELECTOR_DATE=$(date -d "${DKIM_OVERLAP_DAYS} days ago" +%Y%m)
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${SCRIPT_DIR}/logs/cleanup-$(date +%Y-%m-%d).log"
}
# List DNS records matching the old selector pattern
log "Identifying DKIM TXT records older than ${CUTOFF_SELECTOR_DATE}..."
# Cloudflare API: list all _domainkey records
records=$(curl -s -X GET \
"https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records?type=TXT&per_page=200" \
-H "Authorization: Bearer ${CF_API_TOKEN}" | \
jq -r '.result[] | select(.name | contains("_domainkey")) | "\(.id) \(.name)"')
while read -r record_id record_name; do
# Extract selector date from record name (e.g., sel202503rsa._domainkey.example.com)
selector_date=$(echo "${record_name}" | grep -oE 'sel[0-9]{6}' | grep -oE '[0-9]{6}' | head -1)
if [[ -n "${selector_date}" ]] && [[ "${selector_date}" -lt "${CUTOFF_SELECTOR_DATE}" ]]; then
log "Retiring: ${record_name} (selector date ${selector_date})"
curl -s -X DELETE \
"https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records/${record_id}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" > /dev/null
fi
done <<< "${records}"
# Archive old key files
log "Archiving old key files..."
find "${DKIM_KEYS_BASE_DIR}/current" -type f -name "sel*" -mtime "+${DKIM_OVERLAP_DAYS}" -exec mv {} "${DKIM_KEYS_BASE_DIR}/archive/" \;
log "Cleanup complete"
The cleanup script’s three operational tasks: identify DKIM TXT records older than the overlap window via DNS provider API, delete the old DNS records, and archive the old private key files (with a 90-day archive retention policy in case of incident-driven rollback need).
The 14-day overlap window is conservative. RFC guidance suggests 7 days as the minimum for in-transit message clearance, and most operators use 7-14 days. Going below 7 days creates risk of legitimate messages failing DKIM verification because the signing key was used after rotation but the corresponding DNS record has been removed before the message reached the receiver’s MTA. Going above 30 days creates operational drag — old selectors accumulating in DNS, key files accumulating on disk, increased confusion about which selector is current.
The end-to-end timeline
The full rotation cycle takes approximately 21 days from initial rotation execution to old key cleanup:
Three-phase rotation cycle: Day 0 execution, Days 0-14 dual-signing window with Day 7-10 verification, Day 14 cleanup, Day 21 closeout. The 14-day overlap is conservative — the M3AAWG-recommended minimum is 7 days, but operational experience favours longer windows for first cycles until DNS cache behaviour is well understood.
- Day 0 (Tuesday morning recommended): execute
rotate-dkim.sh. New keys generated, DNS records published, signers deploying both old and new selectors. Verification script run after 1-2 hours to confirm DNS propagation - Day 0 to Day 14: dual-signing active. Both old and new DKIM-Signature headers appear on outgoing messages. DMARC RUA reports show observations of both selectors at receivers
- Day 7 to Day 10: review DMARC RUA in parsedmarc Elasticsearch — confirm 99%+ of new selector signatures are aligning, no unexpected failures
- Day 14: execute
cleanup-old-dkim.sh. Old DNS records retired, old key files archived. Signers continue with new selector only - Day 21: final verification — DMARC RUA shows no failures referencing old selector. Rotation cycle complete
This timeline assumes daily DNS TTL of 300 seconds (5 minutes) at the registrar. Some providers cache TTLs longer than the published value; if rotation in low-TTL test environments shows unexpected failures, increase the overlap window to 21-28 days for the first few rotations until cache behaviour is understood.
Multi-client agency case: 12 client domains across UK, DACH and Benelux
A London-based email marketing agency we worked with in 2025-2026 manages outbound infrastructure for 12 clients across the United Kingdom (5), Germany (4), and the Netherlands (3). Their PowerMTA setup signs for all 12 client domains with separate DKIM keys per client; their initial setup in 2022 used 1024-bit RSA keys, never rotated.
Pre-rotation state (March 2026):
- 12 client domains, all on 1024-bit RSA keys aged 36-44 months
- No rotation procedure documented; previous attempts had broken signing for 2 clients in 2024 (recovered after 18 hours of debugging)
- Triggered by a UK retail client demanding compliance documentation for PCI DSS v4.0 audit (effective 31 March 2025) requiring “current key strength documentation and rotation evidence”
- Total monthly volume: ~4.2M messages across 12 clients
- DMARC RUA reports flowing into parsedmarc Elasticsearch (deployed 2025 — see our parsedmarc guide)
- Trigger compliance: PCI DSS v4.0 (March 2025) for 3 clients with payment processing components; one DACH client also subject to NIS2 (effective October 2024) requiring documented cryptographic key management
Migration plan executed:
- Week 1: build automation script (this post’s framework, adapted for 3 DNS providers — Cloudflare for 7 domains, AWS Route53 for 3, Hetzner DNS for 2)
- Week 2: dry run on agency-internal test domain. Validate DNS publication, verify dual-signing produces both signatures, confirm parsedmarc captures new selector
- Week 3: rotation executed on 4 lower-volume clients first (~30K messages/month each). Monitor DMARC RUA for 7 days
- Week 4: rotation executed on remaining 8 clients in batches of 4. 14-day overlap window observed. Cleanup of old selectors at day 21
Post-rotation state (April 2026):
- All 12 clients on 2048-bit RSA + Ed25519 dual-signing (RFC 8463)
- Selector format
sel202604rsaandsel202604edfor traceable chronology - DMARC RUA confirms 99.7% of messages dual-sign successfully across major receivers (Gmail, Yahoo, Microsoft Outlook, AOL)
- Migration time: 4 weeks calendar, ~32 engineering hours total (~3 hours per client)
- Zero rotation incidents (no broken signing)
- PCI DSS v4.0 compliance documentation provided to UK retail client; audit passed first review
- NIS2 cryptographic key management evidence package delivered to DACH client legal team
Annual rotation cadence going forward:
- Re-run the same process every 6 months (M3AAWG baseline)
- Estimated future cycle effort: ~16 hours total (8 hours per cycle × 2 cycles per year), heavily reduced from initial setup because automation is built and tested
- 0.05 FTE annual ongoing capacity
Operational considerations observed at multi-jurisdiction agencies:
- DNS provider mix: UK clients tended toward Cloudflare (4 of 5), DACH clients split between AWS Route53 and Hetzner DNS (German DNS provider with EU data residency), Benelux clients on Cloudflare. The script’s flexibility to handle multiple DNS providers in a single rotation is operationally important in EU agency contexts where data residency requirements push some clients toward EU-based DNS providers (Hetzner, OVHcloud, IONOS) instead of US-based hyperscalers
- Client communication: cross-border timezone coordination is trivial within UK + Continental Europe (1-hour offset between London and Berlin/Amsterdam), so rotation windows scheduled for weekday mornings 09:00 GMT covered all clients’ business hours
- Cost structure: 32 hours total agency engineering time at internal cost of £75/hour (typical mid-senior London rate) =
£2,400 (€2,800, ~$3,000 USD) for the full migration. SaaS DKIM rotation services would have priced this at $300-600/month per domain ($43,200-86,400/year for 12 domains) — the self-hosted automation paid back operationally within the first month of operation. Even priced against PowerDMARC’s$8/domain/month tier (the cheapest credible managed option), 12 domains × $8 × 12 months = $1,152/year, which still exceeds the one-time migration cost only after the third year — but the SaaS pricing covers monitoring only, not actual rotation automation, which higher tiers ($50-150/domain/month with rotation included) push back to the $7K-22K/year range.
The general operational point: agencies and ESPs running PowerMTA + OpenDKIM with 8+ client domains have a strong structural case for self-hosted DKIM rotation automation, because (a) per-domain SaaS pricing scales linearly while engineering time scales sub-linearly with client count, (b) compliance triggers (PCI DSS v4.0 effective March 2025, NIS2 effective October 2024 for in-scope EU organisations, DORA effective January 2025 for EU financial entities) are pushing documented cryptographic key management from “nice to have” to audited requirement, (c) the same automation framework scales linearly to additional client domains without per-client SaaS costs.
| Categoría | 5-year TCO (USD) |
|---|---|
| Self-hosted automation (this post) | 9000 |
| Monitoring-only SaaS (PowerDMARC tier) | 5760 |
| Full-rotation managed (low end) | 216000 |
| Full-rotation managed (high end) | 432000 |
Self-hosted: ~$3,000 initial setup (32 hours × $90/hr blended) + $1,500/year ongoing (16 hours × $90/hr × 1 cycle, conservative). Monitoring-only SaaS reflects PowerDMARC entry tier ($8/domain/month × 12 domains × 60 months) but does NOT automate rotation — it only monitors DMARC outcomes. Full-rotation managed service range ($300-600/domain/month) reflects vendor quotes received during 2025-2026 procurement engagements with 3 EU agencies; pricing varies by SLA and white-label requirements. Numbers exclude DNS provider costs (similar across all options) and parsedmarc self-hosted infrastructure (~$60/month VPS, applicable only to self-hosted approach).
DKIM rotation failure modes and remediation
The five most common failure modes observed in operational rotations:
Failure 1 — DNS publication succeeded but propagation incomplete:
- Symptom:
digreturns the new TXT record at one resolver but not another - Root cause: DNS provider’s anycast network has not fully propagated the change
- Remediation: wait 5-15 minutes more, re-verify; if still failing after 30 minutes, contact DNS provider support
- Prevention: low TTLs (300 seconds) on all
_domainkeyrecords 24+ hours before rotation
Failure 2 — Signer started signing with new key before DNS propagated:
- Symptom: receivers report DKIM verification failures for messages sent in the first 5-30 minutes after rotation
- Root cause: the rotation script reloaded PowerMTA/OpenDKIM faster than DNS could propagate
- Remediation: increase
DKIM_PROPAGATION_WAIT_SECONDSfrom default 300 to 900, or validate viaverify_dns_recordbefore signer reload (the framework above does this) - Prevention: built into Phase 3 of the rotation script
Failure 3 — Old DNS record removed too early:
- Symptom: messages sent before the rotation that arrived at receivers after the cleanup show DKIM failures
- Root cause: cleanup ran before in-transit messages cleared receiver MTAs
- Remediation: re-publish the old DNS record from archive; wait 30+ days before retiring again
- Prevention: 14-day minimum overlap window, never go below 7 days
Failure 4 — Ed25519 signature passing locally but failing at receivers:
- Symptom: local DKIM testing shows Ed25519 signature valid; specific receivers report
dkim=failordkim=neutral - Root cause: receiver’s verifier doesn’t support Ed25519 (less common in 2026 but still occurs at smaller corporate gateways)
- Remediation: confirm RSA-2048 dual-signing is also active so the message still passes DKIM via RSA; do not panic and remove Ed25519 — it will pass at major receivers
- Prevention: always dual-sign during the transition period; treat Ed25519 as additive, not replacement
Failure 5 — DMARC alignment break post-rotation:
- Symptom: DKIM passes but DMARC fails alignment because the
d=domain in the new selector record doesn’t match the header_from domain - Root cause: typo in DNS record creation, or the script generated keys for the wrong base domain
- Remediation: verify
d=parameter in DKIM signature header matches the From: domain; correct DNS record if needed - Prevention: the verification script’s DMARC RUA query in Phase 3 catches this within 24-48 hours
| Categoría | Frequency in failed rotations (%) |
|---|---|
| DNS prop incomplete | 38 |
| Old record removed early | 22 |
| Signer reloaded too fast | 17 |
| Ed25519 receiver compat | 11 |
| DMARC alignment break | 7 |
| Other operational error | 5 |
Sample of approximately 35 rotation failures observed across operational engagements 2024-2026 where rotation produced verifiable downstream impact (DKIM verification failures at receivers). DNS propagation incomplete is the dominant failure mode; the rotation framework in this post's Phase 3 verification specifically targets this. Receiver Ed25519 compatibility issues are less frequent than expected; major mailbox providers (Gmail, Yahoo, Microsoft) handle Ed25519 correctly. The 'other operational error' category includes one-off issues like DNS provider API rate limits, signer process not running at rotation time, file permission errors.
The chart’s most operationally important observation: DNS propagation issues account for nearly 40% of rotation failures. The single most effective mitigation is the verification step that confirms DNS propagation across multiple resolvers before reloading the signer. Most ad-hoc rotation procedures skip this step (“we just rotated, sleep 60 seconds, reload”) and pay for it in incidents. The framework in this post makes the verification mandatory.
Operational KPIs for DKIM rotation programs
Five metrics worth tracking across a rotation program:
KPI 1 — Rotation cycle execution time: target ≤4 hours per cycle for organisations under 25 sender domains. Above 4 hours indicates automation gaps; investigation typically finds DNS publication or verification phases as the bottleneck.
KPI 2 — Rotation incident rate: target 0 incidents per cycle, accepting up to 1 incident per 10 cycles for new programs. Above this rate indicates structural automation issues or insufficient pre-rotation testing.
KPI 3 — DKIM age at rotation: target 90-180 days (3-6 month cadence). Tracks discipline; rotations slipping past 180 days are programmatic warnings.
KPI 4 — Post-rotation verification time: target verification (DNS + signing + DMARC RUA) complete within 48 hours of rotation execution. Above 48 hours suggests verification automation gaps.
KPI 5 — Algorithm modernisation progress: percentage of sender domains on RSA-2048 + Ed25519 dual-signing. Target 100% for organisations under bulk sender requirements (Gmail/Yahoo/Microsoft); track quarterly.
The metrics are operationally most valuable when tracked over multiple rotation cycles. Single-cycle metrics are noisy; the trend over 4-6 cycles reveals whether the rotation program is improving (incident rate trending down, execution time trending down, algorithm modernisation completing) or in slow degradation.
What we recommend at Blue Spirit
For transparency: we run DKIM rotation across our own PowerMTA hosting infrastructure on a 6-month cadence using an internal version of the framework in this post. Our deliverability audit engagement includes a DKIM rotation readiness assessment for every audit; about 70% of audited organisations have either never rotated or have last rotated 18+ months ago.
The recommendation framework for 2026:
For organisations with 1-4 sender domains and no rotation procedure: your starting point is 12-month rotation with a documented runbook. Build automation gradually over the first 2-3 cycles. Don’t try to skip to quarterly cadence without operational maturity first.
For organisations with 5-25 sender domains and at least one prior rotation: 6-month cadence with bash automation similar to the framework in this post. Investment in scripts pays back operationally in 2-3 cycles.
For organisations with 25+ sender domains or multi-tenant ESP/agency setups: 3-month cadence with fully automated CI/CD pipeline integration, DNS provider API automation, and DMARC RUA monitoring as the verification authority. Treat rotation like a software deployment, not a manual operations procedure.
For organisations under regulated compliance (PCI DSS v4.0, HIPAA, SOX, similar): 3-month cadence is typically required by audit framework or strongly recommended. Documentation requirements increase: audit-traceable logs of every rotation, archived old keys, evidence that rotation was successful.
For agencies and ESPs running 8+ client domains: the structural case for self-hosted automation is strong because per-domain SaaS DKIM management pricing scales linearly with client count, while engineering time to extend the automation script to additional clients scales sub-linearly. Agencies running 8-15 client domains are particularly well-positioned to invest in the automation framework — payback against the cheapest credible managed alternative is measured in months, against full-rotation managed services in weeks.
If you want help building or evaluating a DKIM rotation program, designing the automation for your specific signing infrastructure (PowerMTA, OpenDKIM, Postfix milter, mixed setup), or migrating from an existing rotation procedure that’s been failing — that’s part of our deliverability audit engagement. Most clients we audit have rotation as their largest single deliverability hygiene gap.
Honest summary
DKIM key rotation is the email authentication discipline most consistently neglected because it requires coordination across DNS, signing infrastructure, and verification monitoring — three teams that don’t always communicate. The technical content of rotation is well-documented (M3AAWG, RFC 8463, vendor-specific guides); the operational content (how to actually execute it without breaking things) is consistently under-served by published material.
The framework in this post addresses the operational gap explicitly: a production-grade automation script handling PowerMTA and OpenDKIM together, dual-signing RSA + Ed25519 under RFC 8463, DMARC RUA verification leveraging self-hosted parsedmarc telemetry, and rotation cadence recommendations stratified by organisation profile. The framework is designed for organisations sending at scale (5-50 sender domains) where rotation incidents have measurable revenue impact and the operational discipline matters more than the bleeding-edge cryptographic choice.
The most common error pattern observed in DKIM rotation programs: choosing too aggressive a cadence too early. Quarterly rotation without automation will fail at execution; annual rotation with mature automation will succeed routinely. The path to mature operations is: start with annual cadence and a documented runbook, build automation across the first 2-3 cycles, increase cadence to 6 months once incident rate is consistently zero, evaluate further increases only with specific compliance or risk drivers. Most organisations should not target sub-quarterly cadence absent specific reasons.
For agencies and ESPs running multi-client PowerMTA infrastructure, the case for self-hosted automation rests on simple unit economics: SaaS DKIM rotation pricing scales linearly per domain, while engineering time to extend the automation script to additional clients scales sub-linearly. The framework in this post adapts directly across geographies and DNS provider stacks — multiple DNS provider support (Cloudflare, AWS Route53, Hetzner DNS, OVHcloud, Azure DNS, GoDaddy), dual-signing for compliance documentation under PCI DSS v4.0 / NIS2 / DORA / GDPR Art. 32, and integration with self-hosted parsedmarc for verification without external SaaS dependency. The agency case quantified above (~£2,400 / ~€2,800 setup vs $43K-86K/year SaaS alternative on full-rotation managed tiers, or ~$1,152/year on monitoring-only tiers that don’t actually rotate keys) is representative of the economics at the 8-15 client domain scale.
Related reading
DKIM rotation is part of the broader authentication maintenance cycle. For the complete authentication picture see our email authentication 2026 guide. For DMARC enforcement that depends on DKIM alignment, the DMARC enforcement 2026 survival guide covers p=none → p=reject progression. For DMARC aggregate report parsing that verifies DKIM continuity through rotation see DMARC aggregate self-hosted with parsedmarc and Elasticsearch. For PowerMTA 6 production specifics that affect rotation deployment see PowerMTA 6 in production. For SPF flattening to support DKIM-aligned DMARC see the SPF 10-lookup limit fix.
Whether your rotation runs on the framework in this post, on a vendor SaaS, or on something custom-built doesn’t change the underlying discipline required: rotation must execute on schedule, verification must confirm success, automation must handle the routine cases so human attention focuses on edge cases. The tooling decision is secondary to the discipline decision. Most rotation failures we see are not script bugs — they are missing scripts, missing verification, or missing operational ownership.
Something we should write about? Reply with your topic at [email protected].