To set up DKIM for multiple domains, give every domain you send From its own key: generate DKIM separately for each domain in each service that sends as it, and publish that service's records under selector._domainkey.yourdomain in that domain's DNS. The signature's d= must match the From domain (or its parent) for DMARC to count it, so one domain's key can't vouch for another product's mail. Selectors only have to be unique within a domain, so reusing names like google or selector1 across domains is fine.
How DKIM finds the right key for each domain
Every DKIM signature carries two tags that matter here: d= (the signing domain) and s= (the selector). A receiver fetches the public key from a fixed location built from those two values. RFC 6376 section 3.6.2.1 gives the example: a signature with d=example.com and s=foo.bar is verified with the key at foo.bar._domainkey.example.com.
Three things follow from that, and they answer most multi-domain questions:
- Keys are namespaced by domain.
google._domainkey.product-a.comandgoogle._domainkey.product-b.comare different DNS names. The same selector on two domains never clashes. - One domain can hold many keys. Each service picks its own selector, so Google Workspace, Amazon SES, and a newsletter tool can all sign for the same domain at once.
- The key proves the
d=domain, nothing more. A valid signature fromd=product-a.comsays nothing about mail whose From address is onproduct-b.com.
Why each From domain needs its own signature
DKIM on its own will happily pass a message signed by any domain. DMARC is what ties the signature to the address people see. Under RFC 7489 section 3.1.1, relaxed alignment (the default) requires the d= domain and the From domain to share the same organizational domain. Microsoft puts it plainly: a message needs to be DKIM signed by the domain in the From address.
| From address | Signature d= | DKIM | DMARC alignment |
|---|---|---|---|
hi@product-a.com | product-a.com | Pass | Aligned |
hi@news.product-a.com | product-a.com | Pass | Aligned (same organizational domain) |
hi@product-b.com | product-a.com | Pass | Not aligned |
hi@product-b.com | your provider's domain | Pass | Not aligned |
The last two rows are the multi-domain trap. Everything looks fine in a DKIM checker, but DMARC fails unless SPF happens to align instead. Gmail requires the From domain to align with either the SPF or the DKIM domain for direct mail (Google sender guidelines), so signing every product with one "main" domain doesn't work.
Subdomains are the exception that saves work. Because of relaxed alignment, one key on example.com covers app.example.com and news.example.com. Amazon SES builds this in: when you verify a domain with DKIM, DKIM authentication also applies to all subdomains of that domain. Microsoft 365 is stricter and says each subdomain you send from needs its own DKIM configuration.
The records each provider needs per domain
The number of records depends on the provider, not on how many domains you have. Repeat the provider's setup once per domain.
| Provider | Records per domain | Host | Value |
|---|---|---|---|
| Amazon SES Easy DKIM | 3 CNAMEs | <token>._domainkey | <token>.dkim.amazonses.com |
| Microsoft 365 | 2 CNAMEs | selector1._domainkey, selector2._domainkey | Shown in the Defender portal for that domain |
| Google Workspace | 1 TXT | google._domainkey | The key generated for that domain |
| Self-hosted OpenDKIM | 1 TXT per selector | <selector>._domainkey | The .txt file opendkim-genkey writes |
Amazon SES
Each domain is a separate identity, and each identity gets its own three tokens. In the console, go to Configuration > Identities > Create identity, choose Domain, and keep Easy DKIM with the default RSA_2048_BIT key. SES then shows three CNAMEs in the form <token>._domainkey.yourdomain pointing to <token>.dkim.amazonses.com. The same thing through the AWS SDK for JavaScript v3, looped over several domains:
import { SESv2Client, CreateEmailIdentityCommand } from "@aws-sdk/client-sesv2";
const ses = new SESv2Client({ region: "us-east-1" });
const domains = ["product-a.com", "product-b.com", "product-c.io"];
for (const domain of domains) {
const { DkimAttributes } = await ses.send(
new CreateEmailIdentityCommand({
EmailIdentity: domain,
DkimSigningAttributes: { NextSigningKeyLength: "RSA_2048_BIT" },
})
);
for (const token of DkimAttributes?.Tokens ?? []) {
console.log(`${domain} CNAME ${token}._domainkey ${token}.dkim.amazonses.com`);
}
}
Two SES details to plan for. DKIM setup is per Region: if you send from more than one Region, you complete DKIM in each of them (or use Deterministic Easy DKIM to replicate an identity's signing setup). And verification is not instant: SES says DNS detection can take up to 72 hours.
Microsoft 365
Add each custom domain to Microsoft 365 first, then open the DKIM tab at Email authentication settings in the Defender portal. Every custom domain needs its own two CNAMEs at selector1._domainkey and selector2._domainkey. The hostnames are the same in every tenant, but the targets embed the domain with dashes (contoso.com becomes contoso-com), so copy the values for each domain from the portal or from Get-DkimSigningConfig. Once the CNAMEs resolve, switch the domain's toggle to Enabled.
Google Workspace
In the Admin console, go to Menu > Apps > Google Workspace > Gmail > Authenticate email, pick the domain from the list, and choose Generate New Record. Google's documentation says you must get a unique DKIM key for each domain, so repeat this for every domain in the dropdown. The default selector is google, which is fine to reuse on every domain for the reason above.
Several domains on one Postfix server (OpenDKIM)
The quick fix you'll find in forum threads is listing every domain on OpenDKIM's Domain line. That signs all of them with one shared key. A key per domain takes two lookup tables instead. In /etc/opendkim.conf:
KeyTable /etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
Generate a 2048-bit key per domain (opendkim-genkey defaults to 1024 bits):
opendkim-genkey -b 2048 -d product-a.com -D /etc/opendkim/keys/product-a.com -s mail
opendkim-genkey -b 2048 -d product-b.com -D /etc/opendkim/keys/product-b.com -s mail
The KeyTable maps a key name to the d= domain, the selector, and the private key file:
mail._domainkey.product-a.com product-a.com:mail:/etc/opendkim/keys/product-a.com/mail.private
mail._domainkey.product-b.com product-b.com:mail:/etc/opendkim/keys/product-b.com/mail.private
The SigningTable picks a key from the From address. With refile:, the left side is a wildcard pattern and the first match wins:
*@product-a.com mail._domainkey.product-a.com
*@product-b.com mail._domainkey.product-b.com
Publish each domain's mail.txt as a TXT record at mail._domainkey in that domain's zone. Avoid a bare * pattern: it signs mail for any domain, including ones with no published key.
Worked example: three products on one Google Workspace account
Say you run acme.dev (company mail and product email), acmeforms.com, and acmeinvoice.io. All three have mailboxes in one Google Workspace account and send app email through Amazon SES. That is two senders on each of three domains:
| Domain | Google Workspace | Amazon SES |
|---|---|---|
acme.dev | TXT google._domainkey | 3 CNAMEs <token>._domainkey |
acmeforms.com | TXT google._domainkey (its own key) | 3 different CNAMEs |
acmeinvoice.io | TXT google._domainkey (its own key) | 3 different CNAMEs |
Twelve DKIM records in total, and no two of them share a DNS name. Google's three google._domainkey records hold three different keys. SES's nine tokens are unique per identity. If acmeforms.com later adds a newsletter tool, that tool generates a key and selector for acmeforms.com only, and the other two domains stay untouched.
Check every domain's keys with one script
Once records are live, check them all from one list. This script uses only Node built-ins (Node 18 or newer). resolveTxt follows CNAMEs, so SES and Microsoft 365 selectors work too. It reports whether each selector has a usable key and the RSA key length.
// dkim-check.mjs: node dkim-check.mjs
import { resolveTxt } from "node:dns/promises";
import { createPublicKey } from "node:crypto";
// Every domain you put in a From address, and the selectors its senders use.
const domains = {
"acme.dev": ["google", "abcdefexampletoken1"],
"acmeforms.com": ["google", "abcdefexampletoken2"],
};
function parseTags(record) {
return Object.fromEntries(
record
.split(";")
.map((tag) => tag.trim().split("="))
.filter(([name]) => name)
.map(([name, ...value]) => [name.trim(), value.join("=").replace(/\s+/g, "")])
);
}
async function checkSelector(domain, selector) {
const name = `${selector}._domainkey.${domain}`;
try {
const record = (await resolveTxt(name)).map((chunks) => chunks.join("")).join("");
const tags = parseTags(record);
if (!tags.p) return `${name}: no usable key (p= is empty or missing)`;
if (tags.k === "ed25519") return `${name}: ok, ed25519`;
const key = createPublicKey({ key: Buffer.from(tags.p, "base64"), format: "der", type: "spki" });
return `${name}: ok, ${key.asymmetricKeyDetails.modulusLength}-bit RSA`;
} catch (err) {
return `${name}: ${err.code ?? err.message}`;
}
}
for (const [domain, selectors] of Object.entries(domains)) {
for (const selector of selectors) console.log(await checkSelector(domain, selector));
}
Replace the example tokens with the ones your providers gave you. An empty p= means the key was revoked on purpose, which is normal for an old selector after rotation. ENOTFOUND means the record isn't published, or is published under the wrong name.
If you don't know a domain's selectors, the free email deliverability checker tries common ones (google, selector1, selector2, default, and others) along with SPF, DMARC, MX, and blacklists. For the definitive answer, send a message and read the s= and d= tags in its DKIM-Signature header.
Troubleshooting DKIM across domains
A Mailmark domain shows DKIM pending. Each of the three SES CNAMEs is checked separately on the domain page, so the one marked missing is the one to fix. The domain setup docs cover the full record set.
DKIM passes, DMARC fails. Look at header.d= in the Authentication-Results header (Gmail: Show original). If it shows your provider's domain or a different product's domain, that domain's DKIM isn't set up in the service that sent it. Set it up for the From domain specifically. How SPF, DKIM, and DMARC work together explains alignment in more detail.
The record was added to the wrong zone. Most DNS dashboards append the zone name automatically, so entering google._domainkey.acmeforms.com inside the acmeforms.com zone creates google._domainkey.acmeforms.com.acmeforms.com. Enter only google._domainkey, then confirm with the script.
A long key was rejected or truncated. A 2048-bit key doesn't fit in one 255-character TXT string. RFC 6376 lets the record be split into several strings that receivers join back together (section 3.6.2.2), and most DNS hosts split it for you. If yours doesn't, paste the value as quoted chunks, or use 1024 bits there. SES Easy DKIM avoids this because the key lives behind a CNAME.
Some mail from a domain is signed and some isn't. A second service is sending as that domain without its own DKIM. Every tool that sends From the domain needs its own key and selector on it. This often turns up right after adding a support desk or billing tool.
A new SES Region sends unsigned mail. DKIM is per Region in SES. Set it up again in the new Region, or replicate the identity with Deterministic Easy DKIM.
If DKIM aligns everywhere and mail still lands in spam, the cause is usually reputation or content: see why emails go to spam. For the rest of the records each sending domain needs, see DNS records for email.
Checklist for each new domain
- Add the domain to every service that will send From it, not just the first one.
- Use 2048-bit keys where the provider and DNS host allow it.
- Publish each provider's records in that domain's own zone, hostname only.
- Enable signing in the provider after the records resolve.
- Add a DMARC record to the domain (
p=noneto start) so you get reports on what signs as it. - Send a test from the domain and confirm
header.d=matches the From domain or its parent. - Add the domain and its selectors to your check script.
Frequently asked questions
Can I use the same DKIM key for multiple domains?
Technically yes: publish the same public key under a selector on each domain and sign each domain's mail with d= set to that domain. It works, but one leaked private key then lets someone sign as every domain at once. Google Workspace, Microsoft 365, and Amazon SES all generate a separate key per domain, and self-hosted setups should too.
Can I use the same DKIM selector name on different domains?
Yes. The public key is looked up at selector._domainkey.domain, so google._domainkey.a.com and google._domainkey.b.com are unrelated records. Selectors only need to be unique within one domain.
Do subdomains need their own DKIM keys?
Not for DMARC. With relaxed alignment, the default, a signature with d=example.com aligns with a From address on news.example.com because both share the organizational domain. Amazon SES applies a verified domain's DKIM to all of its subdomains automatically. Microsoft 365 asks for a separate DKIM setup for each subdomain you send from.
Why does DMARC fail when DKIM passes?
DKIM passes for whatever domain is in the signature's d= tag. DMARC only counts it when that domain shares an organizational domain with the From address. Mail from you@product-b.com signed with d=product-a.com, or with your email provider's own domain, passes DKIM and fails DMARC alignment.
How many DKIM records does each domain need?
It depends on the provider, not the number of domains: 3 CNAMEs per domain for Amazon SES Easy DKIM, 2 CNAMEs per domain for Microsoft 365, and 1 TXT record per domain for Google Workspace. Each sending service on a domain adds its own selector.