Setting Up DKIM Signing for Your Domain

DKIM (DomainKeys Identified Mail) attaches a cryptographic signature to every outbound message and publishes the matching public key in DNS, so receiving servers can prove the message was authorized by your domain and was not altered in transit. After reading this guide you will be able to generate a 2048-bit RSA key pair, publish the correct selector._domainkey record (as a TXT record you control or a CNAME pointing at your email service provider), enable signing, verify it with dig and a live test message, and rotate selectors safely without breaking mail flow.

Key implementation points:

  • DKIM signs a chosen set of headers plus a hash of the body; the receiver re-computes both hashes against the published public key.
  • The public key lives at <selector>._domainkey.<domain> as a TXT record, or behind a CNAME when your provider hosts the key.
  • Use 2048-bit RSA keys — 1024-bit is considered weak and many receivers downgrade trust on it.
  • DKIM only achieves identifier alignment (and therefore protects you) when paired with SPF and DMARC.
DKIM signing and verification sequence Sending MTA hashes the body and headers, signs with the private key, and adds a DKIM-Signature header. The receiver fetches the public key from selector._domainkey in DNS and re-verifies both hashes. Sending MTA DNS (selector._domainkey) Receiving MTA Hash body (bh=) + hash headers Sign with private key Message + DKIM-Signature header TXT query: selector key Public key (p=) Re-hash + verify pass = aligned

How DKIM Signing Works

When your mail server sends a message, it computes two hashes. The body hash (bh=) covers the message body after canonicalization. The header hash covers a specific, ordered list of headers named in the h= tag (typically From, To, Subject, Date, Message-ID). The server encrypts the header hash with its private key and writes the result into a DKIM-Signature: header inserted at the top of the message.

A receiving server reads the d= (domain) and s= (selector) tags from that header, queries DNS for <selector>._domainkey.<d> to fetch the public key, decrypts the signature, and re-computes both hashes. If the body hash and header hash both match, the signature passes. Crucially, the d= domain is what DMARC checks for alignment against the From: header — so a passing signature on an unrelated domain does nothing for you.

The selector is an arbitrary label (for example s1, mail, google, k1) that lets you publish multiple keys at once. This is what makes rotation possible: you publish a new key under a new selector, switch the signer to it, and retire the old one later.

Prerequisites and Environment Setup

You need:

  • Control of the authoritative DNS zone for the domain (or a delegated _domainkey subzone).
  • openssl 1.1+ and dig (bind-utils / dnsutils) on your workstation.
  • Either an email service provider (ESP) that signs for you, or a self-hosted MTA such as Postfix with OpenDKIM / rspamd.

Decide first whether your ESP signs on your behalf. Providers like Google Workspace, Amazon SES, SendGrid, Postmark and Mailgun generate the key pair, keep the private key, and ask you to publish a record. In that case you usually publish a CNAME that delegates the lookup to the provider, so they can rotate keys without you touching DNS. Self-hosting means you generate and hold the private key yourself and publish a TXT record.

Step 1: Generate a 2048-bit Key Pair (Self-Hosted)

Skip this step if your ESP issues the key. For a self-hosted MTA, generate the pair on the signing host:

# Private key — keep this readable only by the signer (e.g. opendkim)
openssl genrsa -out s1.private 2048

# Derive the public key for the DNS record
openssl rsa -in s1.private -pubout -out s1.public

Expected side effect: two files. s1.private must be chmod 600 and owned by the signing daemon. Never commit it or expose it over the network.

Now extract the raw base64 public key (strip the PEM header, footer and newlines) into the p= value:

grep -v '^-' s1.public | tr -d '\n'

This prints one long base64 string. That string is the p= tag value.

Step 2: Build and Publish the _domainkey Record

TXT record (self-hosted keys)

Publish the public key at <selector>._domainkey.<domain>. With selector s1 for example.com, the record name is s1._domainkey.example.com:

s1._domainkey.example.com. 3600 IN TXT ( "v=DKIM1; k=rsa; t=s; "
  "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArandombase64...IDAQAB" )

Tag meanings:

Tag Purpose Notes
v=DKIM1 Version Must be first when present.
k=rsa Key type rsa (default) or ed25519.
p= Public key Base64 from Step 1; empty p= revokes the key.
t=s Flags s = strict, no subdomain wildcard matching.
h=sha256 Hash algos Optional; restricts allowed hash.

Because a single DNS TXT string is capped at 255 octets, a 2048-bit p= value must be split into multiple quoted chunks inside one record (as shown above). The resolver concatenates the chunks; the visible split is not part of the key.

CNAME record (ESP-managed keys)

When a provider hosts the key, you publish a CNAME so DNS lookups for your selector resolve to their zone. Amazon SES, for example, issues three:

selector1._domainkey.example.com. 3600 IN CNAME selector1.dkim.amazonses.com.
selector2._domainkey.example.com. 3600 IN CNAME selector2.dkim.amazonses.com.
selector3._domainkey.example.com. 3600 IN CNAME selector3.dkim.amazonses.com.

