Part 4: Set Up SPF and DKIM with Postfix on CentOS 8/RHEL 8 Mail Server

After completing part 1 and part 2, we have a working Postfix SMTP server and Dovecot IMAP server. We can send and receive email using a desktop email client. Although I have created correct MX, A and PTR record, my emails were flagged as spam by Gmail and Outlook mail. So in this part, we are going to look at how to improve email delivery to recipient’s inbox by setting up SPF and DKIM on CentOS/RHEL server.

What are SPF and DKIM Records?

SPF and DKIM are two types of TXT records in DNS that can help with preventing email spoofing and making legitimate emails delivered into the recipient’s inbox instead of spam folder. If your domain is abused by email spoofing, then your emails are likely to landed in recipient’s spam folder if the recipient didn’t add you in 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 records.

Create an SPF Record in DNS

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

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 name.
  • v=spf1 indicates this is a 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 sent from other hosts will be flagged as forged. 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 strict policy.

Note that some DNS managers require you to wrap the SPF record with double-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 machine like below:

dig your-domain.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 online SPF validator such as spf.myisp.ch to see which hosts are allowed to send emails for your domain and debug your SPF record if any error occurs. The dmarcian SPF surveyor can help test your SPF record syntax.

Configuring SPF Policy Agent

We also need to tell our Postfix SMTP server to check the SPF record of incoming emails to detect forged emails. First install required packages:

sudo dnf install epel-release

sudo dnf install pypolicyd-spf

Then add a user for policyd-spf.

sudo adduser policyd-spf --user-group --no-create-home -s /bin/false

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 will run as the policyd-spf user.

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

centos 8 pypolicyd-spf

Note: You should not run policyd-spf in a chroot environment.

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 (for querying DNS). The following lines will impose restrictions on incoming emails by 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 (mailfrom) identity=mailfrom;

Setting Up DKIM

Two common pieces of software that can do DKIM signing and verification on Linux are OpenDKIM and Amavis. We will use OpenDKIM because it’s lightweight and OpenDMARC doesn’t work with Amavis.

Install and Configure OpenDKIM on CentOS 8/RHEL8

Install OpenDKIM from the EPEL (Extra Packages for Enterprise Linux) repository.

sudo dnf install epel-release

sudo dnf install opendkim perl-Getopt-Long

Edit OpenDKIM main configuration file.

sudo nano /etc/opendkim.conf

Find the following line.

Mode     v

By default, OpenDKIM runs in verification mode (v), which will verify the DKIM signature of incoming email messages. We need to sign outgoing emails, so change this line to the following to enable signing mode.

Mode           sv

Then find the following lines.

## Specifies whether or not the filter should generate report mail back
## to senders when verification fails and an address for such a purpose
## is provided. See opendkim.conf(5) for details.
SendReports yes

## Specifies the sending address to be used on From: headers of outgoing
## failure reports. By default, the e-mail address of the user executing
## the filter is used (executing_user@hostname).
# ReportAddress "Example.com Postmaster" <postmaster@example.com>

When a signature verification fails and the signature included a reporting request (“r=y”) and the signing domain advertises a reporting address (i.e. ra=user) in a reporting record in the DNS, OpenDKIM will send a structured report to that address containing details needed to reproduce the problem. You may want to use a particular From email address to send the report. Uncomment the ReportAddress parameter and change email address. Note that this will not create the backscatter problem, because report emails will sent to an email address specified in the sender domain’s DNS record.

Find the following line and comment it out, because we will use separate keys for each domain name.

KeyFile   /etc/opendkim/keys/default.private

Next, find the following 4 lines and uncomment them.

# KeyTable            /etc/opendkim/KeyTable

# SigningTable        refile:/etc/opendkim/SigningTable

# ExternalIgnoreList  refile:/etc/opendkim/TrustedHosts

# InternalHosts       refile:/etc/opendkim/TrustedHosts

Save and close the file.

Create Signing Table, Key Table and Trusted Hosts File

Edit the signing table file.

sudo nano /etc/opendkim/SigningTable

