What You Are Building

A mail server that accepts inbound email, scans it for malware, scores it for spam, and delivers clean mail to local mailboxes — all on a single Ubuntu server. The stack:

Amavis is the glue. Postfix hands mail to Amavis, which passes it through ClamAV and SpamAssassin, then re-injects clean mail back into Postfix for delivery. This is the standard production architecture — it keeps each component doing one job.

Prerequisites

DNS Records

Before you start, set these DNS records:

mail.example.com.     A       203.0.113.10
example.com.          MX  10  mail.example.com.
example.com.          TXT     "v=spf1 mx -all"

The A record points your mail hostname to the server. The MX record tells the world where to deliver mail for your domain. The SPF record authorises your MX server to send on behalf of the domain. DKIM and DMARC come later.

Step 1: System Preparation

sudo apt update && sudo apt upgrade -y
sudo hostnamectl set-hostname mail.example.com

Edit /etc/hosts to include your FQDN:

127.0.0.1   localhost
203.0.113.10  mail.example.com mail

Verify:

hostname -f

This should return mail.example.com. Postfix relies on the system hostname for its greeting banner and HELO identity.

Step 2: Install Postfix

sudo apt install -y postfix

The installer asks for a configuration type. Select Internet Site and set the system mail name to example.com (your domain, not the hostname).

Core Configuration

Edit /etc/postfix/main.cf. Replace the defaults with a hardened configuration:

# ─── identity ─────────────────────────────────────────────
myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain
mydestination = $myhostname, $mydomain, localhost.$mydomain, localhost

# ─── network ──────────────────────────────────────────────
inet_interfaces = all
inet_protocols = ipv4
mynetworks = 127.0.0.0/8 [::1]/128

# ─── TLS (inbound) ───────────────────────────────────────
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem
smtpd_tls_security_level = may
smtpd_tls_auth_only = yes
smtpd_tls_protocols = >=TLSv1.2
smtpd_tls_mandatory_protocols = >=TLSv1.2
smtpd_tls_mandatory_ciphers = medium
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache

# ─── TLS (outbound) ──────────────────────────────────────
smtp_tls_security_level = may
smtp_tls_protocols = >=TLSv1.2
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtp_tls_loglevel = 1

# ─── SASL authentication ─────────────────────────────────
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain = $myhostname

# ─── restrictions ─────────────────────────────────────────
smtpd_helo_required = yes
smtpd_helo_restrictions =
    permit_mynetworks,
    reject_invalid_helo_hostname,
    reject_non_fqdn_helo_hostname

smtpd_sender_restrictions =
    permit_mynetworks,
    reject_non_fqdn_sender,
    reject_unknown_sender_domain

smtpd_recipient_restrictions =
    permit_mynetworks,
    permit_sasl_authenticated,
    reject_unauth_destination,
    reject_non_fqdn_recipient,
    reject_unknown_recipient_domain,
    reject_rbl_client zen.spamhaus.org,
    reject_rbl_client bl.spamcop.net

smtpd_relay_restrictions =
    permit_mynetworks,
    permit_sasl_authenticated,
    defer_unauth_destination

# ─── limits ───────────────────────────────────────────────
message_size_limit = 52428800
mailbox_size_limit = 0
smtpd_client_connection_rate_limit = 30
smtpd_client_message_rate_limit = 60
smtpd_error_sleep_time = 5s
smtpd_soft_error_limit = 3
smtpd_hard_error_limit = 5

# ─── delivery ─────────────────────────────────────────────
home_mailbox = Maildir/
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases

# ─── content filter (Amavis) ─────────────────────────────
content_filter = smtp-amavis:[127.0.0.1]:10024

# ─── other hardening ─────────────────────────────────────
disable_vrfy_command = yes
smtpd_banner = $myhostname ESMTP
strict_rfc821_envelopes = yes

Submission Port (587)

Edit /etc/postfix/master.cf. Uncomment and configure the submission service for authenticated clients:

submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_tls_auth_only=yes
  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING
  -o content_filter=smtp-amavis:[127.0.0.1]:10024

This ensures authenticated users send through port 587 with mandatory TLS.

Step 3: Install Dovecot

Dovecot provides IMAP access and handles SASL authentication for Postfix.

