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

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.

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 python3-pip 
sudo -H pip3 install pyspf py3dns 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/local/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 (for querying DNS). The following lines will impose restriction 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 (sender SPF authorized).

Setting up DKIM

Two common pieces of software that can do DKIM signing and verification on Linux are OpenDKIM and Amavis. OpenDKIM is not included in CentOS 8/RHEL 8 repository, so we will use Amavis. Amavis (A Mail Virus Scanner) is a high-performance interface between message transfer agent (MTA) and content filters. It’s commonly used for

  • virus-scanning by integrating with ClamAV
  • spam-checking by integrating with SpamAssassin
  • DKIM signing and verification

In this tutorial, we will focus on DKIM signing and verification. Virus-scanning and spam-checking will be discussed in a future tutorial.

Install Amavisd-new on CentOS 8/RHEL 8

Amavisd-new is written in Perl. We need to enable the EPEL (Extra Packages for Enterprise Linux) and CodeReady Linux Builder repository on RHEL 8 to install some Perl dependencies for Amavisd-new.

sudo dnf install epel-release

sudo subscription-manager repos --enable=codeready-builder-for-rhel-8-x86_64-rpms

On CentOS 8, enable the EPEL (Extra Packages for Enterprise Linux) and PowerTools repository to install Perl dedpendencies for Amavisd-new.

sudo dnf install epel-release
sudo dnf config-manager --set-enabled PowerTools

Then install the amavisd-new package.

sudo dnf install amavisd-new

Start Amavis.

sudo systemctl start amavisd

Enable auto-start at boot time.

sudo systemctl enable amavisd

Check its status:

systemctl status amavisd

Sample output:

 amavisd.service - Amavisd-new is an interface between MTA and content checkers.
   Loaded: loaded (/usr/lib/systemd/system/amavisd.service; enabled; vendor preset: disabled)
   Active: active (running) since Sun 2020-01-19 06:18:14 EST; 40s ago
     Docs: http://www.ijs.si/software/amavisd/#doc
 Main PID: 10827 (/usr/sbin/amavi)
    Tasks: 3 (limit: 5047)
   Memory: 8.8M
   CGroup: /system.slice/amavisd.service
           ├─10827 /usr/sbin/amavisd (master)                                                                                                                                                              
           ├─10831 /usr/sbin/amavisd (virgin child)                                                                                                                                                        
           └─10832 /usr/sbin/amavisd (virgin child)

Amavisd listens on 127.0.0.1:10024, as can be seen with:

sudo netstat -lnpt | grep amavisd

And it runs as the amavis user.

Configuring Amavis

The main configuration file is /etc/amavisd/amavisd.conf.  Open it with:

sudo nano /etc/amavisd/amavisd.conf

First, find the following line. (In Nano text editor, you can press Ctrl W to search a string.)

$mydomain = 'example.com';

Change example.com to your apex domain name.

$mydomain = 'yourdomain.com';

Then find the following line.

# $myhostname = 'host.example.com';