Add the following line at the end of this file. This tells OpenDKIM that if a sender on your server is using a @your-domain.com address, then it should be signed with the private key identified by 20200308._domainkey.your-domain.com.

*@your-domain.com    20200308._domainkey.your-domain.com

20200308 is the DKIM selector. A domain name might have multiple DKIM keys. The DKIM selector allows you to choose a particular DKIM key. You can use whatever name for the DKIM selector, but I found it’s convenient to use the current date (March 8, 2020) as the DKIM selector. Save and close the file. Then edit the key table file.

sudo nano /etc/opendkim/KeyTable

Add the following line, which specifies the location of the DKIM private key.

20200308._domainkey.your-domain.com     your-domain.com:20200308:/etc/opendkim/keys/your-domain.com/20200308.private

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

sudo nano /etc/opendkim/TrustedHosts

127.0.0.0.1 and ::1 are included in this file by default. Now add the following line. This tells OpenDKIM that if an email is coming from your own domain name, then OpenDKIM should not perform DKIM verification on the email.

*.your-domain.com

Save and close the file.

Generate Private/Public Keypair

Since DKIM is used to sign outgoing messages and verify incoming messages, you need to generate a private key to sign outgoing emails and a public key for receiving SMTP servers to verify the DKIM signature of your email. Public key will be published in DNS.

Create a separate folder for the domain.

sudo mkdir /etc/opendkim/keys/your-domain.com

Generate keys using opendkim-genkey tool.

sudo opendkim-genkey -b 2048 -d your-domain.com -D /etc/opendkim/keys/your-domain.com -s 20200308 -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. I use 20200308 as the DKIM selector. Once the command is executed, the private key will be written to 20200308.private file and the public key will be written to 20200308.txt file.

centos 8 opendkim

By default, only root can read and write to the key files. Make opendkim as the owner of the private key.

sudo chown opendkim:opendkim /etc/opendkim/keys/ -R

Publish Your Public Key in DNS Records

Display the public key

sudo cat /etc/opendkim/keys/your-domain.com/20200308.txt

The string after the p parameter is the public key.

install opendkim centos 8

In you DNS manager, create a TXT record, enter 20200308._domainkey in the name field. (You need to replace 20200308 with your own DKIM selector.) Then go back to the terminal window, copy everything in the parentheses and paste it into the value field of the DNS record. You need to delete all double quotes and line breaks in the value field. If you don’t delete them, then key test in the next step will probably fail.

DKIM records

Test DKIM Key

Enter the following command on your CentOS 8/RHEL 8 server to test your key.

sudo opendkim-testkey -d your-domain.com -s 20200308 -vvv

If everything is OK, you will see the key OK message.

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

If you see “Key not secure”, 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. You can continue to follow this guide.

Now we can start the opendkim service.

sudo systemctl start opendkim

And enable auto-start at boot time.

sudo systemctl enable opendkim

OpenDKIM listens on 127.0.0.1:8891.

Connect Postfix to OpenDKIM

Edit 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. Note that you should use 127.0.0.1 as the address. Don’t use localhost.

# Milter configuration
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:127.0.0.1:8891
non_smtpd_milters = $smtpd_milters

Save and close the file. Then add postfix user to opendkim group.

sudo gpasswd -a postfix opendkim

Restart postfix service.

sudo systemctl restart postfix

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

If your message is not signed and DKIM check failed, you may want to check postfix log (/var/log/maillog) to see what’s wrong in your configuration. Your email server will also perform SPF and DKIM check on 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; envelope-from=someone@gmail.com; receiver=<UNKNOWN> 
Authentication-Results: mail.linuxbabe.com;
	dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="XWMRd2co";
	dkim-atps=neutral

Testing Email Score and Placement

Now you can 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

Microsoft Mailboxes (Hotmail.com, Outlook.com)

Microsoft seems to be using an internal blacklist that block many legitimate IP addresses. If your emails are rejected by 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. In my test, the email landed in my Gmail inbox. However, it’s stilled labeled as spam in my outlook.com email although both SPF and DKIM are passed.