sudo apt install -y dovecot-core dovecot-imapd dovecot-lmtpd

Configure Authentication

Edit /etc/dovecot/conf.d/10-auth.conf:

disable_plaintext_auth = yes
auth_mechanisms = plain login

Edit /etc/dovecot/conf.d/10-master.conf to expose the auth socket to Postfix:

service auth {
  unix_listener /var/spool/postfix/private/auth {
    mode = 0660
    user = postfix
    group = postfix
  }
}

Configure Mail Location

Edit /etc/dovecot/conf.d/10-mail.conf:

mail_location = maildir:~/Maildir

Configure TLS

Edit /etc/dovecot/conf.d/10-ssl.conf:

ssl = required
ssl_cert = </etc/letsencrypt/live/mail.example.com/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.example.com/privkey.pem
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes

Restart Dovecot:

sudo systemctl restart dovecot
sudo systemctl enable dovecot

Step 4: Obtain a TLS Certificate

sudo apt install -y certbot
sudo certbot certonly --standalone -d mail.example.com

Stop any service on port 80 first, or use the webroot method if Apache is running. Set up automatic renewal:

sudo systemctl enable certbot.timer

Create a renewal hook to reload both services. Write /etc/letsencrypt/renewal-hooks/deploy/reload-mail.sh:

#!/bin/bash
systemctl reload postfix
systemctl reload dovecot
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-mail.sh

Step 5: Install ClamAV

sudo apt install -y clamav clamav-daemon

ClamAV's signature updater (freshclam) runs as a service. Let it complete its first update:

sudo systemctl stop clamav-freshclam
sudo freshclam
sudo systemctl start clamav-freshclam
sudo systemctl enable clamav-freshclam

The first freshclam run downloads the full virus database. This takes a few minutes.

Start the ClamAV daemon:

sudo systemctl start clamav-daemon
sudo systemctl enable clamav-daemon

Configure ClamAV

Edit /etc/clamav/clamd.conf. Key settings:

LogFile /var/log/clamav/clamav.log
LogTime yes
LogSyslog yes
MaxFileSize 50M
MaxScanSize 150M
StreamMaxLength 50M

The MaxFileSize should match or exceed your Postfix message_size_limit.

Verify ClamAV Is Working

sudo clamdscan /usr/share/doc/clamav/examples/

If it reports OK or No virus found, the daemon is running correctly.

Test with the EICAR test string:

echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > /tmp/eicar.txt
sudo clamdscan /tmp/eicar.txt
rm /tmp/eicar.txt

ClamAV should detect this as Eicar-Signature.

Step 6: Install SpamAssassin

sudo apt install -y spamassassin spamc

Enable and start the SpamAssassin daemon:

sudo systemctl enable spamassassin
sudo systemctl start spamassassin

Configure SpamAssassin

Edit /etc/spamassassin/local.cf:

# ─── scoring ──────────────────────────────────────────────
required_score          5.0
report_safe             0
rewrite_header Subject  [SPAM]

# ─── Bayes ────────────────────────────────────────────────
use_bayes               1
bayes_auto_learn        1
bayes_auto_learn_threshold_nonspam  0.1
bayes_auto_learn_threshold_spam     12.0

# ─── network tests ───────────────────────────────────────
skip_rbl_checks         0
use_razor2              0
use_pyzor               0

# ─── trusted networks ────────────────────────────────────
trusted_networks        127.0.0.0/8
internal_networks       127.0.0.0/8

# ─── shortcircuit ─────────────────────────────────────────
ifplugin Mail::SpamAssassin::Plugin::Shortcircuit
  shortcircuit USER_IN_WHITELIST       on
  shortcircuit USER_IN_DEF_WHITELIST   on
  shortcircuit USER_IN_ALL_SPAM_TO     on
  shortcircuit SUBJECT_IN_WHITELIST    on
endif

Update SpamAssassin Rules

sudo sa-update
sudo systemctl restart spamassassin

Schedule automatic rule updates:

sudo crontab -e

Add:

30 3 * * * /usr/bin/sa-update && systemctl restart spamassassin

Step 7: Install and Configure Amavis

Amavis is the content-filter daemon that ties Postfix, ClamAV and SpamAssassin together.

sudo apt install -y amavisd-new

