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

05 PostfixAdmin <- Intro -> 07 RoundCube WebMail

We believe in data independence, and support others who want data independence.
This tutorial is complete 2025-08-14 except there is no page for setting up postscreen.

This is version 2 and everthing works.

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

To improve mail delivery to other email servers we need to setup DMARC, SPF, and DKIM records so recipient mail servers can identify we are allowed to send emails for our domain. 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.

I tried 2 versions of opendkim with unbound dnssec and chronyd ntp security but opendkim-testkey reports 'key not secure' unless opendkim.conf with specified explicitly. Dig shows dnssec is working with the ad flag.

Be careful when setting up the ipfw firewall. Initially I did not add rules to allow ipv6 DNS queries and postfix would report google ipv6 addresses as unknown and block the emails. It looked like an unbound issue or that postfix wasn't using unbound and pkg wasn't able to always connect to the freebsd mirror. Also the log showed IPFW was denying more than normal connection to port 53. Adding the correct firewall rules and adding val-permissive-mode: yes to unbound fixed both problems (though not sure if the permissive had anything to do with it.

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.

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

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

Add New Records
A @ 147.135.37.135
AAAA @ 2604:2dc0:200:187::1
A mail 147.135.37.135
A mx 147.135.37.135
AAAA mail 2604:2dc0:200:187::1
AAAA mx 2604:2dc0:200:187::1
CNAME autoconfig mail.okbsd.com
CNAME autodiscover mail.okbsd.com
CNAME www okbsd.com
TXT @ v=spf1 ip4:147.135.37.135 ip6:2604:2dc0:200:187::1 mx ~all
TXT _dmarc v=DMARC1; p=quarantine; rua=mailto:postmaster@okbsd.com; ruf=mailto:postmaster@okbsd.com; sp=quarantine

In Custom MX Records
MX @ smtp.okbsd.com 0
MX @ mail.okbsd.com 10

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 in the DNS ...

nslookup -type=txt okbsd.com
okbsd.com text = "v=spf1 ip4:147.135.37.135 ip6:2604:2dc0:200:187::1 mx ~all"

nslookup -type=txt _dmarc.okbsd.com
_dmarc.okbsd.com text = "v=DMARC1; p=quarantine; rua=mailto:postmaster@okbsd.com; ruf=mailto:postmaster@okbsd.com; sp=quarantine"

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

Configure the SPF policy agent on FreeBSD to check SPF records on incoming mail.

pkg install py311-spf-engine

Automatically starting pyspf-milter at boot time.

You will need to add a policyd-spf user and group, user 114 is used on Debian and was available on my system. Here is a nice oneliner to add user and group

pw useradd -n policyd-spf -d /nonexistent -s /usr/sbin/nologin -u 114 -i 114

check it
grep policyd-spf /etc/passwd /etc/group
/etc/passwd:policyd-spf:*:114:114:User &:/nonexistent:/usr/sbin/nologin
/etc/group:policyd-spf:*:114:

sysrc pyspf_milter_enable="YES"

nano /usr/local/etc/postfix/main.cf
--- add to end of file ---
policyd-spf_time_limit = 3600 smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination, check_policy_service unix:private/policyd-spf

nano /usr/local/etc/postfix/master.cf
smtp inet n - n - - smtpd -o milter_macro_daemon_name=VERIFYING

nano /usr/local/etc/postfix/master.cf
--- add to end of file ---
policyd-spf unix - n n - 0 spawn user=policyd-spf argv=/usr/local/bin/policyd-spf

You can change the format of the spf header line

Header-Type = Recieved-SP (default)
Header-Type = AR requires Authserv_Id = hostname

Add MacroList daemon_name|VERIFYING

nano /usr/local/etc/python-policyd-spf/policyd-spf.conf
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 MacroList daemon_name|VERIFYING # Header_Type default is Recieved-SPF # Header_Type = Received-SPF # Header_Type = SPF # Header_Type AR requires Authserv_Id #Authserv_Id = smtp.okbsd.com #Header_Type = AR

service postfix restart
service postfix status

service dovecot restart
service dovecot status

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 to see if spf passed.

If you chose Header_Type = AR

Authentication-Results: smtp.okbsd.com; spf=pass (sender SPF authorized)

For Header_Type = Received-SPF (default)

Received-SPF: Pass (mailfrom) identity=mailfrom

If you have problems recieving email check /var/log/maillog and send yourself a test email ...

tail -f /var/log/maillog

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.

Install ca_root_nss - not sure why this is required but there was a note in the forums

pkg install ca_root_nss

Update the root.key file with unbound, this could be run from a cron.

unbound-anchor
unbound-anchor -v
/usr/local/etc/unbound/root.key has content
success: the anchor is ok

pkg install opendkim
pw useradd -c "" -n opendkim -d /var/run/opendkim -s /usr/sbin/nologin -u 118 -i 118
pw groupmod opendkim -m postfix

Enable opendkim in rc.conf and change default settings
nano /etc/rc.conf
milteropendkim_enable="YES" milteropendkim_cfgfile="/usr/local/etc/mail/opendkim.conf" milteropendkim_uid="opendkim" milteropendkim_gid="opendkim" milteropendkim_socket="local:/var/spool/postfix/opendkim/opendkim.sock" milteropendkim_socket_perms="0770"

nano /usr/local/etc/mail/opendkim.conf
BodyLengthDB refile:/usr/local/etc/mail/bodylengthdb.cfg Canonicalization relaxed/simple #Domain example.com #KeyFile /var/db/dkim/example.private #LogWhy no Mode sv On-BadSignature reject OversignHeaders From PidFile /var/run/opendkim/opendkim.pid Socket local:/var/spool/postfix/opendkim/opendkim.sock SubDomains No Syslog Yes SyslogSuccess Yes TrustAnchorFile /usr/local/etc/unbound/root.key UMask 007 UserID opendkim KeyTable file:/usr/local/etc/mail/keytable SigningTable refile:/usr/local/etc/mail/signingtable InternalHosts refile:/usr/local/etc/mail/trustedhosts ExternalIgnoreList refile:/usr/local/etc/mail/trustedhosts

nano /usr/local/etc/mail/bodylengthdb.cfg
.*

Create socket directory
mkdir /var/spool/postfix/opendkim
chown opendkim:opendkim /var/spool/postfix/opendkim
chmod 755 /var/spool/postfix/opendkim

Create DKIM keys.

mkdir -p /usr/local/etc/mail/keys
mkdir -p /usr/local/etc/mail/keys/okbsd.com
opendkim-genkey -b 2048 -d okbsd.com -D /usr/local/etc/mail/keys/okbsd.com -s 20250807 -v

chown -R opendkim:opendkim /usr/local/etc/mail/keys
find /usr/local/etc/mail/keys -type d -exec chmod 500 {} \;
find /usr/local/etc/mail/keys -type f -exec chmod 400 {} \;

Setup signingtable keytable and trustedhosts

nano /usr/local/etc/mail/signingtable
*@okbsd.com 20250807._domainkey.okbsd.com

nano /usr/local/etc/mail/keytable
20250807._domainkey.okbsd.com okbsd.com:20250807:/usr/local/etc/mail/keys/okbsd.com/20250807.private

nano /usr/local/etc/mail/trustedhosts
127.0.0.1 147.135.37.135 2604:2dc0:200:187::1

Namecheap Advanced DNS -> Toggle On DNS Sec

Copy the results to your DNS as a TXT RECORD

cat /usr/local/etc/mail/keys/okbsd.com/*.txt

Remove 2 sections with " <spaces> " before submitting the DNS TXT Record
TXT 20250807._domainkey v=DKIM1; k=rsa; p=MIIBIjA...DAQAB

nslookup -type=txt 20250807._domainkey.okbsd.com
20250807._domainkey.okbsd.com text = "v=DKIM1; k=rsa; p=MIIBIjA...DAQAB"

Go to mxtoolbox.com and do a DKIM lookup.
20250807._domainkey.okbsd.com

Start opendkim and check that it is running

service milter-opendkim start
service milter-opendkim status
milteropendkim is running as pid 3637.

service postfix restart
service dovecot restart

Test the dkim key - I was not able to get key secure without -x path

opendkim-testkey -d okbsd.com -s 20250807 -vvv -x /usr/local/etc/mail/opendkim.conf
opendkim-testkey: checking key '20250807._domainkey.okbsd.com'
opendkim-testkey: key secure
opendkim-testkey: key OK

Add OpenDKIM to Postfix - add to end of file

nano /usr/local/etc/postfix/main.cf
# Milter configuration milter_default_action = accept milter_protocol = 6 smtpd_milters = unix:opendkim/opendkim.sock non_smtpd_milters = $smtpd_milters

service postfix restart
service milter-opendkim restart
service postfix status
service milter-opendkim status

Send a test mail from jack@okbsd.com to an external email address.

The mail should have a header that contains something like this ...

Authentication-Results: mx.domain.tld (amavisd-new); dkim=pass (2048-bit key)
header.d=okbsd.com
...
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=okbsd.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==

Install and Configure DMARC

pkg install opendmarc

cp /usr/local/etc/mail/opendmarc.conf.sample /usr/local/etc/mail/opendmarc.conf

pw useradd -c "" -n opendmarc -d /nonexistent -s /usr/sbin/nologin -u 119 -i 119
pw groupmod opendmarc -m postfix

Create opendmarc socket directory

mkdir /var/spool/postfix/opendmarc
chown -R opendmarc:opendmarc /var/spool/postfix/opendmarc
chmod -R 770 /var/spool/postfix/opendmarc

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

Make the ignore.hosts files - I avoided making an extra directory with only 1 files at /usr/local/etc/opendmarc/ignore.hosts and put it under mail.

nano /usr/local/etc/mail/ignore.hosts
127.0.0.1 ::1 147.135.37.135 2604:2dc0:200:187::1 localhost

It is interesting to note that for both dkim and dmarc sockets, permissions can't be set in the configuration file but must be set when the service starts in the /usr/local/etc/rc.d scripts. Sometimes these variables can be set in /etc/rc.conf. See below where I had to modify the opendmarc script.

nano /etc/rc.conf
opendmarc_enable="YES" opendmarc_runas="opendmarc:opendmarc" opendmarc_socketspec="unix:/var/spool/postfix/opendmarc/opendmarc.sock" opendmarc_socketperms="0700"

I use unix or local sockets when possible as they are only accessible from the same machine, should be more secure, and are more efficient that an INET solution. As long as you don't need to use the milter from non-local address.

Possibly Optional: The socket permissions on my first install weren't set right so I forced it by adding these lines at the end of the rc.d file.

This was not necessary for my second install on FreeBSD 14.3.

nano /usr/local/etc/rc.d/opendmarc
opendmarc_socketperms=${opendmarc_socketperms-"0770"} if [ -S ${opendmarc_socketspec##local:} ] ; then chmod -R ${opendmarc_socketperms} ${opendmarc_socketspec##local:} > /dev/null 2>&1 elif [ -S ${opendmarc_socketspec##unix:} ] ; then chmod -R ${opendmarc_socketperms} ${opendmarc_socketspec##unix:} > /dev/null 2>&1 fi

DMARC Postfix Integration - change the line below to include the opendmarc socket

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

service opendmarc start
service opendmarc status
service postfix restart

Send an email from another account to jack@okbsd.com to check DMARC

grep dmarc /var/log/maillog
Aug 7 14:50:08 okbsd opendmarc[7033]: 8D58038354: otherdomain.tls pass

Check the mail header

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

##################################
# Next we will install Roundcube #
##################################

05 PostfixAdmin <- Intro -> 07 RoundCube WebMail