Setup Email Server From Scratch On FreeBSD #2 - 06 SPF DKIM DMARC

05 PostfixAdmin <- Intro -> 07 RoundCube WebMail

We believe in data independence, and support others who want data independence.
Debian Email From Scratch version 2 finished 2025-07-30.

We are still adding to it but it all works!


#########################################
# Setting Up SPF DMARC and DKIM Records #
#########################################

To improve mail delivery to other email servers we need to setup SPF, DKIM, and
DMARC records so recipient mail servers can identify we are allowed to send
emails for our domain. We also need these mail filters, milters, to mark or
block incoming spam. To setup these records we need to modify the DNS records
for our domain. I covered DMARC and SPF DNS setup on the first page of server
installation, and I'll post it here again to refresh.

Login to your DNS provider, which is usually the same as domain registrar. I use
Namecheap and DNS is included with the domain registration, no extra charge.

DKIM requires dnssec so enable dns security aka dnssec. Go to the NameCheap
advanced DNS page and toggle on DNSSEC. The toggle may be a little broken, so
click in the empty space where it's supposed to appear till it toggles on and
is visible. Test with dig and look for the ad flag using another dnssec
enabled name server. The 'ad' flag won't show up if you test against the
master nameserver for the same domain.

Namecheap Advanced DNS -> Toggle On DNS Sec

Test if dnssec is enabled, look for the line with the 'ad' flag

root@okdeb.com:~# dig @8.8.8.8 okdeb.com +dnssec +multiline
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

Here is what you should have setup with your DNS already.

Namecheap -> Account -> Domains -> DNS -> Advanced DNS

Add New Records
A @ 15.204.113.148
AAAA @ 2604:2dc0:202:300::3645
A mail 15.204.113.148
A mx 15.204.113.148
AAAA mail 2604:2dc0:202:300::3645
AAAA mx 2604:2dc0:202:300::3645
CNAME autoconfig mx.okdeb.com
CNAME autodiscover mx.okdeb.com
CNAME www okdeb.com
TXT @ v=spf1 ip4:15.204.113.148 ip6:2604:2dc0:202:300::3645/64 mx ~all
TXT _dmarc v=DMARC1; p=quarantine; rua=mailto:postmaster@okdeb.com; ruf=mailto:postmaster@okdeb.com; sp=quarantine

In Custom MX Records
MX @ mx.okdeb.com 0
MX @ mail.okdeb.com 10

You will need to add a SV records for Autodiscover
Type Service Protocol Priority Weight Port Target
SRV Record _autodiscover _tcp 5 0 443 mx.okdeb.com

DMARC tells recieving servers what action to take if SPF lookup fails or
doesn't match and where to send reports of failures or problems.

SPF identifies which servers are authorized to send email on the domains behalf,
in DNS the @ means this domain and the domain name is appended to the other
entries unless you put a dot on the end.

Check the spf record ...

nslookup -type=txt okdeb.com
Server: 127.0.0.53
Address: 127.0.0.53#53

Non-authoritative answer:
okdeb.com text = "v=spf1 ip4:15.204.113.148 ip6:2604:2dc0:202:300::3645/64 mx ~all

Check the dmarc record ...

nslookup -type=txt _dmarc.okdeb.com
Server: 127.0.0.53
Address: 127.0.0.53#53

Non-authoritative answer:
_dmarc.okdeb.com text = "v=DMARC1; p=quarantine; rua=mailto:postmaster@okdeb.com; ruf=mailto:postmaster@okdeb.com; sp=quarantine"

You can also check your record validity with mxtoolbox.com
https://mxtoolbox.com/
Enter your domain name and submit

Setup SPF milter to check SPF records for incoming email.

apt install postfix-policyd-spf-python

Integrate policyd-spf with postfix

nano /etc/postfix/master.cf
--- add at the end ---
# spf milter policyd-spf unix - n n - 0 spawn user=policyd-spf argv=/usr/bin/policyd-spf

If there already is a smtpd_recipient_restrictions, add the "check_policy_service"
after the "reject_unauth_destination" or the server will become an open relay.

nano /etc/postfix/main.cf
policyd-spf_time_limit = 3600 smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination, check_policy_service unix:private/policyd-spf

Optional, change the format of the spf header line, leave as default
Header-Type = Recieved-SPF (default)
Header-Type = AR requires Authserv_Id = hostname

nano /etc/postfix-policyd-spf-python/policyd-spf.conf
# For a fully commented sample config file see policyd-spf.conf.commented debugLevel = 1 TestOnly = 1 HELO_reject = Fail Mail_From_reject = Fail PermError_reject = False TempError_Defer = False skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1 # Authserv_Id = mx.okdeb.com # Header_Type = Received-SPF # Header_Type = SPF # Header_Type = AR

systemctl restart postfix dovecot

Check if the unix socket is created in the correct location

ls /var/spool/postfix/private/policyd-spf
/var/spool/postfix/private/policyd-spf

Send a test email and check the raw email source for a line like ...

Check if SPF milter is working, send an email to the new account jack@okdeb.com
and view the source and make sure it contains a line like ...

Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip...
- or -
Authentication-Results: mx.okdeb.com; spf=pass (sender SPF authorized) ...

