Part 4: Set Up SPF & DKIM with Postfix on Debian Server

After completing part 1 and part 2, we have a working Postfix SMTP server and Dovecot IMAP server. We can send and receive emails using a desktop email client. In this tutorial, we are going to learn how to improve email delivery to the recipient’s inbox by setting up SPF and DKIM on Debian server.

What are SPF and DKIM Records?

SPF and DKIM are two types of TXT records in DNS that allow you to detect email spoofing and help legitimate emails deliver into the recipient’s inbox instead of the spam folder. If your domain is abused by email spoofing, then your emails are likely to land in the recipient’s spam folder if they didn’t add you to the address book.

SPF (Sender Policy Framework) record specifies which hosts or IP addresses are allowed to send emails on behalf of a domain. You should allow only your own email server or your ISP’s server to send emails for your domain.

DKIM (DomainKeys Identified Mail) uses a private key to add a signature to emails sent from your domain. Receiving SMTP servers verify the signature by using the corresponding public key, which is published in your domain’s DNS zone.

Step 1: Create an SPF Record in DNS

In your DNS management interface, create a new TXT record like below.

Record Type   Name    Value
TXT           @       v=spf1 mx ~all

create spf record in DNS

Where:

  • TXT indicates this is a TXT record.
  • Enter @ in the name field to represent the apex domain (example.com).
  • v=spf1 indicates this is an SPF record and the SPF record version is SPF1.
  • mx means all hosts listed in the MX records are allowed to send emails for your domain and all other hosts are disallowed.
  • ~all indicates that emails from your domain should only come from hosts specified in the SPF record. Emails that are from other hosts will be flagged as untrustworthy. Possible alternatives are +all, -all, ?all, but they are rarely used.

-all means that emails sent from not-allowed hosts should be rejected, never to land in the recipient’s inbox or spam folder. I have seen it used by facebook.com, but we generally don’t need such a strict policy.

Some folks might think that -all will be better as it will reject emails from untrusted hosts. Well, using -all in your SPF policy can cause your own emails to be rejected when the recipient has two SMTP servers and when the main SMTP server goes offline, your emails will be temporarily stored on the backup SMTP server. When the main SMTP server comes back online, the email will be relayed from the backup SMTP server to the main SMTP server. Since you didn’t list the recipient’s backup SMTP server in your SPF record, the email will be rejected by the recipient’s main SMTP server. So you should use ~all in your SPF policy.

You can also list a specific IP address in your SPF record.

TXT    @     v=spf1 mx ip4:12.34.56.78 ~all

Note that some DNS managers require you to wrap the SPF record with quotes like below.

TXT    @    "v=spf1 mx ~all"

To check if your SPF record is propagated to the public Internet, you can use the dig utility on your Linux box like below. (On Debian, you need to install the bind9-dnsutils package in order to use dig command: sudo apt install bind9-dnsutils)

dig example.com txt

The txt option tells dig that we only want to query TXT records.

use dig utility to query spf record

You can also use the dmarcian SPF surveyor to test your SPF record syntax.

Step 2: Configure SPF Policy Agent on Debian Server

We also need to tell our Postfix SMTP server to check SPF record for incoming emails. This doesn’t help outgoing email delivery but will help with detecting forged incoming emails.

Install required packages:

sudo apt install postfix-policyd-spf-python

Then edit the Postfix master process configuration file.

sudo nano /etc/postfix/master.cf

Add the following lines at the end of the file, which tells Postfix to start the SPF policy daemon when it’s starting itself.

policyd-spf  unix  -       n       n       -       0       spawn
    user=policyd-spf argv=/usr/bin/policyd-spf

Save and close the file. Next, edit Postfix main configuration file.

sudo nano /etc/postfix/main.cf

Append the following lines at the end of the file. The first line specifies the Postfix policy agent timeout setting. The following lines will impose a restriction on incoming emails by rejecting unauthorized email and checking SPF record.

policyd-spf_time_limit = 3600
smtpd_recipient_restrictions =
   permit_mynetworks,
   permit_sasl_authenticated,
   reject_unauth_destination,
   check_policy_service unix:private/policyd-spf

Save and close the file. Then restart Postfix.

sudo systemctl restart postfix

Next time, when you receive an email from a domain that has an SPF record, you can see the SPF check results in the raw email header. The following header indicates the sender sent the email from an authorized host.

