Configuring DKIM signing on a Postfix server for a single domain is well-documented. Extending that configuration to sign outbound mail for multiple separate domains — each with its own private key, its own selector, and its own DNS record — requires a different approach using OpenDKIM's key table and signing table architecture. This guide covers the complete multi-domain OpenDKIM setup, the common milter permission issues that cause silent DKIM signing failures, key rotation procedures, and the verification steps that confirm each domain is signing correctly.
Architecture: KeyTable and SigningTable
Single-domain OpenDKIM configurations often use the simple KeyFile and Domain directives in opendkim.conf. This approach doesn't scale to multiple domains because it maps all outbound mail to one key. The correct multi-domain architecture uses two lookup files:
- KeyTable (
/etc/opendkim/KeyTable): Maps a named key entry to a domain, selector, and private key file path - SigningTable (
/etc/opendkim/SigningTable): Maps each sending domain (or email pattern) to a key entry from the KeyTable
When Postfix submits a message to the OpenDKIM milter, OpenDKIM reads the From: header domain, looks up that domain in the SigningTable, finds the corresponding key entry, and applies the correct private key for signing. Each domain gets its own key, its own selector, and generates its own DKIM signature.
Installation
# Debian/Ubuntu
sudo apt update
sudo apt install opendkim opendkim-tools -y
# RHEL/CentOS/Rocky Linux
sudo dnf install opendkim opendkim-tools -y
# Verify installation
opendkim --version
Directory Structure
Create a consistent directory layout before generating keys:
# /etc/opendkim.conf Mode sv Syslog yes SyslogSuccess yes LogWhy yes Canonicalization relaxed/relaxed ExternalIgnoreList refile:/etc/opendkim/TrustedHosts InternalHosts refile:/etc/opendkim/TrustedHosts KeyTable refile:/etc/opendkim/KeyTable SigningTable refile:/etc/opendkim/SigningTable Mode sv PidFile /var/run/opendkim/opendkim.pid SignatureAlgorithm rsa-sha256 Socket local:/run/opendkim/opendkim.sock # /etc/opendkim/KeyTable (one line per domain) default._domainkey.domain1.com domain1.com:default:/etc/opendkim/keys/domain1.com/default.private default._domainkey.domain2.com domain2.com:default:/etc/opendkim/keys/domain2.com/default.private # /etc/opendkim/SigningTable *@domain1.com default._domainkey.domain1.com *@domain2.com default._domainkey.domain2.com # /etc/postfix/main.cf milter_protocol = 6 milter_default_action = accept smtpd_milters = local:/run/opendkim/opendkim.sock non_smtpd_milters = local:/run/opendkim/opendkim.sock
sudo mkdir -p /etc/opendkim/keys
sudo chown -R opendkim:opendkim /etc/opendkim
sudo chmod 700 /etc/opendkim/keys
For each domain, keys will live in a per-domain subdirectory:
/etc/opendkim/keys/
example.com/
mail.private # Private signing key (kept secret)
mail.txt # Public key for DNS (share this)
anotherdomain.org/
mail.private
mail.txt
Generating 2048-bit Keys Per Domain
Generate a separate 2048-bit key for each domain. The selector name (mail in this example) appears in the DKIM-Signature header and in the DNS record name. Use date-based selectors for easier key rotation management (2026q2, 20260401):
# Create per-domain directory and generate key
sudo mkdir -p /etc/opendkim/keys/example.com
sudo opendkim-genkey \
--directory=/etc/opendkim/keys/example.com \
--domain=example.com \
--selector=mail \
--bits=2048
# Repeat for each additional domain
sudo mkdir -p /etc/opendkim/keys/anotherdomain.org
sudo opendkim-genkey \
--directory=/etc/opendkim/keys/anotherdomain.org \
--domain=anotherdomain.org \
--selector=mail \
--bits=2048
# Set correct ownership and permissions on all keys
sudo chown -R opendkim:opendkim /etc/opendkim/keys/
sudo chmod -R 600 /etc/opendkim/keys/*/*.private
2048-bit is mandatory. Gmail's 2025 bulk sender guidance and Microsoft's requirements both specify 2048-bit minimum key length. OpenDKIM's default may be 1024-bit on older installations — always explicitly pass --bits=2048.
Configuring opendkim.conf
Replace the default OpenDKIM configuration at /etc/opendkim.conf:
# /etc/opendkim.conf — Multi-domain signing configuration
# Logging
Syslog yes
SyslogSuccess yes
LogWhy yes
# Mode: s = sign, v = verify, sv = sign and verify
Mode sv
# Canonicalization
# relaxed/simple is the standard recommendation
# relaxed allows minor header whitespace changes (safe for forwarding)
Canonicalization relaxed/simple
# Socket — use TCP socket for Postfix compatibility
Socket inet:8891@127.0.0.1
# Signing algorithm
SignatureAlgorithm rsa-sha256
# User/Group for the OpenDKIM process
UserID opendkim
UMask 002
# Multi-domain lookup files
KeyTable refile:/etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
# Trusted hosts (can inject unsigned mail)
ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
InternalHosts refile:/etc/opendkim/TrustedHosts
# PID file
PidFile /var/run/opendkim/opendkim.pid
KeyTable and SigningTable Files
KeyTable format
Each line maps a key name (arbitrary identifier) to domain:selector:private_key_path:
# /etc/opendkim/KeyTable
# Format: key-name domain:selector:private-key-path
mail._domainkey.example.com example.com:mail:/etc/opendkim/keys/example.com/mail.private
mail._domainkey.anotherdomain.org anotherdomain.org:mail:/etc/opendkim/keys/anotherdomain.org/mail.private
SigningTable format
Maps email patterns or domains to key table entries. The refile: prefix in opendkim.conf enables regex matching:
# /etc/opendkim/SigningTable
# Format: pattern key-name
*@example.com mail._domainkey.example.com
*@anotherdomain.org mail._domainkey.anotherdomain.org
# Subdomains — if you send from subdomain addresses
*@mail.example.com mail._domainkey.example.com
TrustedHosts file
Lists hosts that can inject mail without requiring incoming DKIM verification:
# /etc/opendkim/TrustedHosts
127.0.0.1
::1
localhost
your.server.hostname.com
Set correct permissions:
sudo chown opendkim:opendkim /etc/opendkim/KeyTable \
/etc/opendkim/SigningTable /etc/opendkim/TrustedHosts
sudo chmod 644 /etc/opendkim/KeyTable \
/etc/opendkim/SigningTable /etc/opendkim/TrustedHosts
Postfix Milter Integration
Add the milter configuration to /etc/postfix/main.cf:
# /etc/postfix/main.cf — add to existing configuration
# OpenDKIM milter integration
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:127.0.0.1:8891
non_smtpd_milters = $smtpd_milters
The milter_default_action = accept setting is critical — it means that if OpenDKIM is unavailable or crashes, Postfix continues delivering email without DKIM signing rather than rejecting all outbound mail. In production, monitor OpenDKIM process health separately.
Restart both services:
sudo systemctl enable opendkim
sudo systemctl restart opendkim
sudo systemctl restart postfix
# Verify OpenDKIM is listening
ss -tlnp | grep 8891
The Permissions Problem: Most Common Failure
The most frequent cause of DKIM signing failures on multi-domain setups is a permissions mismatch between the OpenDKIM process user and the Postfix process group. OpenDKIM runs as the opendkim user; Postfix processes run under the postfix user. The milter socket communication works, but if the private key files aren't readable by the OpenDKIM process, signing silently fails — mail is delivered without DKIM signatures and no error appears in the mail log.
# Add postfix user to opendkim group (allows socket access)
sudo usermod -a -G opendkim postfix
# Verify key file permissions — .private files must be readable by opendkim only
sudo ls -la /etc/opendkim/keys/example.com/
# Expected: -rw------- 1 opendkim opendkim mail.private
# -rw-r--r-- 1 opendkim opendkim mail.txt
# Fix if wrong:
sudo chown -R opendkim:opendkim /etc/opendkim/keys/
sudo chmod 600 /etc/opendkim/keys/*/*.private
sudo chmod 644 /etc/opendkim/keys/*/*.txt
# Restart after group change
sudo systemctl restart postfix opendkim
Publishing DKIM DNS Records
Each domain needs a DNS TXT record. The public key is in the mail.txt file generated by opendkim-genkey:
# View the DNS record content
cat /etc/opendkim/keys/example.com/mail.txt
The output looks like:
mail._domainkey IN TXT ( "v=DKIM1; k=rsa; "
"p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ..."
"...rest of key data..." ) ; ----- DKIM key mail for example.com
At your DNS provider, create a TXT record:
- Host/Name:
mail._domainkey(some providers require the full name:mail._domainkey.example.com) - Type: TXT
- Value:
v=DKIM1; k=rsa; p=YOURPUBLICKEY(the concatenated value without the parentheses and line breaks)
Verify DNS propagation:
dig TXT mail._domainkey.example.com +short
Verification and Testing
Test key configuration before sending
# Test that OpenDKIM can find and use each key
sudo -u opendkim opendkim-testkey \
-d example.com \
-s mail \
-k /etc/opendkim/keys/example.com/mail.private \
-v
A successful result shows: opendkim-testkey: using default configfile /etc/opendkim.conf followed by opendkim-testkey: key OK.
Send a test email and verify headers
Send email from each configured domain to a Gmail address you control. View the full message source (Gmail: three-dot menu → Show Original). Look for:
Authentication-Results: mx.google.com;
dkim=pass header.i=@example.com header.s=mail header.b=AbCdEf;
If you see dkim=fail, check:
- Is the DNS record published and fully propagated? (use
dig TXT mail._domainkey.example.com) - Is the signing key in KeyTable matching the selector in the DNS record name?
- Are private key file permissions correct (readable by opendkim user)?
- Is the domain in the SigningTable matching the From: header exactly?
Check mail logs for DKIM activity
# Real-time log monitoring
sudo tail -f /var/log/mail.log | grep -i dkim
# Expected successful signing log entries:
# opendkim[PID]: MSGID: DKIM-Signature field added (s=mail, d=example.com)
Key Rotation for Multiple Domains
DKIM keys should be rotated every 90–180 days. The zero-downtime rotation process for multi-domain Postfix/OpenDKIM:
# Step 1: Generate new key with new selector name (e.g., date-based)
SELECTOR="mail2026q3"
DOMAIN="example.com"
sudo mkdir -p /etc/opendkim/keys/${DOMAIN}/${SELECTOR}
sudo opendkim-genkey \
--directory=/etc/opendkim/keys/${DOMAIN}/${SELECTOR} \
--domain=${DOMAIN} \
--selector=${SELECTOR} \
--bits=2048
# Step 2: Publish new DNS record
# Add TXT record for ${SELECTOR}._domainkey.example.com
# Wait 24-48 hours for DNS propagation
# Step 3: Add new key to KeyTable
echo "${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:/etc/opendkim/keys/${DOMAIN}/${SELECTOR}/${SELECTOR}.private" \
| sudo tee -a /etc/opendkim/KeyTable
# Step 4: Update SigningTable to use new key
sudo sed -i "s/mail\._domainkey\.${DOMAIN}/${SELECTOR}._domainkey.${DOMAIN}/" /etc/opendkim/SigningTable
# Step 5: Reload OpenDKIM
sudo systemctl reload opendkim
# Step 6: Verify new selector is signing (send test email, check headers)
# Step 7: After 30-day drain period, remove old DNS record
# Keep old private key file for any delayed message re-verification