If you have problems recieving email check with

journalctl -eu postfix
journalctl -eu dovecot


DKIM is a signature that is included in the email, the recieving server looks up
the DKIM public record on DNS does some sort of comparison with the signature in
the email to determine if it is from a valid sender. Configure the email server
to append the DKIM signature, and add the public key the DNS record.

DKIM is supposed to report secure if dnssec is enabled, so make sure dnssec is
enabled. I won't go into setting up a dnssec nameserver here.

apt install opendkim opendkim-tools

adduser postfix opendkim

Configure opendkim - change and add to
nano /etc/opendkim.conf
Canonicalization relaxed/simple On-BadSignature reject BodyLengthDB refile:/etc/mail/bodylengthdb.cfg Mode sv SubDomains no OversignHeaders From Socket local:/var/spool/postfix/opendkim/opendkim.sock # add these lines to end of file TrustAnchorFile /usr/share/dns/root.key #Nameservers 127.0.0.1 # Specify the list of keys KeyTable file:/etc/opendkim/keytable # Match keys and domains. To use regular expressions in the file, use refile: # instead of file: SigningTable refile:/etc/opendkim/signingtable # Match a list of hosts whose messages will be signed. By default, only # localhost is considered as internal host. InternalHosts refile:/etc/opendkim/trustedhosts # Hosts to ignore when verifying signatures ExternalIgnoreList refile:/etc/opendkim/trustedhosts

mkdir -p /etc/dkimkeys
chown -R opendkim:opendkim /etc/dkimkeys
chmod go-rw /etc/dkimkeys

nano /etc/opendkim/signingtable
*@okdeb.com 20250718._domainkey.okdeb.com # *@domain2.com 20250721._domainkey.domain2.com

nano /etc/opendkim/keytable
20250718._domainkey.okdeb.com okdeb.com:20250718:/etc/dkimkeys/okdeb.com/20250718.private # 20250721._domainkey.domain2.com domain2.com:20250721:/etc/dkimkeys/domain2.com/20250718.private

nano /etc/opendkim/trustedhosts
127.0.0.1 ::1 localhost 15.204.113.148 2604:2dc0:202:300::3645 mx.okdeb.com mail.okdeb.com okdeb.com

mkdir -p /etc/dkimkeys/okdeb.com
opendkim-genkey -b 2048 -d okdeb.com -D /etc/dkimkeys/okdeb.com -s 20250718 -v
find /etc/dkimkeys/* -type d -exec chown -R opendkim:postfix {} \;
find /etc/dkimkeys/* -type d -exec chmod 700 {} \;
chmod 600 /etc/dkimkeys/*/*.private

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

echo '.*' > /etc/mail/bodylengthdb.cfg

Publish the public key with your nameserver, again I am using NameCheap, go to
Domains -> Domain -> Manage -> Advanced DNS

Make sure you have enabeled dnssec here

