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
sudoprivileges. - A domain you control, with access to its DNS zone. This guide uses
your_domainas 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
Note2048 bits is the recommended size. 1024-bit keys are considered weak, and 4096-bit keys produce DNS records too long for many DNS providers.
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_miltersapplies to mail that arrives over SMTP, including mail from authenticated users.non_smtpd_miltersapplies to mail submitted locally with thesendmailcommand, for example by PHP or cron.milter_default_action = acceptkeeps 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:
| Field | Value |
|---|---|
| Type | TXT |
| Name / Host | 202609._domainkey (some panels need the full name 202609._domainkey.your_domain) |
| Value | v=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 addedline in the log. Postfix is not passing the message to OpenDKIM, or OpenDKIM treats the sender as external. Check thatpostconf smtpd_milters non_smtpd_miltersshowsinet:localhost:8891, and that theFromdomain matches theDomainsetting 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. Runopendkim-testkeyagain and compare the publishedp=value with/etc/dkimkeys/202609.txt.opendkim-testkey: record not found. The record is missing, still propagating, or was created as202609._domainkey.your_domain.your_domainbecause the panel appended the domain twice. Query it withdigand fix the name.- OpenDKIM fails to start with
can't load key. The private key is not owned byopendkimor is readable by others. Reapply thechownandchmod 600from 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.