Enable Virus and Spam Checking

Edit /etc/amavis/conf.d/15-content_filter_mode:

@bypass_virus_checks_maps = (
   \%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re);

@bypass_spam_checks_maps = (
   \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re);

Uncomment both blocks. By default they are commented out, which disables scanning.

Configure Amavis

Edit /etc/amavis/conf.d/50-user:

use strict;

$max_servers = 2;
$sa_tag_level_deflt  = -9999;  # always add spam headers
$sa_tag2_level_deflt = 5.0;    # add 'spam detected' header at this level
$sa_kill_level_deflt = 15.0;   # quarantine at this level
$sa_dsn_cutoff_level = 20;     # never send DSN above this

$virus_admin = "postmaster\@example.com";
$spam_admin  = "postmaster\@example.com";

$final_virus_destiny  = D_DISCARD;
$final_spam_destiny   = D_PASS;
$final_banned_destiny = D_BOUNCE;
$final_bad_header_destiny = D_PASS;

1;

Add the ClamAV User to the Amavis Group

Amavis and ClamAV need to share files:

sudo usermod -aG amavis clamav
sudo usermod -aG clamav amavis

Restart ClamAV to pick up the group change:

sudo systemctl restart clamav-daemon

Step 8: Wire Postfix to Amavis

The content filter line is already in main.cf from Step 2. Now add the Amavis service to Postfix's master.cf.

Edit /etc/postfix/master.cf and add at the end:

# ─── Amavis content filter ───────────────────────────────
smtp-amavis unix -      -       y       -       2       smtp
  -o smtp_data_done_timeout=1200
  -o smtp_send_xforward_command=yes
  -o disable_dns_lookups=yes
  -o max_use=20

# ─── Amavis re-injection ─────────────────────────────────
127.0.0.1:10025 inet n  -       y       -       -       smtpd
  -o content_filter=
  -o smtpd_delay_reject=no
  -o smtpd_client_restrictions=permit_mynetworks,reject
  -o smtpd_helo_restrictions=
  -o smtpd_sender_restrictions=
  -o smtpd_recipient_restrictions=permit_mynetworks,reject
  -o smtpd_data_restrictions=reject_unauth_pipelining
  -o smtpd_end_of_data_restrictions=
  -o smtpd_restriction_classes=
  -o mynetworks=127.0.0.0/8
  -o smtpd_error_sleep_time=0
  -o smtpd_soft_error_limit=1001
  -o smtpd_hard_error_limit=1000
  -o smtpd_client_connection_count_limit=0
  -o smtpd_client_connection_rate_limit=0
  -o local_header_rewrite_clients=
  -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks,no_milters

The flow: Postfix receives mail on port 25, passes it to Amavis on port 10024, Amavis scans it and re-injects clean mail back to Postfix on port 10025 with content_filter= cleared so it does not loop.

Step 9: Start Everything

sudo systemctl restart clamav-daemon
sudo systemctl restart spamassassin
sudo systemctl restart amavis
sudo systemctl restart postfix
sudo systemctl restart dovecot

Check for errors:

sudo journalctl -u amavis --no-pager -n 50
sudo journalctl -u postfix --no-pager -n 50

Verify Amavis is listening:

ss -tlnp | grep -E '1002[45]'

You should see Amavis on 10024 and Postfix on 10025.

Step 10: DKIM Signing

DKIM signs outbound mail so receiving servers can verify it came from your domain. Install OpenDKIM:

sudo apt install -y opendkim opendkim-tools

Generate Keys

sudo mkdir -p /etc/opendkim/keys/example.com
sudo opendkim-genkey -b 2048 -d example.com -D /etc/opendkim/keys/example.com -s mail -v
sudo chown -R opendkim:opendkim /etc/opendkim

Configure OpenDKIM

Edit /etc/opendkim.conf:

Syslog          yes
SyslogSuccess   yes
LogWhy          yes
Canonicalization relaxed/simple
Mode            sv
SubDomains      no
AutoRestart     yes
AutoRestartRate 10/1M
Background      yes
DNSTimeout      5
SignatureAlgorithm rsa-sha256

KeyTable        refile:/etc/opendkim/key.table
SigningTable    refile:/etc/opendkim/signing.table
InternalHosts   refile:/etc/opendkim/trusted.hosts
ExternalIgnoreList refile:/etc/opendkim/trusted.hosts

