wiki:postlicyd.conf
Warning: Can't synchronize with repository "(default)" (No changeset dc6d1e9e48a6781853cd8be230004212a0a662b8 in the repository). Look in the Trac log for more information.

postlicyd configuration file

postlicyd configuration file describes the automaton that postlicyd evaluates each time it receives a query from postfix. The automaton is a list of independent filters, each filter has a name, a type, some specific configuration, and some exit points. The configuration file also contains some global configuration variables.

This page tries to give a comprehensive description of what can be done with postlicyd.conf.

Format description

In this page, when describing the format of the file, I'll often use the following terms:

name_token
A name token is a string that only contains alphanumerical characters and underscores and that stats with a letter.
name_token := [[:alpha:]][[:alnum:]_]*
value_token
A value token is either a C-like string (between double quotes) or a raw string (without quotes) that starts at the first non-blank character and ends at the last non-blank character before reaching a semi-colon ";"
value_token := cstring | rawstring
cstring := "([^[:blank:]\"]|\n|\r|\\|\" )*"
rawstring := ([^[:blank:];][^;]*[^[:blank:];]|[^[:blank:];])
parameter
A lot of elements of the configuration file are in the form:
parameter := name_token = value_token ;
query_format_string
A query format is a value_token that contains named placeholders in the form ${name_token}. The name token must identify a field of the query, as described in next section. When a query format is interpreted, the placeholders are replaced with the value of the field for the current query.

When a blank character is found while not in a value_token, it is ignored. When a # is found while not in a value_token, the rest of the line is interpreted like a blank character.

Query

A query is a list of identifiers sent by postfix to postlicyd that give information on the current SMTP transaction. The list of valid identifiers is listed on  postfix website. To this list, postlicyd adds shortcuts to get the domain part of the sender address (sender_domain) and the domain part of the recipient address (recipient_domain).

postlicyd 0.8 introduces two new parameters: normalized_client that is either the IP of the client (client_addresss) if the client_name looks like it belongs to a pool of dialup or the client_address/24 (in IPv4), and normalized_sender that contains the sender email after removal of the VERP extension (username+machin => username) and replacement of the trailing digits from the username by a '#'. This two extra-parameters were previously available only in the greylister.

The query contains an instance name that identify the SMTP transaction. postlicyd uses this instance to build a context: as long as on the same connection, the queries sent to postlicyd use the same instance name, the context is preserved. In simpler language: as long as in the same SMTP transaction, postlicyd keeps a context between queries. This can be used to perform actions when DATA is received based on informations from RCPT. This is illustrated in the postlicyd page.

The context of a query contains 64 counters (identified by their index from 0 to 63). When the context is created, the counters are all set to 0. The counters can only handle unsigned integers. As described in the following sections, counters can be updated by post-actions and interpreted by filters of type counter.

Filters

A filter describes a node of the automaton. A filter is a function that takes the query and its context as input and gives a reply. A filter will modify neither the query nor the context, but you can choose to modify the context as a postaction of a filter.

Syntax

The syntax of a filter looks like:

filter_name {
  type = filter_type;

  parameters

  hooks
}
filter_name
The filter name is a name_token. It must be globally uniq (a name cannot be shared between several filters). It is used to identify the filter.
filter_type
The filter is one of the supported type that will be described in this document.
parameters
The parameters are a set (potentially empty) of parameter tokens. This parameters describe the configuration variables of the filter. This document contains a detailed description of configuration variables available for each filter type.

hooks:

The hooks are a set of parameter token with special names. The name of the parameter starts with on_ followed by the name of reply while the value of the parameter describes an action. Let's give an example. Suppose a filter that can reply match, then I must define a hook called on_match with the action to be executed when the filter replies match. A filter must contain a hook for all reachable reply. The format of the action is described in the following parameter.

Post actions

A post-action is the action executed when a filter gives it reply. An action can be divided in two independent parts:

  • alter context
  • what to do next?

The syntax is:

action := (alter_context)next

The context alteration is a suite of actions to perform. Currently, it can be one of the following actions:

alter_context := (post_action:)*
post_action := (counter|warn)
  • counter = counter:id:amount: update the counter with id by adding the given amount
  • warn = warn:message: log a warning. The message must be a query_format string without any colon (:)

