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 partially complete 2025-08-07
This is version 2 and everthing works up to and including Roundcube.
#######################################
# 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.
# The socket permissions aren't set right for this configuation so I forced it
# by adding these lines at the end of the rc.d file. I have suggested upgrades
# to the rc.d startup script for opendmarc on FreeBSD. This might not be the
# best way to do this but it works. I add these lines to the end of the file ...
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,unix:/var/run/pyspf-milter/pyspf-milter.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