Create /etc/opendkim/signing.table:

*@example.com    mail._domainkey.example.com

Create /etc/opendkim/key.table:

mail._domainkey.example.com    example.com:mail:/etc/opendkim/keys/example.com/mail.private

Create /etc/opendkim/trusted.hosts:

127.0.0.1
localhost
mail.example.com

Add the DKIM DNS Record

Display the public key:

sudo cat /etc/opendkim/keys/example.com/mail.txt

Add the output as a TXT record for mail._domainkey.example.com in your DNS.

Connect OpenDKIM to Postfix

Edit /etc/default/opendkim and set the socket:

SOCKET="inet:8891@localhost"

Add to /etc/postfix/main.cf:

# ─── DKIM ─────────────────────────────────────────────────
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:localhost:8891
non_smtpd_milters = $smtpd_milters

Restart both:

sudo systemctl restart opendkim
sudo systemctl restart postfix

Step 11: DMARC Record

Add a DMARC DNS record:

_dmarc.example.com.  TXT  "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com; ruf=mailto:dmarc@example.com; fo=1; adkim=s; aspf=s"

Start with p=none if you are not yet confident in your SPF and DKIM alignment. Move to p=quarantine once reporting confirms legitimate mail passes, then to p=reject.

Step 12: Create a Mail User

sudo adduser --disabled-login --gecos "Mail User" mailuser
sudo passwd mailuser

Create the Maildir:

sudo -u mailuser mkdir -p /home/mailuser/Maildir/{cur,new,tmp}

Test delivery:

echo "Test message" | mail -s "Test" mailuser@example.com
ls /home/mailuser/Maildir/new/

If a file appears, local delivery is working.

Step 13: Testing the Full Pipeline

Test Inbound Spam Filtering

Send yourself the GTUBE test string (the spam equivalent of EICAR):

echo "Subject: Test spam

XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X" | sendmail mailuser@example.com

Check the delivered message headers:

cat /home/mailuser/Maildir/new/*

Look for X-Spam-Flag: YES and X-Spam-Status: Yes, score= above your threshold.

Test Virus Scanning

Send the EICAR test file as an attachment and check that Amavis discards it:

sudo tail -f /var/log/mail.log

In another terminal:

echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' | mail -s "Virus test" -A /dev/stdin mailuser@example.com

The mail log should show Amavis intercepting and discarding the message as INFECTED.

Test DKIM

Send a message to an external address and check the headers. Gmail shows dkim=pass in the Authentication-Results header. Or use:

echo "DKIM test" | mail -s "DKIM check" check-auth@verifier.port25.com

The bounce-back report shows your SPF, DKIM and DMARC results.

Step 14: Hardening Checklist

Postfix

TLS

ClamAV

SpamAssassin

System

sudo ufw allow 25/tcp
sudo ufw allow 587/tcp
sudo ufw allow 993/tcp
sudo ufw enable

Step 15: Log Monitoring

Postfix logs to /var/log/mail.log. Watch for:

Set up log rotation if not already present:

/var/log/mail.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    postrotate
        /usr/lib/rsyslog/rsyslog-rotate
    endscript
}

For ongoing monitoring, consider piping Postfix logs into a tool like pflogsumm for daily summaries:

sudo apt install -y pflogsumm

Schedule a daily summary:

0 6 * * * /usr/sbin/pflogsumm -d yesterday /var/log/mail.log | mail -s "Mail stats $(date -d yesterday +\%F)" postmaster@example.com

Summary

The stack is Postfix for SMTP, Dovecot for IMAP and authentication, Amavis as the content filter bridge, ClamAV for virus scanning, SpamAssassin for spam classification, and OpenDKIM for outbound signing. Every message passes through ClamAV and SpamAssassin before delivery. Virus mail is silently discarded. Spam is tagged and delivered so users can train the Bayesian filter by moving misclassified messages. TLS is enforced for authentication, DKIM signs every outbound message, and DMARC tells receiving servers what to do with failures. The weak point, as with every mail server, is ongoing maintenance — keep signatures updated, watch the logs, and tune SpamAssassin's thresholds as the Bayes database matures.