Advanced Mail Filtering: A Deep Dive into Amavisd-new and Amavisd-milter Policy Banks

Amavis isn’t new; in fact, AMaViS started as a shell program back in 1997. It has since evolved into a powerful, flexible tool for content filtering. This article will be a hands-on guide to setting up Amavisd-new with a milter and multiple policy banks. I’ll explain the critical difference between after- and before-queue filtering in Postfix, demonstrate how to use both, and show you how to split your mail traffic for a robust, multi-layered defense.

The Foundations: Basic Configuration

I assume that you have a fresh installation of Amavisd-new and Amavisd-milter on Debian or Ubuntu. While the version shouldn’t matter much, it’s always good practice to check the release notes for any changes. I’m using Debian Bullseye.

Because I’m not a big fan of running Postfix in a chroot environment, I have explicitly removed that from my master.cf. My preference is to have the services run in their own, well-defined environments. If you prefer to use a chroot environment for your services, you are free to do so. You might want to read this one for more details.

Basic Configuration

First, you have to set your domain and hostname. The default Debian configuration tries to automatically detect these values, often resulting in your mail server’s FQDN (e.g., mail.no-uce.de) for the $mydomain variable. This is not ideal as it can cause issues with other services and mail flow.

chomp($myhostname = `hostname --fqdn`);
chomp($mydomain = `head -n 1 /etc/mailname`);

To ensure proper configuration, I explicitly override the $mydomain variable in /etc/amavis/conf.d/50-user to avoid potential problems and ensure only the base domain is used:

$mydomain = 'no-uce.de';

Next, you need to define your internal networks (@mynetworks) and local domains (@local_domains_acl). These settings are crucial as Amavis uses them to determine if a message is originating from an internal or external source—which is the fundamental basis for your policy banks.

@mynetworks = qw( 127.0.0.0/8 84.200.7.144/28 );
@local_domains_acl = ( [ ".$mydomain", ".jeanbruenn.info" ] );

To enable additional ports for services, you can override the default setting. In my case, I’m adding port 10023 for my after-queue content filter:

$inet_socket_port = [10023,10024];

I’m using ClamAV as a virus scanner, which I made Amavisd-new aware of by checking 50-av_scanners and editing my 50-user file to enabling spam and virus checks:

@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);

For my setup, I globally disable certain features that I will later enable on a per-policy basis. This is a robust approach to configuration management, ensuring that settings are only active where they are explicitly needed.

# Disable global originating flag to re-enable in a policy bank
$originating = 0;

# forward method is not used for a milter; we enable
# it in the specific policy bank later.
$forward_method = undef;

# Talking with Postfix
$notify_method = 'smtp:[127.0.0.1]:10025';

# Disable quarantine
$QUARANTINEDIR = undef;  
$virus_quarantine_to = undef; 
$banned_quarantine_to = undef;
$bad_header_quarantine_to = undef; 
$spam_quarantine_to = undef;
$mailfrom_to_quarantine = ''; 

# default actions.
$final_virus_destiny = D_REJECT;
$final_banned_destiny = D_REJECT;
$final_spam_destiny = D_REJECT;
$final_bad_header_destiny = D_PASS;

# I want to be informed about viruses anyway
$virus_admin = "postmaster\@$mydomain";
# Don't send me spam reports
$spam_admin = undef;
# Don't send me info about banned
$banned_admin = undef;

Bug? Internal vs. Inbound (A Critical Amavisd Pitfall)

I noticed a strange behavior with Amavisd-new (2.11.1) and Amavisd-milter (1.7.1). When sending an email from an external account (like Gmail) to my server, Amavis would log “AcceptedInternal,” and an EICAR test would be logged as “RejectedInternal.” This was puzzling, as my Gmail address is clearly not an internal account.

The issue resolved itself only when I enabled DKIM verification ($dkim_verification = 1), after which Amavis logged “AcceptedInbound” and “RejectedInbound” as expected.

I contacted Patrick Ben Koetter, a well-known expert in the field of email systems, about this issue. He clarified that, unlike Postfix, Amavis uses @mynetworks to identify internal senders, not just trusted networks. This means that if you communicate via a socket and 127.0.0.0/8 is in your @mynetworks, Amavis may incorrectly classify external mail as internal, especially if it uses a local loopback.

His insight was that:

Contrary to Postfix, amavis does not use mynetworks to identify who is a trusted network (and who is then allowed to relay in Postfix according to permit_mynetworks), but who, from amavis’ point of view, is an internal sender. If you then send to an internal destination (local domains), it is recorded as AcceptedInternal.