Received-SPF: Pass (sender SPF authorized).

Step 3: Set up DKIM on Debian Server

Install OpenDKIM which is an open-source implementation of the DKIM sender authentication system.

sudo apt install opendkim opendkim-tools

Then add postfix user to opendkim group.

sudo gpasswd -a postfix opendkim

Edit OpenDKIM main configuration file.

sudo nano /etc/opendkim.conf

Uncomment the following lines.

Canonicalization   relaxed/simple
Mode               sv
SubDomains         no

Find the following line.

#Nameservers   127.0.0.1

Change it to

Nameservers     8.8.8.8,1.1.1.1

This tells OpenDKIM to use 8.8.8.8 and 1.1.1.1 to query DNS records. It’s always a good idea to use more than one name server. If one fails, the other can still answer DNS queries. Separate each IP address with a comma.

Next, add the following lines at the end of this file.

# Map domains in From addresses to keys used to sign messages
KeyTable           refile:/etc/opendkim/key.table
SigningTable       refile:/etc/opendkim/signing.table

# Hosts to ignore when verifying signatures
ExternalIgnoreList  /etc/opendkim/trusted.hosts

# A set of internal hosts whose mail should be signed
InternalHosts       /etc/opendkim/trusted.hosts

Save and close the file.

Step 4: Create Signing Table, Key Table, and Trusted Hosts File

Create a directory structure for OpenDKIM

sudo mkdir -p /etc/opendkim/keys

Change the owner from root to opendkim and make sure only opendkim user can read and write to the keys directory.

sudo chown -R opendkim:opendkim /etc/opendkim

sudo chmod go-rw /etc/opendkim/keys

Create the signing table.

sudo nano /etc/opendkim/signing.table

Add the following line to the file. This tells OpenDKIM that if a sender on your server is using a @example.com address, then it should be signed with the private key identified by default._domainkey.your-domain.com. The second line tells that your sub-domains will be signed by the private key as well.

*@example.com      default._domainkey.example.com
*@*.example.com    default._domainkey.example.com

Save and close the file. Then create the key table.

sudo nano /etc/opendkim/key.table

Add the following line, which tells the location of the private key.

default._domainkey.example.com     example.com:default:/etc/opendkim/keys/example.com/default.private

Save and close the file. Next, create the trusted hosts file.

sudo nano /etc/opendkim/trusted.hosts

Add the following lines to the newly created file. This tells OpenDKIM that if an email is coming from localhost or from the same domain, then OpenDKIM should sign the email but won’t perform DKIM verification on the email.

127.0.0.1
localhost

.example.com

Save and close the file.

Note: You should not add an asterisk in the domain name like this: *.example.com. There should be only a dot before the domain name.

Step 5: Generate Private/Public Keypair

We need to generate two keys:

  • private key for signing outgoing emails.
  • public key for receiving SMTP server to verify the DKIM signature. It will be published in your DNS zone, so the receiving SMTP server can find it.

Create a separate folder for the domain.

sudo mkdir /etc/opendkim/keys/example.com

Generate keys using opendkim-genkey tool.

sudo opendkim-genkey -b 2048 -d example.com -D /etc/opendkim/keys/example.com -s default -v

The above command will create 2048 bits keys. -d (domain) specifies the domain. -D (directory) specifies the directory where the keys will be stored and we use default as the selector (-s), also known as the name. Once the command is executed, the private key will be written to default.private file and the public key will be written to default.txt file.

Make opendkim as the owner of the private key.

sudo chown opendkim:opendkim /etc/opendkim/keys/example.com/default.private

And change the permission, so only the opendkim user has read and write access to the file.

sudo chmod 600 /etc/opendkim/keys/example.com/default.private

Step 6: Publish Your Public Key in DNS Records

Display the public key

sudo cat /etc/opendkim/keys/example.com/default.txt

The string after the p parameter is the public key.

opendkim display public dkim record

In your DNS manager, create a TXT record, enter default._domainkey in the name field. Then go back to the terminal window, copy everything between the parentheses and paste it into the value field of the DNS record. You need to delete all double quotes and white spaces in the value field. If you don’t delete them, then the key test in the next step will probably fail.

dkim record

Step 7: Test DKIM Key