cat /etc/dkimkeys/okdeb.com/*.txt

This will output the key as several lines, you will need to paste this as one line,
there are usually 2 " " sections to remove and also remove the beginning and ending
quote. Then remove the quotes and spaces like ....

"v=DKIM1; h=sha256; k=rsa; " "p=MII...
v=DKIM1; h=sha256; k=rsa; p=MII...

and

...1234" "5678...
...12345678...

add this to your DNS, choose TXT Record and add the selector._domainkey then the
DKIM line all one line ...
20250718._domainkey v=DKIM1; h=sha256; k=rsa; p=MIIBI ... a very long line no spaces

The txt record can take time to propagate but is usually a few minutes.
Check it - it is normal to have a space and quotes in the output here

dig txt 20250718._domainkey.okdeb.com +dnssec +multiline
nslookup -type=txt 20250718._domainkey.okdeb.com

Test the key - in some situations it will say key not secure, it can be
ignored and is usually the result of /etc/dkimkeys directory permissions not
being secure, TrustAnchorFile is not specified in opendkim.conf, dnssec is not
enabled for the domain, using a local authoritative dns server, or dnssec
depends on ntpsec .. eg time problems.

opendkim-testkey -d okdeb.com -s 20250718 -vvv
opendkim-testkey: using default configfile /etc/opendkim.conf
opendkim-testkey: checking key '20250718._domainkey.okdeb.com'
opendkim-testkey: key secure
opendkim-testkey: key OK

Check it with mxtoolbox

https://mxtoolbox.com

Configure openkim socket

mkdir -p /var/spool/postfix/opendkim
chown -R opendkim:postfix /var/spool/postfix/opendkim

nano /etc/default/opendkim
--- change the SOCKET line as follows ---
SOCKET="local:/var/spool/postfix/opendkim/opendkim.sock"

Configure openkim in postfix

nano /etc/postfix/main.cf
--- add these to end of file ---
milter_default_action = accept milter_protocol = 6 smtpd_milters = unix:opendkim/opendkim.sock non_smtpd_milters = $smtpd_milters

systemctl restart opendkim postfix dovecot

Now try sending an email from the jack@okdeb.com account to see if the header has
been signed by opendkim. You should see some lines as follows...

Send a test mail from your mail server, in this case jack@okdeb.com.

The mail should have a header that contains something like this ...
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=okdeb.com;
s=CRHdAnOqqitUaWRuNkHLdIpbgw76; t=1747256382;
bh=6GInUBItXoVjcpY1TOdSfAVLN/R6eU2ujlFwzTrfFtw=;
h=Date:To:From:Subject:From;
b=RdNarAxFLxIVz/D4sMfkK7OTzpQdgzCBX1tN6Z1xiRlPAEtq3Z6PMEbIanKhBB5Zu
BbHUodrOHVuib8hyvoCV0U6sp5Kz0jexiYqgM4R2KQkHrg+nIICfewSj6PHtfwMy8i
Tq67eaEv7jr7ShwRkZaQcqNHaZHyjFUmXlMy9y82F0mH5f2vVw+bZ2zbVMMVW3AYEv
f/espHlUSbKbQsuLxEj/TTHcNGQ7YD3Moji7PL7e57vkQTS8r4QyFh3OkI8Jc62W8E
9Rj9BA0kZ6lEFcH89wvi/BwmJowspeOcLYX3OoQtJ2UeZhrvpXg8kZAlytXJap+1dv
xdjzbn58GIq4g==

Authentication-Results: mail.otherdomain.com (amavisd-new); dkim=pass (2048-bit key)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=okdeb.com;
s=20250718; t=1752857804;
bh=PU2XIErWsXvhvt1W96ntPWZ2VImjVZ3vBY2T/A+wA3A=;
h=Date:To:From:Subject:From;
b=bbdnwmFL4iZDHbP+u9feWSwHMbaqp7N+Stq7lF09ePLDjGzegxMR5Z2p4hrzMUHB/
...
m/KD8R8a5DOGQ==

ARC-Authentication-Results: i=1; mx.google.com;
dkim=pass header.i=@okdeb.com header.s=20250718 header.b=O2Rb3Ztg;
spf=pass (google.com: domain of jack@okdeb.com designates 15.204.113.148 as permitted sender) smtp.mailfrom=jack@okdeb.com;
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=okdeb.com


Install and Configure DMARC
We do not need reports so select dbconfig-common: No

apt install opendmarc
dbconfig-common: No

nano /etc/opendmarc.conf
AuthservID OpenDMARC PidFile /run/opendmarc/opendmarc.pid PublicSuffixList /usr/share/publicsuffix/public_suffix_list.dat RejectFailures true Socket local:/var/spool/postfix/opendmarc/opendmarc.sock Syslog true TrustedAuthservIDs localhost,mx.okdeb.com,mail.okdeb.com,okdeb.com UMask 0002 UserID opendmarc IgnoreAuthenticatedClients true RequiredHeaders true SPFSelfValidate true IgnoreHosts /etc/opendmarc/ignore.hosts

Make the ignore.hosts file
nano /etc/opendmarc/ignore.hosts
127.0.0.1 147.135.65.9 2604:2dc0:202:300::3645 ::1 localhost

Create and set permissions for opendmarc socket
mkdir -p /var/spool/postfix/opendmarc
chown -R opendmarc:opendmarc /var/spool/postfix/opendmarc
chmod -R 750 /var/spool/postfix/opendmarc
adduser postfix opendmarc

systemctl enable opendmarc
systemctl restart opendmarc
systemctl status opendmarc

Integrate DMARC into Postfix - change the line below to include the opendmarc socket
Do NOT use smtpd_milters = local:opendkim/opendkim.sock
Use smtpd_milters = unix:opendkim/opendkim.sock

nano /etc/postfix/main.cf
smtpd_milters = unix:opendkim/opendkim.sock,unix:opendmarc/opendmarc.sock

systemctl restart opendkim opendmarc postfix dovecot

If opendmarc opendkim or spf daemon fails to restart or usually it will give
the error restarting too quickly it is a configuration, path, or permissions
error. It can also be a module is not installed eg. I had a module not loading
error and though I had written it up in this howto I had not actually run the
command to install the module: apt get dovecot-mysql. Check permissions on the
socket files in /var/spool/postfix, this will usually show up as unable to
create socket or permissions error, or just can't restart. In another case I
used /etc/opendkim/keys then switched to /etc/dkimkeys and made the change in
this howto but not in the actual opendkim.conf ... so opendkim happily failed
to restart with a restarting too quickly error, no indication of what was the
actual problem. So went I went through the configuration files very carefully
till I found the mistake.

Send yourelf an email and check the mail header for DMARC results.

Authentication-Results: OpenDMARC; dmarc=pass (p=quarantine dis=none) header.from ...

Send an email from another account to your new mail server to check DMARC

grep dmarc /var/log/maillog
May 15 12:15:48 okdeb opendmarc[96573]: A853C2BE2D: domain2.net pass

As suggested by linuxbabe, a great tool to test the spammyness if your emails is
https://www.mail-tester.com

Next up Roundcube Webmail

05 PostfixAdmin <- Intro -> 07 RoundCube WebMail