next describes the next node of the automaton. This can be either another filter or an exit node. To identify another filter, just use its name:

# When filter replies "match" execute "other_filter_name" as following node of the automaton
on_match = other_filter_name;

next can also be describes an exit: we give a reply to postfix:

# When filter replies "match" reply to postfix
on_match = postfix:DUNNO;

# When filter replies "fail" reply 550 to postfix and add a description
on_fail = postfix:550 Sorry but you not allowed to send mail with ${sender_domain};

The format to give an answer to postfix is:

next := postfix:code( explanation)?
code
Code is a policy answer. Valid answers are listed in  access (5) man page of postfix.
explanation
The explanation is optional (depending on the code, as described in the acccess (5) man page). It is a query_format_string.

Here are a few examples of valid hooks:

# If filter replies "whitelist", go to the filter named "whitelist".
on_whitelist = whitelist;

# If filter replies "match", replies "REJECT" with explanation "Blacklisted" to postfix.
on_match = postfix:REJECT Blacklisted;

# If filter replies "fail", replies "450" with explanation containing the domain of the MAIL FROM smtp command.
on_fail  = postfix:450 Greylisted, see http://www.example.org/${sender_domain}.html;

# if filter replies "error", increments counter 0 and reply "DUNNO" to postfix.
on_error = counter:0:1:postfix:DUNNO;

# if filter replies "match", add 10 to the 63th counter and go to filter named whitelist.
on_match = counter:63:10:whitelist;

# if filter replies "greylist", log the IP
on_greylist = warn:The IP ${client_address} is greylisted:postfix:DUNNO;

Filter types

The filter types supported are the following:

iplist: Matching against a RBL or a static list of IPs

This filter try to match the client_address with a list of IPs. This list can be either a RBL or a static of IPs in a text/plain file or in a rbldns zone file. An iplist filter can match the client_address against several list at once, with a different weight for each list. It then produce a score that is the sum of the weight of all matching lists. Then, this score is compared to a soft and a hard threshold.

Valid parameters are:

file = (no)?lock:weight:filename ;
Use the given file as a static IP list. This file can be either a rbldns zone file or a text/plain file with an IP per line. Lines starting with a # are ignored.

(no)?lock tells postlicyd whether or not the list should be locked in memory.

weight is the weight of this list in the matching process (as previously decribed).

rbldns = (no)?lock:weight:filename ;
In current version of postlicyd, this is an alias for file. This can be used to make the configuration file clearer when using rbldns zone files.
dns = weight:hostname ;
Use the given RBL with the given weight.
soft_threshold = score ;
Minimum score that triggers a soft_match result. The score is an integer, default value is 1.
hard_threshold = score ;
Minimum score that triggers a hard_match result. The score is an integer, default value is 1.

When the processing of this filter starts, all static lists are evaluated first. If the score reaches the hard_threshold, processing is interrupted, and the result is returned. If the static lists do not give a result, all the DNS lookup are performed at once, in parallel. When all the DNS lookup terminates, the score is updated and give the result.

Result is computed as follows (first match wins):

  • if the client_address cannot be parsed, the filter returns error
  • if no list is available or all the lists returned an error (only DNS lookups can be in error), returns error
  • if the score is greater or equal to the hard_threshold, returns hard_match
  • if the score is greater or equal to the soft_threshold, returns soft_match
  • else, return fail

Starting with postlicyd 0.7, if no hook is defined for some return values, result is automatically forwarded to another hook. In the case of iplist, the following forwarding are defined:

  • if error is triggered and no on_error is defined, on_fail is called.
  • if soft_match is triggered and no on_soft_match is defined, on_hard_match is called.
# Lookup in a rbl 
spamhaus_and_abuseat { 
  type   = iplist; 
 
  # configuration 
  file   = lock:10:/var/spool/postlicyd/rbl.spamhaus.org; 
  file   = lock:1:/var/spool/postlicyd/cbl.abuseat.org; 
  soft_threshold = 1; 
  hard_threshold = 11; 
 
  # hooks 
  on_soft_match = greylist; 
  on_hard_match = postfix:REJECT optional text; 
  on_fail       = postfix:OK; 
  on_error      = postfix:DUNNO; 
} 

strlist: Matching against a RHBL or a static list of strings