Enter the following command on Debian server to test your key.

sudo opendkim-testkey -d example.com -s default -vvv

If everything is OK, you will see Key OK in the command output.

opendkim-testkey: using default configfile /etc/opendkim.conf
opendkim-testkey: checking key 'default._domainkey.example.com'
opendkim-testkey: key secure
opendkim-testkey: key OK

Note that your DKIM record may need some time to propagate to the Internet. Depending on the domain registrar you use, your DNS record might be propagated instantly, or it might take up to 24 hours to propagate. You can go to https://www.dmarcanalyzer.com/dkim/dkim-check/, enter default as the selector and enter your domain name to check DKIM record propagation.

If you see Key not secure in the command output, don’t panic. This is because DNSSEC isn’t enabled on your domain name. DNSSEC is a security standard for secure DNS query. Most domain names haven’t enabled DNSSEC. There’s absolutely no need to worry about Key not secure. You can continue to follow this guide.

If you see the query timed out error, it means there’s DNS resolution problem on your server. You can run the above command again to see if it will work. If it still doesn’t work,  you can comment out the following line in /etc/opendkim.conf file and restart opendkim.service.

TrustAnchorFile       /usr/share/dns/root.key

Step 8: Connect Postfix to OpenDKIM

Postfix can talk to OpenDKIM via a Unix socket file. The default socket file used by OpenDKIM is /run/opendkim/opendkim.sock, as shown in /etc/opendkim.conf file. But the postfix SMTP daemon shipped with Debian runs in a chroot jail, which means the SMTP daemon resolves all filenames relative to the Postfix queue directory (/var/spool/postfix). So we need to change the OpenDKIM Unix socket file.

Create a directory to hold the OpenDKIM socket file and allow only opendkim user and postfix group to access it.

sudo mkdir /var/spool/postfix/opendkim

sudo chown opendkim:postfix /var/spool/postfix/opendkim

Then edit the OpenDKIM main configuration file.

sudo nano /etc/opendkim.conf

Find the following line.

Socket    local:/run/opendkim/opendkim.sock

Replace it with the following line.

Socket    local:/var/spool/postfix/opendkim/opendkim.sock

Save and close the file. Next, open the /etc/default/opendkim file.

sudo nano /etc/default/opendkim

Find the following line.

SOCKET=local:$RUNDIR/opendkim.sock

Change it to

SOCKET="local:/var/spool/postfix/opendkim/opendkim.sock"

debian opendkim unix socket

Save and close the file.

Next, we need to edit the Postfix main configuration file.

sudo nano /etc/postfix/main.cf

Add the following lines at the end of this file, so Postfix will be able to call OpenDKIM via the milter protocol.

# Milter configuration
milter_default_action = accept
milter_protocol = 6
smtpd_milters = local:opendkim/opendkim.sock
non_smtpd_milters = $smtpd_milters

Save and close the file. Then restart opendkim and postfix service.

sudo systemctl restart opendkim postfix

Step 9: SPF and DKIM Check

You can now send a test email from your mail server to your Gmail account to see if SPF and DKIM checks are passed. On the right side of an opened email message in Gmail, if you click the show original button from the drop-down menu, you can see the authentication results.

Gmail SPF and DKIM check scalahosting

If your message is not signed and DKIM check failed, you can check postfix log (/var/log/mail.log) to see what’s wrong with your configuration. If you see the following message in the mail log,

warning: connect to Milter service local:opendkim/opendkim.sock: No such file or directory

you may want to check if the opendkim systemd service is actually running.

sudo systemctl status opendkim

If opendkim is running and you still see the above error, you might need to edit the /etc/postfix/main.cf file, change

 smtpd_milters = local:opendkim/opendkim.sock

to

 smtpd_milters = local:/opendkim/opendkim.sock

Then restart Postfix.

Your email server will also perform SPF and DKIM checks on the sender’s domain. You can see the results in the email headers. The following is SPF and DKIM check on a sender using Gmail.

Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=2607:f8b0:4864:20::c2d; helo=mail-yw1-xc2d.google.com; [email protected]; receiver=<UNKNOWN> 
Authentication-Results: email.linuxbabe.com;
	dkim=pass (2048-bit key; unprotected) header.d=gmail.com [email protected] header.b="XWMRd2co";
	dkim-atps=neutral