The advantage: the provider rotates the underlying key in their zone and you never re-publish anything. Note that a CNAME cannot coexist with any other record at the same name, which is fine here because selector1._domainkey is a dedicated label.

Step 3: Enable Signing on the Sender

For a provider, signing is a console toggle — enable DKIM after the record resolves. For OpenDKIM, map the selector and key in /etc/opendkim/key.table and signing.table, then reload:

# /etc/opendkim/key.table
s1 example.com:s1:/etc/opendkim/keys/example.com/s1.private

# /etc/opendkim/signing.table
*@example.com s1
systemctl reload opendkim postfix

Send a test message and inspect the raw source. A correctly signed message carries a header like:

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com;
  s=s1; t=1718870400; bh=...; h=From:To:Subject:Date:Message-ID;
  b=Q9k2...signature...

c=relaxed/relaxed is the safest canonicalization pair: it tolerates whitespace and header-folding changes that mail systems routinely introduce, which prevents most body hash failures.

Step 4: Rotate Selectors

Never overwrite a live key in place — in-flight messages signed with the old key will fail verification mid-rotation. Instead, roll forward:

  1. Generate a new key under a new selector (s2).
  2. Publish s2._domainkey and wait for it to propagate everywhere.
  3. Switch the signer to s2.
  4. Leave s1._domainkey published for at least your longest mail-queue/retry window (24-72h), then remove it.

Because low TTL values let resolvers pick up the new selector faster, drop the record TTL before a planned rotation.

Verification

Confirm the record resolves and the key is intact:

dig +short TXT s1._domainkey.example.com
# "v=DKIM1; k=rsa; t=s; p=MIIBIjANBgkqhkiG9w0B...IDAQAB"

For a CNAME-delegated selector, follow the chain to the provider’s TXT:

dig +short CNAME selector1._domainkey.example.com
# selector1.dkim.amazonses.com.
dig +short TXT selector1.dkim.amazonses.com

Query against several public resolvers to catch propagation gaps before you enable signing:

for r in 1.1.1.1 8.8.8.8 9.9.9.9; do
  echo "== $r =="; dig +short TXT s1._domainkey.example.com @$r
done

Finally, send a message to a Gmail account, open Show original, and confirm DKIM: 'PASS' with domain example.com. The d= value must equal your From: domain (or an aligned subdomain) for DMARC to credit the pass.

Troubleshooting

Record split / length issues. A 2048-bit p= exceeds the 255-octet single-string limit. If your provider’s DNS UI rejects it or dig returns a truncated key, split the value into multiple quoted strings within one TXT record. Verify the joined output with dig +short TXT s1._domainkey.example.com | tr -d '" ' and compare against your s1.public base64.

Selector mismatch. Verification fails with “key not found” when the s= tag in the DKIM-Signature header does not match the published selector. Read the header from a real message (s=s1) and confirm a record exists at exactly s1._domainkey.<d>. A common cause is enabling a provider’s signing before its CNAME has propagated.

Body hash failures (bh mismatch). If a mailing list, security gateway, or mail-followup rewrite appends a footer or alters the body after signing, the recomputed body hash will not match and verification fails while the header signature may still look valid. Use c=relaxed/relaxed canonicalization, and where a relay must modify content, have it re-sign rather than forward the original signature.

Empty p= accidentally revokes. An empty p= value is the official revocation signal. If you paste an incomplete key, receivers treat the key as revoked and the message fails outright. Always re-verify the full base64 after publishing.

1024-bit downgrade. Some receivers flag or distrust 1024-bit keys. If reports show weak-key warnings, rotate to a fresh 2048-bit selector using the roll-forward procedure above.

Frequently Asked Questions

Do I need DKIM if I already have SPF? Yes. SPF authorizes sending IPs but breaks on forwarding, and on its own does not satisfy DMARC alignment for many flows. DKIM survives forwarding and provides the second aligned identifier, which is why both are configured together.

How long should I keep an old selector after rotation? Keep the retired selector._domainkey record published for at least your longest mail retry/queue window — commonly 24 to 72 hours — so messages signed just before the switch still verify.

Why is my DKIM passing but DMARC still failing? DMARC requires the DKIM d= domain to align with the From: header domain. A signature that passes on a provider’s own domain (for example amazonses.com) does not align. Publish a record under your own domain so the d= value matches.

Can one domain have multiple DKIM keys at once? Yes — that is the entire purpose of selectors. Each ESP and each rotation generation gets its own selector._domainkey record, and the signer names the selector it used in the s= tag.

Should I use RSA or Ed25519? RSA-2048 has universal receiver support and is the safe default. Ed25519 keys are shorter and faster but not yet universally validated, so publish an Ed25519 key alongside an RSA key rather than replacing it.

Back to Advanced SRV & MX Routing