What if Your Emails Are Still Being Marked as Spam?

I have more tips for you in this article: How to stop your emails 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, then subscribe to our free newsletter to receive more useful articles, or follow us on Twitter or like our Facebook page.

Rate this tutorial
[Total: 2 Average: 5]

8 Responses to “Part 4: Set Up SPF and DKIM with Postfix on CentOS 8/RHEL 8 Mail Server

  • This worked like a dream. My new mailserver has working TLS, DKIM, SPF and DMARC. Thanks for the clear instructions!

  • greetings. I’m using centos8 on hostwinds and the guide has been great so far. I’m having problems getting the DKIM TXT value into DNS settings on hostwinds. I’m not using a managed host, so hostwinds wouldn’t help me get my DKIM record setup. Were you able to sucessfully add a dkim txt record to the hostwinds dns manager? (not cpanel). If so, how did you structure the value?

  • regarding SPF: why not select the most restrictive method for forgers? As end user, you still get the mail with the forged enveloppe-from in your mailbox and most mail-users don’t bother with checking headers for “softfail”, and that’s understandable.
    So in order to get something out of this, you’ll probably want another mechanism to pickup on the “softfail” header? Personally, I do like the noqueue option, not even wasting bandwith on forged emails. Just my way I guess? Are there good reasons to just go for softfail? I guess so since it appears to be the most generally used.

    • Using -all in SPF policy will sometimes cause your own emails to be rejected. For example, if the recipient’s main mail server goes offline, and your email is sent to a backup mail server, then the backup mail server sends your email to the main mail server. Your email will be rejected. So use softfail (~all) in your SPF record.

      Nowadays, it’s a standard practice to combine SPF and DKIM to form a DMARC policy to reject forged emails.

  • Grant Griffith
    10 hours ago

    Thanks for all of this, very helpful. But I am having issues with DKIM for some reason. All testing seems to be good on the server, but when actually sending an email from a client it fails.

    Delivered-To: ggriffit@gmail.com
    Received: by 2002:a5e:d916:0:0:0:0:0 with SMTP id n22csp2046290iop;
            Sat, 26 Sep 2020 08:31:46 -0700 (PDT)
    X-Google-Smtp-Source: ABdhPJyAvaszsVC5z5SNUyQ8ZMQ9bMo2j1Yzo3WfUaz0A8Yhu+PuxdepfQ2kdC+7lC0kTxxK8gbs
    X-Received: by 2002:a25:c6c3:: with SMTP id k186mr5945675ybf.91.1601134306294;
            Sat, 26 Sep 2020 08:31:46 -0700 (PDT)
    ARC-Seal: i=1; a=rsa-sha256; t=1601134306; cv=none;
            d=google.com; s=arc-20160816;
            b=otHkvkZLOG+Q8B90ymw4714k5Jzqwvpy3koY4o/JHLSL+Eu6SAiJmTIvNJY25Hx2nL
             oXIJ5QSf8k3EBzJ0CDZ2GHAOtYh4R/lGBr6STc+aXlsufoGD5r+sqVTRa/SaJThT3nk/
             XfPMGJxSyOvqx11kgi/cMl4z/W0T4uiSNbU1o5wJnoAftFDOusShaJwWUhLX/no2NSCl
             cNxSOh/JHEuhE9XnWCF8SomQMSkTnrEmzbKmOI3RpDm4wAxhnbNnWLTQLMFbN/xqr/ii
             ZZ9qFYRivoHRVnUEeSWrpDS5CK2iMIzmXh2UQt2XOXJoZfrN/aTtT0Cf21QCbG4JnhEm
             dbvQ==
    ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
            h=content-language:content-transfer-encoding:mime-version:user-agent
             :date:message-id:subject:from:to:dkim-signature:dkim-filter;
            bh=KFwoz3+PGL66l/CGi3KZjGmp93eU5Z0LNuofRTvivgc=;
            b=K7w0/ykzKkKelvIV0nHS/M0DjI5bYfqhFyw6Phqh0/IAg1IE/FdgvQ/n+R/A7PRval
             GePJU3fWKe9aIAImwTj1NieK75no7S+R0ffFGheaU0yYYROz7Ho/SigjXN6N2czNHgGL
             T6S6DU3bdHn0fFWZ65clYOwyJAa15CGuUKW4Til+h9tGSmF7zbOtdNn8lFLy0/W1Ioqp
             O+tEuvfD5TNjAXVny54rYIWpSzibrUy+17s4Iabx4mY+SjwHEMDqfkxl4JRepwHfKuSL
             6f+kDLP/5/dD+qoKMbBI4z53zCDPOwxTluevHZokThNSArOC8+yJqa+/npUqwBo9otVP
             eOAw==
    ARC-Authentication-Results: i=1; mx.google.com;
           dkim=neutral (invalid public key) header.i=@batesvilletechnology.com header.s=20200923 header.b=VVTo6GOv;
           spf=pass (google.com: domain of grifgr10@batesvilletechnology.com designates 209.173.134.91 as permitted sender) smtp.mailfrom=grifgr10@batesvilletechnology.com;
           dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=batesvilletechnology.com
    Return-Path: 
    Received: from mail-out.batesvilletechnology.com (mail-out.batesvilletechnology.com. [209.173.134.91])
            by mx.google.com with ESMTPS id p22si6327377yba.345.2020.09.26.08.31.45
            for 
            (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
            Sat, 26 Sep 2020 08:31:46 -0700 (PDT)
    Received-SPF: pass (google.com: domain of grifgr10@batesvilletechnology.com designates 209.173.134.91 as permitted sender) client-ip=209.173.134.91;
    Authentication-Results: mx.google.com;
           dkim=neutral (invalid public key) header.i=@batesvilletechnology.com header.s=20200923 header.b=VVTo6GOv;
           spf=pass (google.com: domain of grifgr10@batesvilletechnology.com designates 209.173.134.91 as permitted sender) smtp.mailfrom=grifgr10@batesvilletechnology.com;
           dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=batesvilletechnology.com
    Received: from [192.168.1.67] (unknown [216.8.201.232]) by mail-out.batesvilletechnology.com (Postfix) with ESMTPSA id 606B82062B40 for ; Sat, 26 Sep 2020 15:31:45 +0000 (UTC)
    DKIM-Filter: OpenDKIM Filter v2.11.0 mail-out.batesvilletechnology.com 606B82062B40
    DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=batesvilletechnology.com; s=20200923; t=1601134305; bh=KFwoz3+PGL66l/CGi3KZjGmp93eU5Z0LNuofRTvivgc=; h=To:From:Subject:Date:From; b=VVTo6GOvndvqRJxi7hFauw38GFWxErEJzGkA/8PQihNT0/XHwsmKo9R/D6FplvPAB
    	 jGQ4+lATxiX5mJG6M/cHI3PjSFURME6Fr9W0wz2r+YqTOkvb1se8lfquQy5aRlLj2h
    	 pTv1uFLRKGSzQQCd8/3WjU5UBihAupZpkX1iTo7WfpOX3EU19o+1Y3G40p8L7Zec5p
    	 1oQpm44HJJB11ZZ1CjKBfiHJCUjL+er+mU+nASyrMGL1HL7elPInXLOEiKEyoCcyD1
    	 qWhEbPF1Ifz9XbBrYpWzNyYQNFKByp/uQrKChFB/uqLW2JaFSwILQ6YGF6i+6zwL4H
    	 0gQQfJtkKERfQ==
    To: ggriffit@gmail.com
    From: Grant 
    Subject: test
    Message-ID: 
    Date: Sat, 26 Sep 2020 11:31:46 -0400
    User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Thunderbird/78.2.2
    MIME-Version: 1.0
    Content-Type: text/plain; charset=utf-8; format=flowed
    Content-Transfer-Encoding: 7bit
    Content-Language: en-US
    
    test4
    

    Spent several hours searching and double checking things and can not figure out what I missed. Any help would be greatly appreciated.

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.