Configuration Error in Email Client

DKIM signing could fail if you don’t use the correct SMTP/IMAP settings in your email client.

Correct Settings:

  • SMTP protocol: enter mail.your-domain.com as the server name, choose port 587 and STARTTLS. Choose normal password as the authentication method.
  • IMAP protocol: enter mail.your-domain.com as the server name, choose port 143 and STARTTLS. Choose normal password as the authentication method.

or

  • SMTP protocol: enter mail.your-domain.com as the server name, choose port 465 and SSL/TLS. Choose normal password as the authentication method.
  • IMAP protocol: enter mail.your-domain.com as the server name, choose port 993 and SSL/TLS. Choose normal password as the authentication method.

Wrong Settings:

  • Use port 25 as the SMTP port in mail clients to submit outgoing emails.
  • No encryption method was selected.

Port 25 should be used for SMTP server to SMTP server communication. Please don’t use it in your email client to submit outgoing emails.

You should select an encryption method (STARTTLS or SSL/TLS) in your email client.

Step 10: Testing Email Score and Placement

You can also go to https://www.mail-tester.com. You will see a unique email address. Send an email from your domain to this address and then check your score. As you can see, I got a perfect score.

imporve email server reputation

Mail-tester.com can only show you a sender score. There’s another service called GlockApps that allow you to check if your email is placed in the recipient’s inbox or spam folder, or rejected outright. It supports many popular email providers like Gmail, Outlook, Hotmail, YahooMail, iCloud mail, etc

glockapps email placement test scalahosting

Microsoft Mailboxes

If your emails are rejected by Microsoft Outlook or Hotmail, you need to submit the sender information form. After that, your email will be accepted by Outlook/hotmail, but may still be labeled as spam.

What if Your Emails Are Still Being Marked as Spam?

I have more tips for you in this article: 7 effective tips to stop your emails from being marked as spam.

Next Step

In part 5, we will see how to create DMARC record to protect your domain from email spoofing. As always, if you found this post useful, please subscribe to our free newsletter or follow us on Twitter, or like our Facebook page.

Rate this tutorial
[Total: 15 Average: 4.8]

