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.

OpenDKIM
most common Postfix DKIM milter — open-source, well-documented
2048-bit
minimum RSA key — configure in opendkim.conf
milter
mail filter interface — Postfix connects milters via Unix socket or TCP
/etc/opendkim/keys/
default key storage path — protect with chmod 600

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:

OpenDKIM + Postfix — complete multi-domain configuration
# /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:

  1. Is the DNS record published and fully propagated? (use dig TXT mail._domainkey.example.com)
  2. Is the signing key in KeyTable matching the selector in the DNS record name?
  3. Are private key file permissions correct (readable by opendkim user)?
  4. 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