The behaviour of strlist filter is very close to iplist. The main difference is that strlist performs lookup on emails or domain name. As for iplist, strlist support both DNS lookup to RHBL and lookup in text/plain files. The static list of strings can have 2 formats. The first one is the rbldns zone file format. The second one is a list of strings, one per line. In this second case, line starting with a # are ignored. A line can be either a string or a regexp. A regexp is identified by the fact it is delimited by slashes. Regexps must be anchored (either left () if prefix match is activated, or right ($) if suffix match is activated):

This short example shows a list of strings designed to match the suffix of a domain name:

# This is a list of strings
.mydomain.org
.yourdomain.org

# This is a regexp match, to match suffix, we anchor the regexp on the right
/\.(.*)domain\.org$/

strlist supports the following parameters:

file = (no)?lock:order:weight:filename ;
This parameter is the same as the file parameter of iplist. So, I'll only explain the order parameter. The order describes the kind of matching to use. The matching can be done either from the beginning of the string (prefix) or from its end (suffix), it can match the whole string or only a part. Valid values for order are:

  • prefix: the whole string is in the file (matching done from the start of the string). If the file contains a regexp, it must be anchored on the left ().
  • partial-prefix: a prefix of the string is in the file. If the file contains a regexp, it must be anchored on the left ()
  • suffix: the whole string is in the file (matching done from the end of the string). If the file contains a regexp, it must be anchored on the right ($).
  • partial-suffix: a suffix of the string is in the file. If the file contains a regexp, it must be anchored on the right ($)
rbldns = (no)?lock:weight:filename ;
This build a list of strings from a rbldns zone file. This support both suffix (*.domain) formats and fully qualified domains.
dns = weight:hostname ;
Use the given RHBL with the given weight.
soft_threshold = score ;
Minimum score that triggers a soft_match result. The score is an integer, default value is 1.
hard_threshold = score ;
Minimum score that triggers a hard_match result. The score is an integer, default value is 1.
fields = field(,field)* ;
List the fields of the query that are matched against the lists. You can match 2 kinds of fields:
  • hostnames: helo_name, client_name, reverse_client_name, sender_domain and recipient_domain
  • emails: sender and recipient
You cannot match fields of more than one of these types in a filter.

Warning, no space is allowed in this parameter.

# Whitelist some clients 
client_whitelist { 
  type  = strlist; 
 
  # configuration 
  file    = lock:1:suffix:/var/spool/postlicyd/client_whitelist; 
  rbldns  = lock:1:/va/spool/postlicyd/abuse.rfc-ignorant.org; 
  fields  = client_name,sender_domain,helo_name; 
 
  # hooks 
  on_hard_match = postfix:OK; 
  on_fail       = spamhaus_and_abuseat; 
} 

greylist: Greylister