Remove the comment character (#) and change the hostname to your real hostname like below. $myhostname is used by Amavisd-new for node identification, and it is important to get it right for ESMTP EHLO, loop detection, and so on.

$myhostname = 'mail.yourdomain.com';

By default, DKIM signing and verification are both enabled, as indicated by the following two lines, which is fine.

$enable_dkim_verification = 1;  # enable DKIM signatures verification
$enable_dkim_signing = 1;       # load DKIM signing code, keys defined by dkim_key

Next, we can add the following line, which defines the DKIM selecor and DKIM key for this particular domain name.

dkim_key('yourdomain.com', '20200119', '/var/spool/amavisd/dkim/yourdomain.com.pem');

You can use whatever name for the DKIM selector, but I found it’s convienent to use the current date (January 19, 2010) as the DKIM selector. The private key is located at /var/spool/amavised/yourdomain.com.pem. Then we also need to add the following lines.

@dkim_signature_options_bysender_maps = ( {
    # 'd' defaults to a domain of an author/sender address,
    # 's' defaults to whatever selector is offered by a matching key

    # explicit 'd' forces a third-party signature on foreign (hosted) domains
    "yourdomain.com"  => { d => "yourdomain.com", a => 'rsa-sha256', ttl => 10*24*3600 },

    # catchall defaults
    '.' => { a => 'rsa-sha256', c => 'relaxed/simple', ttl => 30*24*3600 },
} );

This tells Amavis that emails sent from yourdomain.com should be signed with the DKIM key belonging to yourdomain.com. If you have two domains, you can use one of the domains’ DKIM key to sign emails of both domains, but it’s a good practice to use a dedicated DKIM key for each domain. Save and close the file.

Then we need to generate a DKIM private key. Run the following command to create the /var/spool/amavisd/dkim/ directory to store the keys.

sudo mkdir /var/spool/amavisd/dkim/

Generate a private key for your domain.

sudo amavisd genrsa /var/spool/amavisd/dkim/yourdomain.com.pem 2048

Display the public key.

sudo amavisd -c /etc/amavisd/amavisd.conf showkeys

The string after the p parameter is the public key.

In you DNS manager, create a TXT record, enter 20200119._domainkey in the name field. If you used a different DKIM selector, replace 20200119 with your real 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 will probably fail.

After saving your changes. Check the TXT record with this command.

dig  TXT   20200119._domainkey.yourdomain.com

Now you can run the following command to test if your DKIM DNS record is correct.

sudo amavisd -c /etc/amavisd/amavisd.conf testkeys

If the DNS record is correct, the test will pass.

TESTING#1 linuxbabe.com: 20200119._domainkey.linuxbabe.com => pass

Using A Dedicated Port for Email Submissions

The default Amavis listening port 10024 is for scanning incoming email messages. It’s a good practice to use a different port such as 10026 for email submissions from authenticated users. Edit the Amavis main configuration file.

sudo nano /etc/amavisd/amavisd.conf

Find the following line.

$inet_socket_port = 10024;

Add a # character to comment it out.

#$inet_socket_port = 10024;

Then uncomment the following line, so Amavisd will also listen on port 10026.

$inet_socket_port = [10024,10026];

Scrolling down a little bit, you can find the following line, which sets the “ORIGINATING” policy for port 10026.

$interface_policy{'10026'} = 'ORIGINATING';

Then you can find the following lines, which defines the “ORIGINATING” policy.

$policy_bank{'ORIGINATING'} = {  # mail supposedly originating from our users
  originating => 1,  # declare that mail was submitted by our smtp client
  allow_disclaimers => 1,  # enables disclaimer insertion if available
  # notify administrator of locally originating malware
  virus_admin_maps => ["virusalert@$mydomain"],
  spam_admin_maps  => ["virusalert@$mydomain"],
  warnbadhsender   => 1,
  # forward to a smtpd service providing DKIM signing service
  forward_method => 'smtp:[127.0.0.1]:10027',
  # force MTA conversion to 7-bit (e.g. before DKIM signing)
  smtpd_discard_ehlo_keywords => ['8BITMIME'],
  bypass_banned_checks_maps => [1],  # allow sending any file names and types
  terminate_dsn_on_notify_success => 0,  # don't remove NOTIFY=SUCCESS option
};

In the above lines, you can see that by default Amavis would forward emails to a SMTPD service providing DKIM signing servie. Since we are going to use Amavis itself to sign emails submitted from authenticated users, we need to comment out the forward_method directive.

# forward_method => 'smtp:[127.0.0.1]:10027',

Save and close the file. Next, we need to tell SELinux to allow Amavis to use port 10026. Install the following package, which provides the semanage command.

sudo dnf install policycoreutils-python-utils

Then set the port type of 10026 to amavisd_recv_port_t, so Amavis will be able to listen on port 10026.

sudo semanage port -m -t amavisd_recv_port_t -p tcp 10026

Restart Amavis

sudo systemctl restart amavisd

Check its status to see if the restart is successful.

systemctl status amavisd

Integrating Postfix SMTP Server with Amavis

Amavisd-new works as a SMTP proxy. Email is fed to it through SMTP, processed, and fed back to the MTA through a new SMTP connection.

Edit the Postfix main configuration file.

sudo nano /etc/postfix/main.cf

Add the following line at the end of the file. This tells Postfix to turn on content filtering by sending every incoming email messages to Amavis listening on 127.0.0.1:10024.

content_filter = smtp-amavis:[127.0.0.1]:10024

Save and close the file. Then edit the master.cf file.

sudo nano /etc/postfix/master.cf

Add the following lines at the end of the file. This instructs Postfix to use a special SMTP client component called smtp-amavis to deliver email messages to Amavis. Please allow at least one whitespace character (tab or spacebar) before each -o.  In postfix configurations, a preceding whitespace character means that this line is continuation of the previous line.

smtp-amavis   unix   -   -   n   -   2   smtp
    -o syslog_name=postfix/amavis
    -o smtp_data_done_timeout=1200
    -o smtp_send_xforward_command=yes
    -o disable_dns_lookups=yes
    -o max_use=20

Then add the following lines at the end of the file. This tells Postfix to run an additional smtpd daemon listening on 127.0.0.1:10025 to receive email messages back from Amavis.

127.0.0.1:10025   inet   n    -     n     -     -    smtpd
    -o syslog_name=postfix/10025
    -o content_filter=
    -o mynetworks_style=host
    -o mynetworks=127.0.0.0/8
    -o local_recipient_maps=
    -o relay_recipient_maps=
    -o strict_rfc821_envelopes=yes
    -o smtp_tls_security_level=none
    -o smtpd_tls_security_level=none
    -o smtpd_restriction_classes=
    -o smtpd_delay_reject=no
    -o smtpd_client_restrictions=permit_mynetworks,reject
    -o smtpd_helo_restrictions=
    -o smtpd_sender_restrictions=
    -o smtpd_recipient_restrictions=permit_mynetworks,reject
    -o smtpd_end_of_data_restrictions=
    -o smtpd_error_sleep_time=0
    -o smtpd_soft_error_limit=1001
    -o smtpd_hard_error_limit=1000
    -o smtpd_client_connection_count_limit=0
    -o smtpd_client_connection_rate_limit=0
    -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks,no_address_mappings

Next, add the following line to the submission service.

 -o content_filter=smtp-amavis:[127.0.0.1]:10026

Like this:

submission     inet     n    -    y    -    -    smtpd
    -o syslog_name=postfix/submission
    -o smtpd_tls_security_level=encrypt
    -o smtpd_tls_wrappermode=no
    -o smtpd_sasl_auth_enable=yes
    -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
    -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
    -o smtpd_sasl_type=dovecot
    -o smtpd_sasl_path=private/auth
    -o content_filter=smtp-amavis:[127.0.0.1]:10026

So emails submited from authenticated users will be forwarded to Amavis for DKIM signing. If you have enabled the smtps service for Microsoft Outlook users, then you also need to add this line to the smtps service.

smtps     inet  n       -       y       -       -       smtpd
    -o syslog_name=postfix/smtps
    -o smtpd_tls_wrappermode=yes
    -o smtpd_sasl_auth_enable=yes
    -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
    -o smtpd_sasl_type=dovecot
    -o smtpd_sasl_path=private/auth
    -o content_filter=smtp-amavis:[127.0.0.1]:10026

Save and close the file. Restart Postfix for the changes to take effect.

sudo systemctl restart postfix

Check its status to see if the restart is successful.

systemctl status 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.

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; [email protected]; receiver= 
Authentication-Results: mail.linuxbabe.com;
	dkim=pass (2048-bit key; unprotected) header.d=gmail.com [email protected] 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.

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

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: 0 Average: 0]