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 # ##################################