The greylister uses a quite common greylist algorithm (mainly inspired from  postgrey). It uses 2 files to store its databases. The first one is for the greylisting database (path/prefix_greylist.db, the second one is for the autowhitelister (path/prefix_whitelist.db), where path and prefix are two parameters of the filter.

The valid parameters are:

path = path ;
Directory where the database is stored. This parameter is mandatory

prefix = string ;
String prepended to the name of the database files. Default one is empty. You may choose the set a prefix if you want to setup several greylisters.
lookup_by_host = boolean ;
When performing a lookup, if a the hostname contains the last number of the IP, then the IP is used by the greylister. In the other case, we use the IP used is the IP of a domain that contains this IP. If this flag is set to true, the domain behaviour is disabled, and matching is always performed with the IP of the host. Default value is false.
no_sender = boolean ;
Greylisting is performed on a tuple (client_address, sender, recipient). If this option is set to true, the tuple only contains the client address (modified as explained in lookup_by_host and the recipient. Default value is false (do include the sender).
no_recipient = boolean ;
same as no_sender but excludes the recipient from the tuple. Default value is false. You must set this value to true if you want to use a greylister before MAIL FROM is received.
delay = integer ;
number a tuple is greylisted (this is the number of seconds before a retrial with the same tuple can be accepted. Default value is 300 (5 minutes).
retry_window = integer ;
number of seconds we wait for a retry. If a retrial happen after the retry_window expired, the client is greylisted (again). Default value is 172800 (2 days).
client_awl = integer ;
number of times a client must pass the greylister before being whitelisted. Default value is 5.
max_age = integer ;
maximum age of an entry in the database. Too old entries are automatically deleted. Default value is 108000 (30 hours).
cleanup_period = integer ;
minimum number of seconds between 2 cleanups of the database. The cleanup of the database is very important since it removes useless entries and do a compaction of the database. This makes lookups faster and reduces the memory consumption of the
normalize_sender = boolean ;
by default, the greylister do not use the sender address as is: it runs a normalizer on it to remove username extension (in username+ext@…, the normalizer produces username@…) and factorize number (toto1736849@… is normalized to toto#@example.com). You can choose to disable this behaviour using this parameter. As a consequence, the greylister might be more aggressive.

this parameter has been introduced in postlicyd 0.8.

The filter can returns the following values:

  • if the client is whitelisted, returns whitelist
  • if the client is greylisted, returns greylist
# Perform greylisting 
greylist { 
  type   = greylist; 
 
  # configuration 
  path   = /var/spool/postlicyd/; 
  prefix = greylist_; 
 
  # hooks 
  on_greylist  = postfix:DEFER_IF_PERMIT optional text; 
  on_whitelist = postfix:OK; 
} 

rate: Rate control

This filter defines a rate control process. This filter uses a circular buffer of at most 128 slots (so, the resolution of this filter is 1 second if the delay is smaller than 128 seconds or delay/128 else). This filter matches if at least soft_match requests occurred during the last delay seconds. Valid parameters are:

key = query_format ;
defines the format of the key. Some examples of keys are:
  • ${client_address}: control the emission rate of an IP address.
  • ${sender}: control the emission rate of an email address.
  • ${client_address}/${sender}: control the emission rate of a pair (IP, email)
  • ...

This parameter is mandatory.

delay = integer ;
number of seconds a hit remains valid.
path = path ;
Directory where the database is stored. This parameter is mandatory
prefix = string ;
String prepended to the name of the database files. Default one is empty. You may choose the set a prefix if you want to setup several rate controls. All the entries of a database must have the same delay value, so you can use the same database for several rate filter with the same delay value. However, in most cases, you should prefer to use a database per filter.
soft_threshold = integer ;
number of hits during the last delay seconds that triggers a soft_match. Default value is 1, so, by default, every hit triggers at least a soft_match.
hard_threshold = integer ;
number of hits during the last delay seconds that triggers a hard_match. Default value is 1, so, by default, every hit triggers a hard_match.

cleanup_period = integer ;

minimum number of seconds between two database cleanups. The default value is 86400 seconds (one day).

This filter can returns the following values (in this order of processing):

  • if the number of hits is greater or equals to the hard threshold
    • if the previous number of hits was greater or equals to hard threshold, returns hard_match.
    • else, return hard_match_start
  • if the number of hits is greater or equals to the soft threshold, returns soft_match.
    • if the previous number of hits was greater or equals to soft threshold, returns soft_match.
    • else, return soft_match_start
  • else, returns fail.

The following hook are aliased:

  • if hard_match_start is triggered and on_hard_match_start is not defined, on_hard_match is called
  • if soft_match_start is triggered and on_soft_match_start is not defined, on_soft_match is called
  • if soft_match is triggered and on_soft_match is not defined, on_hard_match is called

This filter is available in postlicyd 0.8 and later.

Example:

# Limit the number of emails sent by an IP to 1000 per hour.
# If the rate exceed 100 per hour, use filter hang to reduce the emission rate of the IP
rate {
  type = rate;

  # configuration
  path = /var/spool/pfixtools;
  prefix = hour_;
  hard_threshold = 1000;
  soft_threshold = 100;
  key = ${client_address};
  delay = 3600;

  # Hooks
  on_hard_match = warn:IP ${client_address} is sending too many emails:postfix:421 Two many mails from your ip: ${client_address};
  on_soft_match = warn:IP ${client_address} is sending many emails:hang
  on_fail = spf;
}

match: Simple boolean expressions

The match filter defines a list of conditions to be matched against the fields of the query.

match_all = boolean ;
if true, the filter matches only if all the conditions match.
condition = field_name OP value ;
defines a condition for the given field of the query. For operators that supports values, the value is either a query_format_string or a regexp (between slashes with a i modifier for case-insensitive matches). Valid operators are:
  • EQUALS or ==: field_name is strictly equal to value
  • EQUALS_i or =i: field_name is case insensitively equal to value
  • DIFFERS or !=: field_name is not equal to value
  • DIFFERS_i or !i: field_name is not case insensitively equal to value
  • CONTAINS or >=: field_name contains value
  • CONTAINS_i or >i: field_name contains case insensitively value
  • CONTAINED or <=: field_name is contained by value
  • CONTAINED_i or <i: field_name is contained case insensitively by value
  • EMPTY or #=: field_name is empty or not set
  • NOTEMPTY or #i: field_name is not empty
  • MATCH or =~: field_name matches the following regexp
  • DONTMATCH or !~: field_name does not match the following regexp

Possible return values are:

  • match if match_all is false and at least one condition matched
  • match if match_all is true and all the conditions matched
  • else fail
# match one of the condition: "stress mode activated", "client_name contains debian.org" or 
#                             "recipient is empty" 
match { 
  type = match; 
 
  # configuration 
  match_all = false; 
  condition = stress == yes; 
  condition = client_name >= debian.org; 
  condition = recipient #=; 
 
  # hook 
  on_match = postfix:OK; 
  on_fail = counter:0:1:greylist; 
} 

counter: Querying the counters inside the context of the query

Trig an action depending on the value of a counter. This is a very simple filter.

counter = id ;
Set the counter to use (see the query section of this page for a description of the counters).
soft_threshold = integer ;
minimum value of the counter that triggers a soft match. Default value is 1.
hard_threshold = integer ;
minimum value of the counter that triggers a hard match. Default value is 1.

This filter can returns the following values (in this order of processing):

  • if counter value is greater or equals to the hard threshold, returns hard_match.
  • if counter value is greater or equals to the soft threshold, returns soft_match.
  • else, returns fail.

Starting with postlicyd 0.7, if no hook is defined for some return values, result is automatically forwarded to another hook. In the case of counter, the following forwarding are defined:

  • if soft_match is triggered and no on_soft_match is defined, on_hard_match is called.
# match if the counter 0 value is greater than 8, or between 5 and 7 
counter { 
  type = counter; 
 
  # configuration 
  counter        = 0; 
  hard_threshold = 8; 
  soft_threshold = 5; 
 
  # hook 
  on_hard_match = postfix:REJECT ${sender_domain}; 
  on_soft_match = greylist; 
  on_fail       = counter:1:10:match; 
} 

spf: Sender Policy Framework

The spf filter performs SPF lookup, as described by  RFC4408. SPF is a mechanism designed to authenticate the sender of an email: the owner of the domain provides a list of IP that can send email from his domain. SPF uses the record of type TXT from the DNS of the domain. It can also use a specific record of type SPF (RRTYPE 99). This filter support both of them but its default behaviour is to avoid the query for the SPF record since hardly no domain uses it. SPF is an algorithm quite complicated that can give 7 different results.

This filter appears in postlicyd 0.7.

This filter supports the following parameters :

use_spf_record = boolean ;
As explained previously, the filter do not perform lookup for the SPF record by default. If this option is set to true, the lookup is performed in parallel of the lookup for the TXT record. The default value is false.

check_helo = boolean ;
If this parameter is true, the filter will check the HELO identity instead of the MAIL domain.

Possible results are:

  • none: no record is published or no checkable domain could be determined.
  • neutral: the domain owner has explicitly stated that he cannot or does not want to assert whether the IP address is authorized. The RFC states that this MUST be treated exactly like the none result. If on_neutral is not provided and neutral is returned, on_none is called.
  • pass: the client is authorized to inject mail with the given identity. Further policy checks can proceed with confidence in the legitimate use of the identity.
  • fail: this is an explicit statement that the client is not authorized to use the domain in the given identity. You can choose to mark the mail based on this or to reject it outright. The RFC states that if you choose to reject the mail, you SHOULD use an SMTP reply code of 550.
  • soft_fail: the domain believes the host is not authorized but is not willing to make that strong of a statement. This result should be treated as somewhere between fail and neutral. If on_soft_fail is not provided, on_fail is called.
  • temp_error: a transient error was encountered while performing the check. You can choose to accept or temporarily reject the message. If on_temp_error is not provided, on_none is called.
  • perm_error: an error condition that requires manual intervention to be resolved has been encountered (e.g.: invalid SPF records, inclusion or redirection). If the domain owner uses macros, this may be result of a checked identity with an unexpected format. If on_perm_error is not provided, on_none is called.

So, you MUST at least provide the following hooks: on_none, on_pass and on_fail.

hang: Wait a few milliseconds

The hang filter is a very simple filter. It wait until a specified amout of millisecond ellapsed. Since postfix waits for a reply of postlicyd before answering the SMTP client, this can be used to simulate a hang in postfix. Hang on connection has been reported to discourage some spambot with very short read timeouts.

This filter type has been introduced in postlicyd 0.7.

Its parameters are:

timeout_ms = integer ;
Number of milliseconds the filter will hang.

It has only one possible exit: timeout, so, you must provide a on_timeout hook.

hang {
  type = hang;

  # Wait 1.5 seconds before giving an answer
  timeout_ms = 1500;

  #
  on_timeout = postfix:DUNNO;
}

srs: Check SRS validity of the recipient address

This srs allow early elimination of invalid SRS emails.

This filter type has been introduced in postlicyd 0.9.

Its parameters are:

bounce_domain = string ;
Domain of the bounces.

secret_file = path ;
File with the SRS secret. This file can the same as the one used by pfix-srsd

Possible results are:

  • none: The recipient is not a bounce and the SRS filter cannot be applied to it.
  • match: The recipient is a bounce and it's a valid SRS-encoded address.
  • fail: The recipient is a bounce but it's not a valid SRS-encoded address.

Global configurations

Entry points

An automaton must have (at least) one entry point. In postlicyd, you can specify the entry point for several automaton since, you may define an automaton per state of the SMTP transation. The syntax is very simple parameter:

state = filter_name;

state is one of the following:

  • client_filter: entry point when postlicyd is called at connection time (from postfix' smtpd_client_restrictions), once per connection
  • helo_filter: entry point when postlicyd is called on HELO/EHLO command (from postfix' smtpd_helo_restrictions), once per transaction
  • sender_filter: entry point when postlicyd is called on MAIL FROM command (from postfix' smtpd_sender_restrictions), once per transaction
  • recipient_filter: entry point when postlicyd is called on RCPT TO command (from postfix' smtpd_recipient_restrictions), once per recipient
  • data_filter: entry point when postlicyd is called on DATA command (from postfix' smtpd_data_restrictions), once per transaction
  • end_of_data_filter: entry point when postlicyd is called at the end of data transfer (from postfix' stmpd_end_of_data_restrictions), once per transaction
  • etrn_filter: entry point when postlicyd is called on ETRN command (from postfix' smtpd_etrn_restrictions)
  • verify_filter: entry point when postlicyd is called on VRFY command.

The value of this parameter is the name of a filter.

Keep in mind that smtpd_delay_reject is set to yes in default postfix configuration. You must set it to no to use client_filter, helo_filter and sender_filter.

Misc. options

port = integer ;
Port the which postlicyd is bound. The default value is 10000. If the port is also specified as a command line parameter, then the value specified on command line overides this parameter.

You must restart postlicyd to change the port (reload does not affect the port).

log_format = query_format_string ;
Format of the log printed in syslog in default log level. In default log level, postlicyd prints one line per query in syslog, this parameter let you chose the format of this line. The value of this parameter is a query_format_string. Default value of this parameter may change from one version of postlicyd to another. In postlicyd 0.6 and 0.7, it is:
request client=${client_name}[${client_address}] from=<${sender}> to=<${recipient}> at ${protocol_state}
use_resolv_path = path ;
If you want DNS lookup to be reforwarded to a resolver, you can specify a resolv.conf like file. This is optionnally and not recommended since postlicyd lookup are mostly useless for other users and so, this will polute the cache of your resolver and increase its latency. The default value is not to use a resolv.conf file.

You must restart postlicyd to change this parameter.

This parameter is available as of postlicyd 0.7.

include_explanation = boolean ;
In addition to their answers, the filters can produce an explanation in the form of a short text. By default, this text is ignored by postlicyd but you can choose to append this explanation in the answer sent back to postfix. Only the explanation of the last filter (the one which triggers the postfix: answer) is taken into account, and if no explanation is produced, nothing is appended to you answer. This parameter can be useful when using the spf filter since SPF natively supports explanations.

This parameter has been introduced in postlicyd 0.8.