DKIM (DomainKeys Identified Mail) lets your mail server sign every outgoing message with a private key. Receiving servers fetch the matching public key from your DNS and check the signature, which proves the message really comes from your domain and was not modified in transit. Gmail, Outlook and Yahoo all expect DKIM, and it is required for a DMARC policy to work. In this tutorial you will install OpenDKIM on Ubuntu 24.04, connect it to Postfix, publish the public key in DNS and confirm that your messages pass the check.

Prerequisites

To follow this tutorial, you need:

  • A server running Ubuntu 24.04 LTS with a working Postfix installation that can send mail, for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • A domain you control, with access to its DNS zone. This guide uses your_domain as the sending domain; replace it everywhere with your own.
  • An external mailbox you can read (Gmail or Outlook work well) to check the result.

Step 1 - Installing OpenDKIM

OpenDKIM runs as a milter: a separate daemon that Postfix hands each message to before sending it. Install the daemon and its tools:

sudo apt update
sudo apt install opendkim opendkim-tools

The opendkim package provides the service and /etc/opendkim.conf. The opendkim-tools package provides opendkim-genkey and opendkim-testkey, which you will use in the next steps. Check that the service was installed:

systemctl status opendkim --no-pager
● opendkim.service - OpenDKIM Milter
     Loaded: loaded (/usr/lib/systemd/system/opendkim.service; enabled; preset: enabled)
     Active: active (running)

Step 2 - Generating the DKIM key pair

A DKIM key is identified by a selector, a short label that becomes part of the DNS name where the public key lives (selector._domainkey.your_domain). Using a date as the selector, such as 202609, makes key rotation easy later: you publish a new selector, switch to it, and remove the old one.

Ubuntu provides /etc/dkimkeys, a directory owned by the opendkim user with mode 700, meant for private keys. Generate a 2048-bit key there:

sudo opendkim-genkey -b 2048 -d your_domain -s 202609 -D /etc/dkimkeys

This creates two files:

  • /etc/dkimkeys/202609.private: the private key OpenDKIM signs with.
  • /etc/dkimkeys/202609.txt: the public key, already formatted as a DNS record.

The private key must be readable only by the opendkim user, or the service refuses to use it:

sudo chown opendkim:opendkim /etc/dkimkeys/202609.private
sudo chmod 600 /etc/dkimkeys/202609.private
sudo ls -l /etc/dkimkeys
-rw------- 1 opendkim opendkim 1704 Sep 25 10:12 202609.private
-rw------- 1 root     root      502 Sep 25 10:12 202609.txt

Step 3 - Configuring OpenDKIM

Open the main configuration file:

sudo nano /etc/opendkim.conf

The Ubuntu default file already sets sensible values (Canonicalization relaxed/simple, OversignHeaders From, UserID opendkim). You need to tell it which domain to sign for, which key to use, and where to listen for Postfix. Find the commented Domain, Selector and KeyFile lines and the Socket line, and change them so they read:

Domain                  your_domain
Selector                202609
KeyFile                 /etc/dkimkeys/202609.private

Socket                  inet:8891@localhost

Make sure only one Socket line is uncommented. A TCP socket on localhost is the simplest option on Ubuntu, because Postfix runs its SMTP server in a chroot under /var/spool/postfix and cannot reach the default Unix socket in /run/opendkim.

When no Mode is set, OpenDKIM both signs and verifies. It signs messages submitted from 127.0.0.1 (the default InternalHosts) and messages from clients that authenticated with SMTP AUTH, which covers mail sent by webmail on the same server and by users sending through port 587. Incoming mail from other servers is only verified.

Restart OpenDKIM and check that it listens on port 8891:

sudo systemctl restart opendkim
sudo ss -ltnp | grep 8891
LISTEN 0      4096       127.0.0.1:8891       0.0.0.0:*    users:(("opendkim",pid=4121,fd=3))

If the service does not start, read the reason in the journal with sudo journalctl -u opendkim -n 30. A wrong key path or wrong permissions on the private key are the usual causes.

Step 4 - Connecting Postfix to OpenDKIM

Postfix must pass every message to the milter. Open its main configuration:

sudo nano /etc/postfix/main.cf

Add these lines at the end of the file:

# DKIM signing with OpenDKIM
milter_default_action = accept
smtpd_milters = inet:localhost:8891
non_smtpd_milters = $smtpd_milters
  • smtpd_milters applies to mail that arrives over SMTP, including mail from authenticated users.
  • non_smtpd_milters applies to mail submitted locally with the sendmail command, for example by PHP or cron.
  • milter_default_action = accept keeps mail flowing if OpenDKIM is down. The messages go out unsigned instead of being rejected.

If smtpd_milters is already set (for example by a spam filter), add OpenDKIM to the existing line separated by a comma instead of creating a second one.

Check the configuration and reload Postfix:

sudo postfix check
sudo systemctl reload postfix

postfix check prints nothing when the configuration is valid.

Step 5 - Publishing the public key in DNS

Display the DNS record that opendkim-genkey prepared:

sudo cat /etc/dkimkeys/202609.txt
202609._domainkey	IN	TXT	( "v=DKIM1; h=sha256; k=rsa; "
	  "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv3n..."
	  "...Q2hwIDAQAB" )  ; ----- DKIM key 202609 for your_domain

In your DNS provider, create a TXT record with these values:

FieldValue
TypeTXT
Name / Host202609._domainkey (some panels need the full name 202609._domainkey.your_domain)
Valuev=DKIM1; h=sha256; k=rsa; p=MIIBIjANBgkq...IDAQAB

The file splits the key into several quoted strings because a single DNS string is limited to 255 characters. Most DNS panels expect one continuous value: join the quoted parts, without the quotes, parentheses or the comment. If you manage a BIND zone file yourself, you can paste the record as it is.

Wait a few minutes, then query the record:

dig +short TXT 202609._domainkey.your_domain
"v=DKIM1; h=sha256; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv3n..." "...Q2hwIDAQAB"

The answer may come back split into several strings. That is normal; receivers join them.

Now let OpenDKIM compare the published key with your private key:

sudo opendkim-testkey -d your_domain -s 202609 -k /etc/dkimkeys/202609.private -vvv
opendkim-testkey: using default configfile /etc/opendkim.conf
opendkim-testkey: key loaded from /etc/dkimkeys/202609.private
opendkim-testkey: checking key '202609._domainkey.your_domain'
opendkim-testkey: key not secure
opendkim-testkey: key OK

key OK means the DNS record matches. key not secure only means your zone is not signed with DNSSEC and does not affect DKIM.

Step 6 - Testing a signed message

Send a message from the server to an external mailbox you can read. Replace the addresses with your own:

printf 'From: admin@your_domain\nTo: [email protected]\nSubject: DKIM test\n\nThis message should be signed.\n' | sudo sendmail -f admin@your_domain [email protected]

Check the mail log for the signing line:

sudo journalctl -u opendkim -n 20 --no-pager | grep "DKIM-Signature"
Sep 25 10:31:07 mail opendkim[4121]: 4cKx2N1QzXz9sWb: DKIM-Signature field added (s=202609, d=your_domain)

Open the message in the receiving mailbox and view the original source (in Gmail: the three-dot menu, then Show original). The authentication results should include:

dkim=pass header.i=@your_domain header.s=202609

Signing mail for several domains

The Domain, Selector and KeyFile directives cover one domain. To sign for several domains, generate a key per domain and use lookup tables instead. Generate a key for the second domain in its own directory:

sudo mkdir -p /etc/dkimkeys/example.net
sudo opendkim-genkey -b 2048 -d example.net -s 202609 -D /etc/dkimkeys/example.net
sudo chown -R opendkim:opendkim /etc/dkimkeys/example.net
sudo chmod 600 /etc/dkimkeys/example.net/202609.private

Create the key table, which maps a key name to a domain, selector and private key file:

sudo nano /etc/opendkim/key.table
202609._domainkey.your_domain  your_domain:202609:/etc/dkimkeys/202609.private
202609._domainkey.example.net  example.net:202609:/etc/dkimkeys/example.net/202609.private

Create the signing table, which maps sender addresses to a key:

sudo nano /etc/opendkim/signing.table
*@your_domain  202609._domainkey.your_domain
*@example.net  202609._domainkey.example.net

The /etc/opendkim directory does not exist by default; create it with sudo mkdir /etc/opendkim before editing the files. Then, in /etc/opendkim.conf, comment out the Domain, Selector and KeyFile lines and add:

KeyTable                /etc/opendkim/key.table
SigningTable            refile:/etc/opendkim/signing.table

The refile: prefix lets the signing table use wildcards like *@example.net. Restart OpenDKIM, publish the TXT record for each domain, and test each one with opendkim-testkey.

Troubleshooting

  • No DKIM-Signature field added line in the log. Postfix is not passing the message to OpenDKIM, or OpenDKIM treats the sender as external. Check that postconf smtpd_milters non_smtpd_milters shows inet:localhost:8891, and that the From domain matches the Domain setting exactly.
  • dkim=fail (bad signature) at the receiver. The message was changed after signing, often by a mailing list or a forwarding service, or the DNS record contains a typo. Run opendkim-testkey again and compare the published p= value with /etc/dkimkeys/202609.txt.
  • opendkim-testkey: record not found. The record is missing, still propagating, or was created as 202609._domainkey.your_domain.your_domain because the panel appended the domain twice. Query it with dig and fix the name.
  • OpenDKIM fails to start with can't load key. The private key is not owned by opendkim or is readable by others. Reapply the chown and chmod 600 from Step 2.

Conclusion

Your server now signs outgoing mail with a 2048-bit DKIM key, the public key is published in DNS, and receivers report dkim=pass. DKIM on its own does not tell receivers what to do with unsigned mail, so the next step is to publish SPF and DMARC records, as described in How to Configure SPF and DMARC Records. Plan to rotate the key once or twice a year by generating a new selector, publishing it, switching Selector and KeyFile, and removing the old record a week later.