14 Responses to “Part 4: Set Up SPF & DKIM with Postfix on Debian Server

  • Chao Jiang
    2 years ago

    Hello! Very nice article!
    Whereas I’d like to ask if you have any idea about the support of QMTP of Postfix?
    Thanks
    Chao

  • Hi, would you tell us where to replace example.com with our domain and where not?(like you did along whole email setup tutorials)

    • Xiao Guoan (Admin)
      2 years ago

      Anywhere you see example.com, you should replace it with your real domain name.

  • Patrick
    2 years ago

    Hi, it seems like there is some extra place where it still decides to use example.com for dkim. Is there any other specific place?

  • Great write up!

    But it seems that emails with no SPF are let through. How can that be changed? Proof below:

    Return-Path: [email protected]
    Received: from bemail.xxxxxx.com (xxxxxxx.com [xxxxxxx.114.165])
    by xxxx with ESMTP
    ; Mon, 9 May 2022 18:07:31 -0400
    Message-ID:
    Received-SPF: None (mailfrom) identity=mailfrom; client-ip=139.180.135.154; helo=main.lspdki.id; [email protected]; receiver=
    Received: from main.lspdki.id (unknown [139.180.135.154])
    by bemail.xxxxx.com (Postfix) with ESMTPS id DC0A9BD180
    for ; Mon, 9 May 2022 18:07:31 -0400 (EDT)
    Received: from [2.56.56.212] (helo=User)
    by main.lspdki.id with smtp (Exim 4.93)
    (envelope-from )
    id 1noBVH-0066RM-3A; Mon, 09 May 2022 22:05:19 +0000
    Reply-To:
    From: “Mrs. Aisha Muhammadu Buhari”
    Subject: I have a BANK DRAFT WORTH’ USD$246,250,000.00
    Date: Mon, 9 May 2022 15:05:18 -0700
    MIME-Version: 1.0
    Content-Type: text/plain;
    charset=”Windows-1251″
    Content-Transfer-Encoding: 7bit
    X-Priority: 3
    X-MSMail-Priority: Normal
    X-Mailer: Microsoft Outlook Express 6.00.2600.0000
    X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2600.0000

    Sir/Madam,

    This is from the office of the first lady of the Federal Republic of Nigeria. Sir/Madam, How are you doing today? I am Mrs. Aisha Muhammadu Buhari the first lady of the Federal Republic of Nigeria. I am sorry for contacting you directly. I have a very genuine and Lucrative business opportunity that I want to share with you. As you may know, this is the final tenure of my husband in the office.

    I have a BANK DRAFT WORTH’ USD$246,250,000.00 ) deposited with the National Bank of Fujairah Dubai, United Arab Emirates. Please I need your help for immediate crediting of this fund into your bank and all it will cost you is sending the cost of re-validating the bank draft to be entered in your name or company and you will start to withdraw from your bank account. Please acknowledge my email so I can provide you with details.

    Confidentiality is the bond of this transaction.

    I await your response.

    Best regards,

    Mrs. Aisha Muhammadu Buhari.
    The First Lady of Federal Republic of Nigeria

    Thank you!

  • Karthik
    2 years ago

    Hi there,

    On Step 6, publishing public key in DNS records. I see there are two lines there for public key in parentheses. Not sure line to include in TXT record ? Attaching screenshot to make it more clear. Im not sure to add the yellow marked public key or yellow + orange marked together. Kindly help.

    • Xiao Guoan (Admin)
      2 years ago

      You need to copy everything between the parentheses.

  • Thomas
    1 year ago

    Dear Xiao,

    I have an error message when testing the DKIM key and google didn’t help to interprete the error message. When I run the “opendkim-testkey” command I get the following three lines:

    opendkim-testkey: using default configfile /etc/opendkim.conf
    opendkim-testkey: checking key 'default._domainkey.viampp.com'
    opendkim-testkey: invalid key version 'DKIM'
    

    When using www.dmarcanalyzer.com I get:

    We have detected the following problems in your DKIM record.
    Error : There is an unknown version detected. This could make your record invalid.
    

    I have reinstalled the packages and created the key again. With a colleague we checked the p string in the DNS a view times and it is correct.

    I am out of ideas. Maybe you can help.

    I am running debian bullseye on a raspberry pi 4 with 8GB memory.

    Thanks in advance.
    Thomas

  • Jason Bourne
    1 year ago

    This is a fantastic guide. Can you add a section on enabling indexed FTS full text search? SOLR?

  • Steve F
    1 year ago

    Thanks for what seems like an excellent guide. Unfortunately, like a lot of people, I find that the opendkim service fails to start with a message:

    systemd[1]: opendkim.service: Can't open PID file /run/opendkim/opendkim.pid (yet?) after start: Operation not permitted
    

    the pid file is created but is owned by root. What do you consider to be the best solution. There are quite a range of solutions offered on the web but I have not had much success with them.

  • Wonderful tutorial! I had tried to set up DKIM before and gave up in confusion, but following your instructions it was very straightforward.

    I did have to do one thing differently. I run bind on my server, so the “in your DNS manager” part didn’t apply, and when I tried to update bind’s db.example.com, removing the parentheses and quotes and spaces as you describe, I got a syntax error. Apparently bind has a line length limit, and I guess that’s why opendkim-genkey used that multi-line format with the parentheses, because when I copied all the lines from /etc/opendkim/keys/example.com/default.txt, including the parentheses and spaces, everything except for the trailing semicolon and the final “—– DKIM key default” line, it worked.

    Everything else worked perfectly. Thanks so much for this excellent tutorial.

  • Alexander Wisely
    3 months ago

    This is a great tutorial, however i got to the end of this section and i can no longer recieve emails from anyone outside my domain. I get error with status code 5.7.1

    Any ideas why this might be?

  • Marcelo Oscar Olcese
    1 month ago

    Synthetic and very well explained.
    Thank you so much.-

Leave a Comment

  • Comments with links are moderated by admin before published.
  • Your email address will not be published.
  • Use <pre> ... </pre> HTML tag to quote the output from your terminal/console.
  • Please use the community (https://community.linuxbabe.com) for questions unrelated to this article.
  • I don't have time to answer every question. Making a donation would incentivize me to spend more time answering questions.

The maximum upload file size: 2 MB. You can upload: image. Links to YouTube, Facebook, Twitter and other services inserted in the comment text will be automatically embedded. Drop file here