Setup Email Server From Scratch On FreeBSD - 01 Server Setup
This tutorial is partially complete 2025-05-14
Postfix, Dovecot, & PostfixAdmin work with MySQL and virtual accounts
can be created with PostfixAdmin and used with an email client. SPF, OpenDKIM,
and DMARC milters, Roundcube with sieve filters and password plugins all work.
Roundcube kolab calendar works (2025-05-20)
################
# Server Setup #
################
Go to your prefered registrar or use an existing domainname and choose a
hostname something like mail.domain.com or mx.domain.com. DNS and MX records
setup will be done after determining the new mail servers IP addresses.
The following assumes the domain name is okbsd.com so change it to your domain
name and change the IPV4 and IPV6 addresses to your own IP addresses. Also
change 'user' to your username where needed.
Setup a server with a hosting provider of choice. I have used Kamatera, One
Provider, MS Azure, and OVHcloud - Vint Hill.
An important issue is that sometimes hosting providers have IP address ranges
which are on a blacklist and some email servers may reject mail from servers
using these ranges. In particular check with a dns checker like dnschecker.org
to to see if the IP you get assigned is on UCEPROTECTL3. It is pretty difficult
to get off a IP range blacklist and the best solution is to ask the hosting
provider to give you a different IP ... and ask them to monitor their customers
email habits more rigorously. That being said even if you are on one of these IP
range spam lists it may not be a big issue. I tested with okbsd.com which was on
such one such list and google accepted the email while outlook accepted it but
put it in the junk folder. Listing it as not junk will improve your domain's
reputation as long as you're not doing mass mailing campaigns or sending spam.
I recently setup a Debian 12 server on Kamatera in Dallas with 2 availability
cores 4GB RAM and 40GB disk space and network speed and performance are good for
$19 a month.
For this FreeBSD mail stack setup, I decided to try OVH Cloud in Vint Hill SYS-1
with 6 cores 12 threads 32GB RAM 512 GB Raid 1 SSD's. This has more than enough
resources to run mail and other services. There is a noticable 2-3 second lag
logging in with ssh, and ping times to 8.8.8.8 and some other sites are
significantly slower than my other hosts. Hosting cost is $33 for setup, plus
$33 plus tax per month. Before continuing this guide I decided to re-install
FreeBDD, but the reinstall failed. I submitted a ticket in the evening and by
morning they had reset and the IMPI/KVM console was at the first page of
FreeBSD installation again.
Install on OVH was a little more tricky since they don't support FreeBSD by
default and the IPMI Java applet wasn't available in my case.
The other option is to use the OVH installer and with the bring your own custom
image option. Usually I use the DVD image since includes ports but download
would be faster if using the disk1 image.
Installation from an OVHcloud template
Type of OS: Custom
CUSTOM(2): Bring Your Own Image
-> Next
Image URL: https://download.freebsd.org/releases/amd64/amd64/ISO-IMAGES/14.2/FreeBSD-14.2-RELEASE-amd64-disc1.iso
Image Type: raw
Path of the EFI bootloader from the OS installed on the server: /boot/efi/efi/boot/bootx64.efi
A few minutes later the machine was provisioned and I switched to the IPMI/KVM
tabs and one of the options there gave me console access with the FreeBSD
installer start screen.
Select Install
Continue with Default Keymap
Set Hostname : okbsd.com
Distribution Select: Use the defaults plus src and ports.
If you need kernel sources and ports can be installed later.
Manual paritioning isn't needed since with ZFS you can change the partitions
later but I bumped the swap size to 4g. Another option is to choose UFS with
or without separate partitions if you never plan to try and increase the disk
size.
Partitioning: Auto (ZFS)
Pool Type: default stripe
ZFS Configuration: you can change swap to 4g if you want
>>> Install
Change password for root
New Password:
IPv4: DHCP
Search: okbsd.com
DNS1: 9.9.9.9
DNS2: 1.1.1.1
Select Timezone ...
Enable: unbound sshd ntp ntp_sync_on_start
System Hardening: I skip all of these though random pid might be an idea
Add User Accounts: user
Full Name: Regular User
If needed to use su - root add the user to group wheel
Invite user into other groups? wheel
Use defaults then set a password of your choosing
It is very important to confirm you know the root password before leaving the
installer. The IPMI/KVM access is a poor interface as it is almost impossible to
cut and paste any command into the IPMI console. While you can get in later if
you know the root password with IPMI it is better to setup a user account with
sudo or confirm root access via ssh before finishing the installer.
Choose EXIT THE INSTALLER
YOU WILL HAVE ONE LAST CHANCE TO OPEN A SHELL - OPEN A SHELL
Manual Configuration : YES
The shell at the end of the FreeBSD install is a chroot'ed shell with root privs
so it is a good time to run some commands to make sure you can get in with ssh
and know your passwords.
# SV - To change root password if needed
root@okbsd.com:~# passwd
# SV - To reset user password if needed
root@okbsd.com:~# passwd user
# SV - Install some programs
pkg update
pkg upgrade
pkg install bash bind-tools sudo nano dnsmasq
# Some commands can't be run in this chrooted environment so if you get
# something like ...
# nslookup google.com
/bin/sh nslookup: not found
# and you'll need to exit the installer and login as root again with IPMI/KVM console.
exit
or
shutdown -r now
# Get as list of shells
cat /etc/shells
# Change your shell with chsh but this is a vi editor so move around with the
# arrow keys, use x to delete in command mode, use i to insert before the cursor,
# x to delete a character
# i insert text before the cursor
# a append text after the cursor
# Escape to exit insert or append mode
# Escape Escape :wq to write quit
# Excape Escape :q! to quit without saving and start over
chsh
Shell: /usr/local/bin/bash
# Change shell for user also
chsh user
Shell: /usr/local/bin/bash
# open a bash shell
# /usr/local/bin/bash
# SV - Create sudo group and setup sudo access for user
pw group add sudo
# To create a use account use ... add to group wheel if you want the user to be
# able to su - root ...
adduser
# Then add your username to either group sudo or wheel
pw group mod sudo -m user
- or -
# If you want to su to root you will need to set user in group wheel as well
pw group mod wheel -m user
# You will most likely need to exit the installer and reboot before the next
# set of commands, if you haven't done it already exit the installer now.
exit
[ Reboot ]
# Edit sudoers file - this is the vi editor use x to delete and 'Esc Esc :wq'
# to write and quit. If you get an error '/bin/sh visudo not found' <reboot>.
# and log back in with IPMI/KVM shell as root.
visudo
# Find the first line starting with # %wheel and type x twice, the line
# should look like this ...
%wheel ALL=(ALL:ALL) ALL
# Find the line starting with # %sudo and type x twice
%sudo ALL=(ALL:ALL) ALL
# Hit Escape Escape :wq
# Connecting with ssh to the server
Connecting with ssh to root may not be allowed and will say failed password if
the password is wrong or AllowRootLogin is not set to yes. So if ssh login
fails, use the user account created during install or create a user and set the
user password from the temporary install shell.
# Set ssh PermitRootLogin to yes, later we'll change to prohibit-password
nano /etc/ssh/sshd_config
# PermitRootLogin no
PermitRootLogin yes
# PermitRootLogin prohibit-password
service sshd restart
# Confirm the IP_ADDR of the new server look for inet <your_address>
ifconfig -a
inet 147.135.65.97 netmask 0xffffff00 broadcast 147.135.65.255
# PC - Open a terminal or command shell and login as root with ssh
ssh root@147.135.65.97
<rootpassword>
# Setup root ssh keys on the server
[root@okbsd ~]# ssh-keygen
<enter><enter><enter>
[root@okbsd ~]# exit
# SSH allows cut and paste between terminal windows so IPMI/KVM can be closed.
# PC - Attempt to reconnect with username and password and confirm user and root access
ssh user@147.135.65.97
<userpassword>
# Setup user ssh keys on the server
user@okbsd.com:~$ ssh-keygen
<enter><enter><enter>
# Confirm sudo works. Sudo allows privileged commands to be run without using sudo.
user@okbsd.com:~$ sudo <cmd>
<userpassword>
# Confirm su works. Su allows login as root so commands can be run without sudo.
user@okbsd.com:~$ su - root
<rootpassword>
root@okbsd.com:~#
# Setup local ssh keys on *nix, Mac, or Windows computer.
# PC - For *nix and Mac use terminal and run ...
ssh-keygen
<enter><enter><enter>
cat ~/.ssh/id_rsa.pub
cd ~/.ssh
# PC - For Windows use cmd.exe and run (profile is your windows user profile) ...
ssh-keygen -t rsa -b 2048
<enter><enter><enter>
cd C:\Users\profile\.ssh\
more id_rsa.pub
# PC - For *nix, Mac, and Windows continue with...
scp id_rsa.pub user@147.135.65.97 /home/user/importedkey.pub
ssh user@147.135.65.97
cat ~/importedkey.pub >> ~/.ssh/authorized_keys
# If scp doesn't work copy and paste the key from the local machine ~/.ssh/id_rsa.pub
# to the new server and paste it with nano as a single line into ~/.ssh/authorized_keys
# PC - Copy from *nix
cat id_rsa.pub
# PC - Copy from Windows
more id_rsa.pub
# SV - Paste to FreeBSD server
nano ~/.ssh/authorized_keys
<paste local pc output all as one line>
# SV - Add the key to user and roots authorized_keys file as well
su - root
cat /home/user/importedkey.pub >> /root/.ssh/authorized_keys
cat /home/user/importedkey.pub >> /home/user/.ssh/authorized_keys
chown -R user:user /home/user/.ssh
# SV - Restart ssh and exit to test passwordless access...
service sshd restart
# PC - Test access using ssh keys no passwords
ssh user@147.135.65.97
<no_password_required>
user@okbsd.com:~$ su - root
<rootpassword>
root@okbsd.com:~# exit
user@okbsd.com:~$ exit
ssh root@147.135.65.97
<no_password_required>
root@okbsd.com:~#
# SV - Remove the copy of the public key
rm /home/user/importedkey.pub
# Once you can login as root without a password, secure root so password login
# is denied and only the ssh key is allowed.
nano /etc/ssh/sshd_config
#PermitRootLogin no
#PermitRootLogin yes
PermitRootLogin prohibit-password
service sshd restart
# Open a separate terminal leaving the first one open to test passwordless access
# to root still works. Test that user passwordless also works, though password is
# still a fallback for the regular user.
ssh root@147.135.65.97
<no password required>
root@okbsd.com:~#
ssh user@147.135.65.97
<no password required>
user@okbsd.com:~$
#################################
# Setup Hostname and Networking #
#################################
# Change autoboot delay - default is 10 seconds
nano /boot/loader.conf
autoboot_delay="2"
# Edit rc.conf and setup hostname and network interfaces
nano /etc/rc.conf
hostname="okbsd.com"
# comment out the default inet6 line
# ifconfig_ix0_ipv6="inet6 accept_rtadv"
# Added the static IPV6 from OVH but I had to modify the prefixlen to get it to work
# since OVH suggested prefixlen 64 but that would put the router on a different subnet
# ipv6 is 128 bits with 16 bits per set, so 2 hexidecimal is 8 bits and 64 - 8 = 56
ifconfig_ix0_ipv6="inet6 2604:2dc0:100:1261::10 prefixlen 56"
ipv6_defaultrouter="2604:2dc0:100:12ff:ff:ff:ff:ff"
#local_unbound_enable="YES"
unbound_enable="YES"
# dnsmasq_enable="YES"
# Setup hosts file
nano /etc/hosts
147.135.65.97 okbsd.com mx.okbsd.com mail.okbsd.com okbsd
2604:2dc0:100:1261::10 okbsd.com mx.okbsd.com mail.okbsd.com okbsd
# --- check your resolver
nano /etc/resolv.conf
search okbsd.com
nameserver 127.0.0.1
nameserver 9.9.9.9
# Setup dnsmasq
cd /etc
ln -s /usr/local/etc/dnsmasq.conf
# --- make the following changes to dnsmasq.conf
nano /etc/dnsmasq.conf
no-resolv
expand-hosts
domain=okbsd.com
server=9.9.9.9
server=8.8.4.4
service dnsmasq restart
[root@okbsd /etc]# nslookup okbsd.com localhost
Server: localhost
Address: ::1#53
Non-authoritative answer:
Name: okbsd.com
Address: 147.135.65.97
Name: okbsd.com
Address: 2604:2dc0:100:1261::10
# Check google's answer
nslookup okbsd.com 8.8.8.8
Server: 8.8.8.8
Address: 8.8.8.8#53
Non-authoritative answer:
Name: okbsd.com
Address: 162.255.119.169
# You can see that this is not my IP address so we need to update the real NS
# servers for the domain, I use Namecheap as my registar and DNS is free so I
# don't run my own public name servers. We need to add some A records and mx
# records to Namecheap's DNS. The following assumes the domain is okbsd.com and
# the IP ADDRESS for my OVH Cloud Dedicated Servers so change to your own domain
# name and IP addresses.
Namecheap -> Account -> Domains -> DNS -> Advanced DNS
# Remove the redirect record.
# Remove the www CNAME record.
# Add New Records
A @ 147.135.65.97
AAAA @ 2604:2dc0:100:1261::10
A mail 147.135.65.97
A mx 147.135.65.97
AAAA mail 2604:2dc0:100:1261::10
AAAA mx 2604:2dc0:100:1261::10
# Add the www CNAME record ...
CNAME www okbsd.com
# Add the autoconfig and autodiscover CNAME records ...
CNAME autoconfig mx.okbsd.com
CNAME autodiscover mx.okbsd.com
# Do not use CNAME, use A and AAAA records for mx, mail exchange hosts. You
# could have added another ipv6 address in rc.conf as an alias and then use one
# for mx and one for mail but this isn't necessary. We can add dmarc and spf and
# dmarc records now and will create admin@147.135.65.97 later.
# Please do not add the following 4 records unless you are setting up a mail
# server with the mailbox specified. Use any valid email address for the rua and
# ruf records but RFC guidelines require a postmaster@domain.tld mailbox or
# alias so it is a logical choice.
# ADD MX Records - scroll down in namecheap - change mx forwarding to custom mx
MX @ mx.okbsd.com 0
MX @ mail.okbsd.com 10
# ADD DMARC and SPF TXT Records
TXT @ v=spf1 ip4:147.135.65.97 ip6:2604:2dc0:100:1261::10/64 mx ~all
TXT _dmarc v=DMARC1; p=quarantine; rua=mailto:postmaster@okbsd.com; ruf=mailto:postmaster@okbsd.com; sp=quarantine
# Modify bash shell settings for FreeBSD or Debian and create a personal bin directory
ssh user@147.135.65.97
mkdir ~/bin
echo $PATH
# If PATH doesn't have ~/bin eg /root/bin or /home/user/bin add ONE of the following
# lines to .profile on FreeBSD or to .bashrc on Debian as below, change user as
# appropriate. If your PATH already includes ~/bin don't add it again!
nano ~/.profile
# ADD ONLY ONE IF NEEDED
export PATH="$PATH:$HOME/bin"
export PATH="$PATH:/home/user/bin"
export PATH="$PATH:/root/bin"
export PATH="$PATH:~/bin"
# LSCOLORS is almost unreadable with some terminal programs but comes out nicely
# with Xterm or Xterm-color.
# If you manage many servers which may by default have same short hostname like
# 'pbx.domain.com' change PS1 so as to easily identify the server with the full
# hostname, PS1='\u@$(hostname -f):\w\$ '
# --- FreeBSD
nano ~/.profile
export PATH="$PATH:~/bin"
EDITOR=/usr/local/bin/nano
export EDITOR
export PS1='\u@$(hostname -f):\w\$ '
export LSCOLORS="ExGxFxdxCxegDxabagacad"
export XTERM_LOCALE="en_US.UTF-8"
export LANG="en_US.UTF-8"
# my aliases
alias rm='rm -i'
alias ls-a='/bin/ls -aGF'
alias ls-l='/bin/ls -lhGF'
alias lsl='/bin/ls -lhIGF'
alias ls='/bin/ls -IGF'
# Debian - root doesn't read ~/.bashrc so this is a work around
nano /etc/bash.bashrc
# load roots bash and aliases on Debian
if [ $EUID -eq 0 ]; then
source "${HOME}/.bashrc"
fi
# Debian - add color to .bashrc
nano .bashrc
export PATH="$PATH:~/bin"
export LS_OPTIONS='--color=auto'
eval "$(dircolors)"
# aliases
alias ls='ls $LS_OPTIONS'
alias rm='rm -i'
# Test your colors
cd ~
mkdir Downloads
ls
# Repeat the above .profile setup as the user and I comment out the fortune line
su - user
nano .profile
.....
# Firewall Setup - Once you start enabing services and adding DNS entries random addresses will start
# hitting your server trying to find a way in for nefarious purposes, so you will need a firewall. On
# FreeBSD I recommend a kernel based firewall called ipfw. I set ipfw default to accept so I won't get
# locked out if there is a script error. The last rule in the script is a global deny all. I have used ipfw
# for nat so I usually enable it. When making changes copy the script to a temporary file and test that
# first before copying it to the active script used at boot.
# I put the rules here and so might reveal some vulnerabilities but I'll eventually scrap this machine and
# setup a new one for tutorial version 2.
nano /boot/loader.conf
# ---
ipfw_load="YES"
ipfw_nat_load="YES"
net.inet.ip.fw.default_to_accept="1"
# ---
nano /etc/rc.conf
# ---
firewall_type="open" # Firewall type (see /etc/rc.firewall)
firewall_script="/etc/ipfw.rules"
firewall_enable="YES"
firewall_logging="YES" # Set to YES to enable events logging
gateway_enable="YES"
firewall_nat_enable="YES"
firewall_nat_interface="ix0"
ipv6_firewall_enable="YES"
ipv6_firewall_type="open" # see rc.firewall6 for what goes here
ipv6_firewall_quiet="NO"
ipv6_firewall_logging="YES"
ipv6_firewall_script=""
# ---
nano /etc/ipfw.rules
# ---
#!/bin/sh
ipfw -q -f flush
fwcmd="ipfw -q"
oif="ix0"
ip="147.135.65.97"
net="147.135.65.97/24"
gateway="147.135.65.254"
mask="255.255.255.0"
ip6="2604:2dc0:c00:12bb::10"
net6="2604:2dc0:c00:12aa::/56"
gateway6="2604:2dc0:100:12aa::1"
${fwcmd} add allow ip from any to any via lo0
${fwcmd} add allow ipv6 from any to any via lo0
${fwcmd} add deny log ip from any to 127.0.0.0/8
${fwcmd} add deny log ip from 127.0.0.0/8 to any
# deny ipv6 to local if not lo0 above
${fwcmd} add deny log ipv6 from any to ::1
${fwcmd} add deny log ipv6 from ::1 to any
# Test - Global Allow
# ${fwcmd} add allow all from any to any
# Test - SSH TEMP
# ${fwcmd} add allow tcp from any to me 22 in via ${oif}
# You need the link-local addresses for IPv6's equivalent of ARP, which is
# done with ICMP and uses link-local addresses. Removing the link-local
# addresses would be similar to blocking ARP on IPv4, it's going to stop
# all communication.
${fwcmd} add allow ipv6-icmp from :: to ff02::/16
${fwcmd} add allow ipv6-icmp from fe80::/10 to fe80::/10
${fwcmd} add allow ipv6-icmp from any to any icmp6types 128,133,134,135,136,137
# deny all other ipv6 link local connections
${fwcmd} add deny log ipv6 from fe80::/10 to any
${fwcmd} add deny log ipv6 from any to fe80::/10
# Deny IP fragments
${fwcmd} add deny all from any to any frag in via ${oif}
# Allow IP fragments / Reassemble inbound packets
# ${fwcmd} add allow all from any to any frag in via ${oif}
# ${fwcmd} add reass all from any to any in
# Allow limited broadcast traffic from my own net.
${fwcmd} add pass all from ${net} to 255.255.255.255
# PING6 - allow all ipv6 ping
${fwcmd} add allow ipv6-icmp from any to any
# Stateful Outbound DNS - only keep state for these
# Stateful rules are needed to resolve ipv6 mail server hostname
# For better preformance use as few stateful rules as possible
${fwcmd} add allow ip from me to any proto udp dst-port 53 keep-state
${fwcmd} add allow ip from me to any proto udp src-port 53 keep-state
# Stateful Outbound Rules - OUT IPV4:IPV6
# Enable these instead if if the stateless rules below don't work
# ${fwcmd} add allow tcp from me to any setup keep-state
# ${fwcmd} add allow udp from me to any keep-state
# ${fwcmd} add allow icmp from me to any keep-state
# Stateless Inbound Rule - for high performance servers
${fwcmd} add allow tcp from any to me 25,80,143,443,465,587,993 in via ${oif}
# Stateless Outbound Rule - for high performance servers
${fwcmd} add allow tcp from me to any out via ${oif}
# Stateful Allow Established - IN IPV4:IPV6 - including DNS
${fwcmd} add allow ip from any to any established
# LAN6 - deny ssh allow other
${fwcmd} add deny log tcp from ${net6} to ${net6} 22 in via ${oif}
${fwcmd} add allow tcp from ${net6} to ${net6}
# LAN4 - deny ssh allow other
${fwcmd} add deny log tcp from ${net} to ${net} 22 in via ${oif}
${fwcmd} add allow tcp from ${net} to ${net}
# openvpn all allow / this is not needed / allow only trusted nets
# ${fwcmd} add pass tcp from any to me 1194 in via ${oif}
# Personal Rules
# Home Internet Example
${fwcmd} add pass tcp from 172.25.25.10:255.255.0.0 to me 22 in via ${oif}
${fwcmd} add allow ipv6 from ff82:a5ab:2abc:15::/64 to me6 22 in via ${oif}
${fwcmd} add allow ipv6 from ff82:a790:b700::/40 to me6 22 in via ${oif}
${fwcmd} add allow ipv6 from ff82:a791::/32 to me6 22 in via ${oif}
# Other Server
${fwcmd} add pass all from 192.168.77.77 to me in via ${oif}
${fwcmd} add allow ipv6 from ff75:a791:abcd:dcba:1234::1 to me6 in via ${oif}
# Office Network SSH PING TRACEROUTE - no ipv6
${fwcmd} add pass tcp from 10.77.21.0/24 to me 22 in via ${oif}
${fwcmd} add pass icmp from 10.77.21.0/24 to me
${fwcmd} add pass udp from 10.77.21.0/24 to me
# Trusted Location SSH PING TRACEROUTE - single ip
${fwcmd} add pass tcp from 172.16.17.18 to me 22 in via ${oif}
${fwcmd} add pass icmp from 172.16.17.18 to me
${fwcmd} add pass udp from 172.16.17.18 to me
# deny and log all other SSH requests
${fwcmd} add deny log tcp from any to me 22 in via ${oif}
# Global Deny - with logging
# ${fwcmd} add deny log ip from any to any
# Global Deny - without logging
${fwcmd} add deny ip from any to any
# ---
I realize some of the keep-state are redundant since I just added the
port 53 rules above. I was having some issues with ipv6 DNS and the new
rules fixed that.
To test your rules be cautious. I'd first put a rule at the top to allow
all from any to any or comment out the script in rc.conf, then reboot.
nano /etc/rc.conf
# ---
# firewall_script="/etc/ipfw.rules"
# ---
<reboot>
Then run the script manually, if you get locked out then you can just reboot
the server and the script won't run, allowing you to get back in and fix the
problem with the script.
sh /etc/ipfw.rules
Check your active rules with ...
ipfw list
If it works then enable it in /etc/rc.conf and test again with a reboot.
nano /etc/rc.conf
# ---
firewall_script="/etc/ipfw.rules"
# ---
We will continue with FAMP setup on the next page.