Original:
Anders als in Postfix identifiziert amavis mit mynetworks nicht wer ein trusted network ist (und in Postfix dann mit permit_mynetworks relayen darf), sondern wer aus Sicht von amavis ein interner Sender ist. Wenn du dann an ein internes Ziel (local domains) sendest, wird als AcceptedInternal verbucht.

Original by Patrick Ben Koetter in response to a mail. Translated from german to english by me. Thanks P@rick!

This is a subtle but critical distinction and a potential pitfall.

Before-Queue vs. After-Queue Filtering

Understanding the difference between before-queue and after-queue filtering is crucial for designing a secure and legally compliant mail system.

Before-queue filtering happens during the SMTP session, before the mail is stored on the server. If a message is rejected, the sender is immediately notified.

After-queue filtering occurs after the server has accepted the email and stored it in the queue.

This distinction leads to an important question: If your mailserver has accepted an email and notified the sender (“250 OK: queued!”), are you still allowed to modify or discard it?

In some jurisdictions, like Germany, legislation does not permit the discarding of a mail message once it has been accepted by an MTA. For example, the German Penal Code (§ 206) on the violation of postal and telecommunication secrecy can be relevant in this context.

In some countries[5] the legislation does not permit mail filtering to discard a mail message once it has been accepted by an MTA, so this rules out an after-queue filtering setup with discarding or quarantining of messages, but leaves a possibility of delivering (possibly tagged) messages, or rejecting them in a before-queue setup (SMTP proxy or milter).

Wikipedia. Amavis. Interface Topology. 3rd September, 2021 https://en.wikipedia.org/wiki/Amavis

I am not a lawyer, and this is not legal advice. However, in my opinion, it is best practice to reject emails as early as possible—within the ongoing SMTP session—to avoid any potential issues. This is why I favor a before-queue filtering approach with a milter for external traffic.

This separation is also the reason why I’m confident sending Non-Delivery Reports (NDRs) to my internal clients (for after-queue filtering), since the mail is fully accepted after filtering. I am not risking backscatter by sending NDRs to external senders, which would be a severe security anti-pattern.

Policy Banks: The Heart of the System

Policy banks are the core of this flexible filtering system. They allow you to apply different filtering rules based on a policy, such as the port a message arrives on. This enables a granular approach, where you can apply stricter rules to external MTA traffic than to authenticated client mail.

1. After-Queue Filtering for Internal Clients (The submission port)

I believe using an after-queue content filter on the submission port (587) is acceptable because the clients and senders are known and authenticated. This allows me to safely send Non-Delivery Reports (NDRs) to my local users. While some people believe sending NDRs is bad practice, I believe it helps to notify users about potential malicious activity on their accounts. Because I only send these reports to my own domains, I do not cause backscatter.

Here is the master.cf configuration for the submission port:

submission inet n       -       n       -       -       smtpd
  -o content_filter=amavisfeed:[127.0.0.1]:10023
  -o syslog_name=postfix/submission
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject

This uses the amavisfeed service as an after-queue content filter. To define this service and the necessary reinjection path in Postfix, add the following to master.cf:

amavisfeed unix    -       -       n        -      2     lmtp
     -o lmtp_data_done_timeout=1200
     -o lmtp_send_xforward_command=yes
     -o disable_dns_lookups=yes
     -o max_use=20
127.0.0.1:10025 inet n    -       n       -       -     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 receive_override_options=no_header_body_checks,no_unknown_recipient_checks,no_milters
    -o local_header_rewrite_clients=

For the policy banks, we route port 10023 to the ORIGINATING policy. This policy sets originating = 1, telling Amavis that these are internal client messages:

# Mails from our clients
$interface_policy{'10023'} = 'ORIGINATING';
$policy_bank{'ORIGINATING'} = {
  originating => 1,
  forward_method => 'smtp:[127.0.0.1]:10025',
  terminate_dsn_on_notify_success => 0,
};

Verification Log (After-Queue Filter):

You can verify that this is working by sending a mail from a local account to an external address (e.g., Gmail) and then checking the log:

Aug 9 09:36:38 mail postfix/smtpd[13049]: connect from localhost[127.0.0.1]
Aug 9 09:36:38 mail postfix/smtpd[13049]: 41FFA13F95D: client=localhost[127.0.0.1]
Aug 9 09:36:38 mail postfix/cleanup[13030]: 41FFA13F95D: message-id=7fc97d3a-396c-0b65-63be-ecc754d8cca9@jeanbruenn.info
Aug 9 09:36:38 mail postfix/qmgr[9059]: 41FFA13F95D: from=himself@jeanbruenn.info, size=1812, nrcpt=1 (queue active)
Aug 9 09:36:38 mail amavis[12438]: (12438-02) Passed CLEAN {RelayedOutbound}, ORIGINATING LOCAL [91.11.190.91]:59010 [91.11.190.91] himself@jeanbruenn.info -> xxx@gmail.com, Queue-ID: 60AE113F8F9, Message-ID: 7fc97d3a-396c-0b65-63be-ecc754d8cca9@jeanbruenn.info, mail_id: qdLl7ssb94l9, Hits: 1.985, size: 847, queued_as: 41FFA13F95D, dkim_new=dkim20210807:jeanbruenn.info, 1813 ms
Aug 9 09:36:38 mail postfix/lmtp[13031]: 60AE113F8F9: to=xxx@gmail.com, relay=127.0.0.1[127.0.0.1]:10023, delay=2, delays=0.14/0/0.01/1.8, dsn=2.0.0, status=sent (250 2.0.0 from MTA(smtp:[127.0.0.1]:10025): 250 2.0.0 Ok: queued as 41FFA13F95D)
Aug 9 09:36:38 mail postfix/qmgr[9059]: 60AE113F8F9: removed
Aug 9 09:36:38 mail postfix/smtp[13038]: Trusted TLS connection established to gmail-smtp-in.l.google.com[74.125.206.26]:25: TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (P-256)
Aug 9 09:36:38 mail postfix/smtp[13038]: 41FFA13F95D: to=xxx@gmail.com, relay=gmail-smtp-in.l.google.com[74.125.206.26]:25, delay=0.4, delays=0.06/0/0.13/0.21, dsn=2.0.0, status=sent (250 2.0.0 OK 1628494598 r6si16905207wru.571 - gsmtp)
Aug 9 09:36:38 mail postfix/qmgr[9059]: 41FFA13F95D: removed

2. Before-Queue Filtering for Inbound Mail

For incoming mail from external MTAs on port 25, I use the Amavis milter (amavisd-milter). This is a crucial before-queue step for robust security and preventing backscatter. The milter traffic is routed through a Unix socket, which is mapped to the AM.PDP-SOCK policy bank.

I explicitly set originating = 0 in this policy to ensure Amavis knows that mail coming through this socket is from an external, non-originating source. This is crucial for the correct filtering and logging of inbound traffic.

# milter traffic: inbound from external mtas
$interface_policy{'SOCK'} = 'AM.PDP-SOCK'; 
$policy_bank{'AM.PDP-SOCK'} = {
  originating => 0,
  protocol => 'AM.PDP',
  auth_required_release => 0,
};

Milter Configuration Details (Unix Socket):

I use the amavisd-milter through a Unix socket for improved performance and security. Permissions are somewhat tricky, so I edit /etc/default/amavisd-milter to ensure Postfix can access the socket:

MILTERSOCKET=/var/spool/postfix/amavis/amavis.sock
MILTERSOCKETOWNER="postfix:postfix"
MILTERSOCKETMODE="0660"

The Postfix side needs to define the milter and set the milter_connect_macros for proper Amavisd-milter communication:

# milter configuration
milter_amavis = unix:/var/spool/postfix/amavis/amavis.sock
milter_connect_macros = "j {client_name} {daemon_name} v _"

The Milter is then enabled on the smtpd service (port 25). If you use Postscreen, the configuration is applied to the second stage of smtpd:

smtpd pass  -       -       n       -       -       smtpd
  -o smtpd_milters=${milter_amavis}

Verification Log (External Inbound):

You can verify that external inbound mail is correctly processed by the milter and routed to the AM.PDP-SOCK policy by checking the logs:

Aug 9 12:05:16 mail postfix/postscreen[13705]: CONNECT from [209.85.218.44]:33540 to [84.200.7.154]:25
Aug 9 12:05:22 mail postfix/postscreen[13705]: PASS OLD [209.85.218.44]:33540
Aug 9 12:05:23 mail postfix/smtpd[13707]: 620DD13FB6D: client=mail-ej1-f44.google.com[209.85.218.44]
Aug 9 12:05:27 mail amavis[13672]: (13672-01) Passed CLEAN {AcceptedInbound}, AM.PDP-SOCK [209.85.218.44] [209.85.218.44] <xxx@gmail.com> -> <himself@jeanbruenn.info>, Queue-ID: 620DD13FB6D, Message-ID: <CAHJLFyeqfxGBkTadP-jNq4tJgAa+M2ZYxAK17WaFv6fyTjwo6w@mail.gmail.com>, mail_id: l-lKDmN1sSk1, Hits: 0.787, size: 2521, dkim_sd=20161025:gmail.com, 3700 ms
Aug 9 12:05:27 mail postfix/qmgr[9059]: 620DD13FB6D: from=<xxx@gmail.com>, size=2753, nrcpt=1 (queue active)
Aug 9 12:05:27 mail postfix/lmtp[13715]: 620DD13FB6D: to=<himself@jeanbruenn.info>, relay=mail.no-uce.de[private/dovecot-lmtp], delay=4.3, delays=3.9/0.01/0.01/0.38, dsn=2.0.0, status=sent (250 2.0.0 <himself@jeanbruenn.info> W2BqD+f9EGGUNQAA8qPOQg Saved)
Aug 9 12:05:27 mail postfix/qmgr[9059]: 620DD13FB6D: removed

3. Filtering for Internal Systems (MTA to MTA)

To further refine my rules, I apply a separate policy for internal systems. Using @client_ipaddr_policy I direct mail from my private IP network (excluding the mail server’s own IP) to a policy bank called INT-MTA. This allows me to set different filtering strengths for my other internal servers acting as smart hosts or relays.

@client_ipaddr_policy = (
    [qw( !84.200.7.154/32 84.200.7.144/28 )] => 'INT-MTA'
);
# internal systems traffic (inbound)
$policy_bank{'INT-MTA'} = {
  originating => 1,
  protocol => 'AM.PDP',
  auth_required_release => 0,
};

Verification Log (Internal MTA Traffic):

This is about playing relay/smarthost for my internal systems. By setting final_virus_destiny => D_PASS and sending the EICAR test through that system, you can verify that the INT-MTA policy is correctly engaged:

Aug 9 19:46:46 mail amavis[15560]: (15560-01) Passed INFECTED (Eicar-Signature) {AcceptedOutbound}, AM.PDP-SOCK/INT-MTA LOCAL [84.200.7.149] [84.200.7.149] <xxx@jeanbruenn.info> -> <xxx@gmail.com>, Queue-ID: 7A15A13F95D, Message-ID: <20210809174644.72111807C8@jeanbruenn.info>, mail_id: 4fM5YJnrUQAK, Hits: 5.262, size: 621, 1933 ms

Custom Filtering per Policy Bank

Policy BankTraffic Typeoriginatingvirus_adminspam_adminenable_dkim_signingReason
ORIGINATINGLocal Clients (After-Queue)1Default (postmaster)Default (undef)1Safe to send NDRs; must sign mail.
AM.PDP-SOCKExternal Inbound (Before-Queue)0undefundef0Prevent backscatter; no admin notice needed for external threats.
INT-MTAInternal Systems (Before-Queue)1Default (postmaster)Enabled1Must sign mail; notify admin for internal abuse/infection.

This fine-tuning is implemented in 50-user:

# mails from our clients / local users
$policy_bank{'ORIGINATING'} = {
  originating => 1,
  forward_method => 'smtp:[127.0.0.1]:10025',
  terminate_dsn_on_notify_success => 0,
};

# milter traffic: inbound from external mtas
$policy_bank{'AM.PDP-SOCK'} = {
  originating => 0,
  protocol => 'AM.PDP',
  auth_required_release => 0,
  virus_admin_maps => undef,
  enable_dkim_signing => 0,
};

# milter traffic: inbound from internal / my own mtas
$policy_bank{'INT-MTA'} = {
  originating => 1,
  protocol => 'AM.PDP',
  auth_required_release => 0,
  spam_admin_maps => ["postmaster\@$mydomain"],
  banned_admin_maps => ["postmaster\@$mydomain"],
};

The Full Configuration

Here is a consolidated overview of the relevant configuration files that enable this split-filtering approach:

/etc/postfix/main.cf

#
# milter configuration
#
milter_amavis = unix:/var/spool/postfix/amavis/amavis.sock
milter_connect_macros = "j {client_name} {daemon_name} v _"

/etc/postfix/master.cf

smtp      inet  n       -       n       -       1       postscreen
smtpd     pass  -       -       n       -       -       smtpd
  -o smtpd_milters=${milter_amavis}

[..]

submission inet n       -       n       -       -       smtpd
  -o content_filter=amavisfeed:[127.0.0.1]:10023
  -o syslog_name=postfix/submission
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject

[..]

amavisfeed unix    -       -       n        -      2     lmtp
     -o lmtp_data_done_timeout=1200
     -o lmtp_send_xforward_command=yes
     -o disable_dns_lookups=yes
     -o max_use=20

127.0.0.1:10025 inet n    -       n       -       -     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 receive_override_options=no_header_body_checks,no_unknown_recipient_checks,no_milters
    -o local_header_rewrite_clients=

/etc/default/amavisd-milter

MILTERSOCKET=/var/spool/postfix/amavis/amavis.sock
AMAVISSOCKET=/var/lib/amavis/amavisd.sock
MILTERSOCKETOWNER="postfix:postfix"
MILTERSOCKETMODE="0660"

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

#
# Basic settings
# ips, networks, ports, sockets, domains belong here
#

$mydomain = 'no-uce.de';
@mynetworks = qw( 127.0.0.0/8 84.200.7.144/28 );
$inet_socket_port = [10023,10024];
@local_domains_maps = ( [ ".$mydomain", ".jeanbruenn.info" ] );

#
# Spam / Virus checks
# enabled here
#

@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);

#
# Reset and defaults
# resetting a few variables here, setting some defaults
#

# i had some issues getting directions right, just to make
# sure i set it to 0 globally and re-enable it for the
# specific policy bank later.
$originating = 0;

# forward method is not used for a milter; we enable it in
# the specific policy bank later.
$forward_method = undef;

# Talking with Postfix
$notify_method = 'smtp:[127.0.0.1]:10025';

# Disable quarantine
$QUARANTINEDIR = undef;  
$virus_quarantine_to = undef; 
$banned_quarantine_to = undef;
$bad_header_quarantine_to = undef; 
$spam_quarantine_to = undef;
$mailfrom_to_quarantine = ''; 

# default actions.
$final_virus_destiny = D_REJECT;
$final_banned_destiny = D_REJECT;
$final_spam_destiny = D_REJECT;
$final_bad_header_destiny = D_PASS;

#
# DKIM
#

$enable_dkim_signing = 1;
$enable_dkim_verification = 1;
# See: https://blog.jeanbruenn.info/2021/08/07/amavisd-new-and-dkim/
# for remaining DKIM configuration here.

#
# Policy Banks
#

# mails from our clients
$interface_policy{'10023'} = 'ORIGINATING';
$policy_bank{'ORIGINATING'} = {
  originating => 1,
  forward_method => 'smtp:[127.0.0.1]:10025',
  terminate_dsn_on_notify_success => 0,
};

# milter traffic (inbound)
$interface_policy{'SOCK'} = 'AM.PDP-SOCK';
$policy_bank{'AM.PDP-SOCK'} = {
  originating => 0,
  protocol => 'AM.PDP',
  auth_required_release => 0,
  virus_admin_maps => undef,
  enable_dkim_signing => 0,
};

# traffic from internal mtas
@client_ipaddr_policy = (
    [qw( !84.200.7.154/32 84.200.7.144/28 )] => 'INT-MTA'
);
$policy_bank{'INT-MTA'} = {
  originating => 1,
  protocol => 'AM.PDP',
  auth_required_release => 0,
  spam_admin_maps => ["postmaster\@$mydomain"],
  banned_admin_maps => ["postmaster\@$mydomain"],
};

See also

  • Amavisd-new. Integrating amavisd-new in Postfix (README.postfix.txt). https://www.ijs.si/software/amavisd/README.postfix.txt
  • Amavisd-new. amavisd-new documentation on policy banks. https://www.ijs.si/software/amavisd/amavisd-new-docs.html#pbanks
  • Amavisd-new. amavisd-new documentation bits and pieces. https://www.ijs.si/software/amavisd/amavisd-new-docs.html
  • Postfix Documentation. Content Inspection (Content Filter). http://www.postfix.org/FILTER_README.html
  • Postfix Documentation. Milter Support (Milter Architecture). http://www.postfix.org/MILTER_README.html

2 thoughts on “Advanced Mail Filtering: A Deep Dive into Amavisd-new and Amavisd-milter Policy Banks”

  1. Hello,

    Trying to follow your guide to setup amavisd-milter and it all seems to work however, opendmarc milter seems to get ignored by postfix. I see e-mails coming in, spf and dkim pass but opendmarc is not involved in the smtp session.

    1. Hi,

      I haven’t used that milter, yet. I’ll have a look at when I have a bit time, though. Maybe you can send me your configuration so that I can have a look?

      Jean

Leave a Reply to jean Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.