postfix
Contents
-
postfix
- About
- Possible system component layout
- Performance
- Install
- Unix-Permissions
- myhostname, mydomain and myorigin
- Initialize the configuration structure
- Trouble Shooting
- Important commands and scripts
- Compatibility Level
- Restrictions
- Choose your setup
- Local netiquette aliases
- Transport
- vmail setup
- Virtual aliases
- Address verify cache
- Crypto
- Submission
- Postgrey
- policy-weightd
- rspamd
- Test cases
About
Possible system component layout
Performance
CPU-Layout
Postfix spawns many processes and benefits from more CPUs.
So a dual core system (single socekt, sharing the same cache) is mandatory and probably also enough.
Mount a tmpfs in /tmp
Please see tmpfs
Install
1 aptitude install mailutils postfix postfix-pcre exim4-daemon-light-
Tool to edit berkely dbs
1 aptitude install db-util
Unix-Permissions
1 root@mail /etc/postfix # find /etc/postfix -type f -exec chmod o-rx {} \;
2 root@mail /etc/postfix # find /etc/postfix/ -type d -exec chmod -v 750 {} \;
3 der Modus von „/etc/postfix/“ wurde von 0755 (rwxr-xr-x) in 0750 (rwxr-x---) geändert
4 der Modus von „/etc/postfix/sasl“ wurde von 0755 (rwxr-xr-x) in 0750 (rwxr-x---) geändert
5 Modus von „/etc/postfix/ldap“ als 0750 (rwxr-x---) erhalten
6 root@mail /etc/postfix # find /etc/postfix/ -type d -exec chgrp -v postfix {} \;
7 die Gruppe von „/etc/postfix/“ wurde von root in postfix geändert
8 die Gruppe von „/etc/postfix/sasl“ wurde von root in postfix geändert
9 die Gruppe von „/etc/postfix/ldap“ wurde von root in postfix geändert
10 root@mail /etc/postfix # find /etc/postfix/ -exec chgrp -v postfix {} \;
11 die Gruppe von „/etc/postfix/“ wurde als postfix erhalten
12 die Gruppe von „/etc/postfix/sasl“ wurde als postfix erhalten
13 die Gruppe von „/etc/postfix/sasl/smtpd.conf“ wurde von root in postfix geändert
14 die Gruppe von „/etc/postfix/postfix-files“ wurde von root in postfix geändert
15 die Gruppe von „/etc/postfix/postfix-script“ wurde von root in postfix geändert
16 die Gruppe von „/etc/postfix/post-install“ wurde von root in postfix geändert
17 die Gruppe von „/etc/postfix/dynamicmaps.cf“ wurde von root in postfix geändert
18 die Gruppe von „/etc/postfix/master.cf“ wurde von root in postfix geändert
19 die Gruppe von „/etc/postfix/ldap“ wurde als postfix erhalten
20 die Gruppe von „/etc/postfix/ldap/mailenabled_distgroups.cf“ wurde von root in postfix geändert
21 die Gruppe von „/etc/postfix/ldap/virtual_alias_maps_mailforwarding.cf“ wurde von root in postfix geändert
22 die Gruppe von „/etc/postfix/ldap/transport_maps.cf“ wurde von root in postfix geändert
23 die Gruppe von „/etc/postfix/ldap/virtual_alias_maps_sharedfolders.cf“ wurde von root in postfix geändert
24 die Gruppe von „/etc/postfix/ldap/mailenabled_dynamic_distgroups.cf“ wurde von root in postfix geändert
25 die Gruppe von „/etc/postfix/ldap/local_recipient_maps.cf“ wurde von root in postfix geändert
26 die Gruppe von „/etc/postfix/ldap/virtual_alias_maps.cf“ wurde von root in postfix geändert
27 die Gruppe von „/etc/postfix/ldap/mydestination.cf“ wurde von root in postfix geändert
28 die Gruppe von „/etc/postfix/transport.db“ wurde von root in postfix geändert
29 die Gruppe von „/etc/postfix/header_checks.inbound“ wurde von root in postfix geändert
30 die Gruppe von „/etc/postfix/header_checks.inbound.db“ wurde von root in postfix geändert
31 die Gruppe von „/etc/postfix/header_checks.internal“ wurde von root in postfix geändert
32 die Gruppe von „/etc/postfix/header_checks.internal.db“ wurde von root in postfix geändert
33 die Gruppe von „/etc/postfix/header_checks.submission“ wurde von root in postfix geändert
34 die Gruppe von „/etc/postfix/header_checks.submission.db“ wurde von root in postfix geändert
35 die Gruppe von „/etc/postfix/transport“ wurde von root in postfix geändert
36 die Gruppe von „/etc/postfix/main.cf“ wurde von root in postfix geändert
Certificate management
1 adduser postfix ssl-cert
myhostname, mydomain and myorigin
Please configure the DNS and host resolver before configuring the MTA. The system must be able to determine its own #FQDN.
myhostname
(if not set it) defaults to the value of gethostname(), the C function implemented by glibc based on uname(), which only returns a hostname without the domain-name
must not be read from file /etc/mailname
mydomain
defaults to the domain part of $myhostname or
if myhostname not set and therefor derived from gethostname() to localdomain- IMHO this value should be set by the admin manually
myorigin
defaults to $myhostname
may in larger setups be set to $mydomain
may be read from a file /etc/mailname (Debian)
1 mx1.rockstable.it
$myorigin should not be assigned to myhostname, (e.g. when it has been read from file /etc/mailname
Otherwise postfix crashes "fatally" on /
To leave no host-specific information in
/etc/postfix/main.cf
I recommend the following configuration
Check it with:
1 postconf -x myhostname mydomain myorigin mydestination
Initialize the configuration structure
Prepare configuration structure
1 declare -a FILES_DB
2 FILES_DB=(
3 /etc/postfix/access_client \
4 /etc/postfix/access_helo \
5 /etc/postfix/access_recipient \
6 /etc/postfix/access_sender \
7 /etc/postfix/body_checks \
8 /etc/postfix/esmtp_access \
9 /etc/postfix/header_checks \
10 /etc/postfix/header_checks_outbound \
11 /etc/postfix/relay_domains \
12 /etc/postfix/transport \
13 /etc/postfix/virtual \
14 /etc/postfix/virtual_alias_domains \
15 /etc/postfix/virtual_mailbox_domains \
16 )
17
18 mkdir /etc/postfix/ldap
19 for FILE in "${FILES_DB[@]}"; do
20 install -o root -g root -m 0640 /dev/null "$FILE"
21 done
Trouble Shooting
When I had no clue what was going on, i took a look into the logs at a higher debugging level. In case of postfix it's as simple as adding -v to the appropriate line in /etc/postfix/master.cf. In this example smtpd -v.
/etc/postfix/master.cf
In this case a reload is not sufficient, you need to restart the service.
1 systemctl restart postfix.service
Don't forget to remove it later on.
Debug Peer List
To gather additional information about a misbehaving host add this line to
/etc/postfix/main.cf
1 debug_peer_list = 127.0.0.1, REM.OTE.N.ET/SNM
And reload postfix
1 postfix reload
Maintenance mode
/etc/postfix/main.cf
And reload postfix
1 postfix reload
Important commands and scripts
Don't forget about the following commands:
1 postconf -d #DEFAULTS
2 postconf -n #CHANGED
3 postconf -x #EXPAND VALUES RECURSIVELY
4 postmap -q #QUERY DATABASE FOR KEY
5 mailq #SHOW QUEUE
6 postqueue -p #SHOW QUEUE, SAME AS mailq
7 postqueue -f #FLUSH/SEND ANY MAIL IN QUEUE
8 postqueue -i ID #IMMEDIATLY REQUEUE MAIL WITH ID
9 postsuper -d ID #DELETE QUEUED EMAIL
10 db_dump #SHOW CONTENT OF BERKELEY DBS
11
Makefile
Nice trick to save some work in common an repeating tasks - use a Makefile.
Install make
1 aptitude install make
/etc/postfix/Makefile
1 postmap:
2 postmap /etc/postfix/access_client
3 postmap /etc/postfix/access_helo
4 postmap /etc/postfix/access_sender
5 postmap /etc/postfix/access_recipient
6 postmap /etc/postfix/body_checks
7 postmap /etc/postfix/esmtp_access
8 postmap /etc/postfix/relay_domains
9 postmap /etc/postfix/transport
10 postmap /etc/postfix/virtual
11 postmap /etc/postfix/virtual_alias_domains
12 postmap /etc/postfix/virtual_mailbox_domains
13 #postmap /etc/postfix/
14 postmap /etc/postfix/restrictions_helo_checks
15 postmap /etc/postfix/restrictions_roleaccount_exceptions
16 newaliases
17 @echo "Postfix databases refreshed - changes active."
18
19 version_vim = $(shell dpkg-query --showformat='$${Version}' -W vim \
20 |sed -r '/^$$/d;s/^(.*\:)?([0-9]+\.[0-9]).*/\2/' \
21 |sed 's/\.//')
22
23 vim_syntax:
24 cat vim_syntax.vim >> /usr/share/vim/vim${version_vim}/syntax/pfmain.vim
25
26 rsync_opts := -CavuzbP
27 excludes := --exclude '*.db' --exclude 'main.cf*' --exclude '*.swp'
28 spath := /etc/postfix
29 node1 := mail1
30 node2 := mail2
31
32 push_node1:
33 rsync ${rsync_opts} ${excludes} ${spath}/ ${node1}:${spath}
34 rsync ${rsync_opts} /etc/aliases ${node1}:/etc/aliases
35 pull_node1:
36 rsync ${rsync_opts} ${excludes} ${node1}:${spath}/ ${spath}
37 rsync ${rsync_opts} ${node1}:/etc/aliases /etc/aliases
38
39 push_node2:
40 rsync ${rsync_opts} ${excludes} ${spath}/ ${node2}:${spath}
41 rsync ${rsync_opts} /etc/aliases ${node2}:/etc/aliases
42 pull_node2:
43 rsync ${rsync_opts} ${excludes} ${node2}:${spath}/ ${spath}
44 rsync ${rsync_opts} ${node2}:/etc/aliases /etc/aliases
45
46 sync2node1: push_node1 pull_node1
47 sync2node2: push_node2 pull_node2
Hint: Makefiles don't like lines starting with Space.
Define a syntax-file to be appended
/etc/postfix/vim_syntax.vim
1 syntax keyword pfmainConf ciphers_insecure
2 syntax keyword pfmainConf cipher_suite_minimum
3 syntax keyword pfmainConf protocols_insecure
4 syntax keyword pfmainConf tls_security_level
5
6 syntax match pfmainRef "$\<ciphers_insecure\>"
7 syntax match pfmainRef "$\<cipher_suite_minimum\>"
8 syntax match pfmainRef "$\<protocols_insecure\>"
9 syntax match pfmainRef "$\<tls_security_level\>"
1 make postmap
Find and delete a stuck email by address
1 ### WITH INTERMEDIATE JSON
2 postqueue -j \
3 |jq -s '.[]|select(.recipients[]|.address=="DST_EMAIL@ADDRESS.TLD")|.queue_id' \
4 |xargs -L1 postsuper -d
5
6 ### OLD SCHOOL
7 postqueue -p \
8 |grep -B2 "DST_EMAIL@ADDRESS.TLD" \
9 |cut -d\ -f1 \
10 |grep -Eo '^[0-9A-F]+' \
11 |xargs -L1 postsuper -d
Or as a script
/usr/local/sbin/postfix_delete_mail.sh
1 #!/bin/bash
2
3 DST="$1"
4 declare -a IDS
5
6 if [ -n "$DST" ]; then
7 IDS=( $(postqueue -p \
8 |grep -B2 "$DST" \
9 |cut -d\ -f1 \
10 |grep -Eo '^[0-9A-F]+')
11 )
12 [ "${#IDS[@]}" -gt 0 ] && \
13 xargs -n1 postsuper -d <<< "${IDS[@]}"
14 else
15 cat <<-EOL
16 Please specify destination email address
17 as first positional parameter.
18 EOL
19 fi
Redirect stuck mail
/usr/local/sbin/postfix_redirect_mail.sh
1 #!/bin/bash -e
2
3 declare -a IDS
4
5 SELF="$(basename $0)"
6
7 usage () {
8 cat <<-EOF
9 $SELF
10
11 -h|--help) Display this page
12 -n|--new-dest) New target email-address
13 -o|--old-dest) Old target email-address
14 -s|--skip-header) Skip the old header
15 EOF
16 }
17
18 # Note that we use `"$@"' to let each command-line parameter expand to a
19 # separate word. The quotes around `$@' are essential!
20 # We need TEMP as the `eval set --' would nuke the return value of getopt.
21 TEMP=`getopt -o hn:o:s --long help,new-dest:,old-dest:,skip-header \
22 -n "$SELF" -- "$@"`
23
24 if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi
25
26 # Note the quotes around `$TEMP': they are essential!
27 eval set -- "$TEMP"
28
29 while true ; do
30 case "$1" in
31 -h|--help) usage; exit ; shift ;;
32 -n|--new-dest) DST_NEW="$2" ; shift 2 ;;
33 -o|--old-dest) DST_OLD="$2" ; shift 2 ;;
34 -s|--skip-header) SKIP_HEADER=true ; shift ;;
35 #-c|--c-long)
36 # # c has an optional argument. As we are in quoted mode,
37 # # an empty parameter will be generated if its optional
38 # # argument is not found.
39 # case "$2" in
40 # "") echo "Option c, no argument"; shift 2 ;;
41 # *) echo "Option c, argument \`$2'" ; shift 2 ;;
42 # esac ;;
43 --) shift ; break ;;
44 *) echo "Internal error!" ; exit 1 ;;
45 esac
46 done
47 #echo "Remaining arguments:"
48 #for arg do echo '--> '"\`$arg'" ; done
49
50 ### SANITIZE USER INPUT
51 if [ -z "$DST_NEW" ]; then
52 echo "Please specify a new destination."
53 exit 1
54 elif [ -z "$DST_OLD" ];then
55 echo "Please specify a old destination."
56 exit 1
57 fi
58
59 ### GET MAIL FOR DST
60 #IDS="$(postqueue -j \
61 # |jq -s '.[]|select(.recipients[]|.address=="'"$DST_OLD"'")|.queue_id' )"
62
63 IDS=( $(postqueue -p \
64 |grep -B2 "$DST_OLD" \
65 |cut -d\ -f1 \
66 |grep -Eo '^[0-9A-F]+')
67 )
68
69 if [ "${#IDS[@]}" -gt 0 ]; then
70 DIR_TMP=$(mktemp -d)
71
72 echo -e "Mail-IDs to be redirected:\n${IDS[@]}"
73 ### PUT EMAILS ON HOLD
74 xargs -L1 postsuper -h <<< ${IDS[@]}
75
76 for ID in "${IDS[@]}"; do
77 if [ $SKIP_HEADER ]; then
78 postcat -qb "$ID" > "$DIR_TMP/$ID"
79 else
80 postcat -qbh "$ID" > "$DIR_TMP/$ID"
81 fi
82
83 FROM="$(postcat -qh "$ID" \
84 |grep '^From: ' \
85 |sed -r 's/^From: //')"
86
87 sendmail -f "$FROM" "$DST_NEW" < "$DIR_TMP/$ID"
88 [ "$?" == 0 ] && postsuper -d "$ID"
89 rm "$DIR_TMP/$ID"
90 done
91 rmdir "$DIR_TMP"
92 else
93 echo "No mail in queue for '$DST_OLD'. Exiting …"
94 fi
Query MX-records
query_mx.sh
A extended version of this idea wrapped in a shell script queries MX records by their priority, resolves their corresponding A records. It also gathers the A-Records of the domain, because they are used as a fallback, if no MX-record is found. Furthermore it gathers the SPF and DMARC TXT-records. It proofed to be very useful during the establishment or migration of mail-servers.
query_mx.sh
1 #!/bin/bash
2
3 ### DOMAINS SHOULD BE A PATH TO A FILE
4 FILE_DOMAINS="$1"
5
6 if [ -f "$FILE_DOMAINS" ]; then
7 DOMAINS="$(awk '{print $1}' "$FILE_DOMAINS" \
8 |grep -v '^\s*#')"
9
10 else
11 echo "File '$FILE_DOMAINS' does not exist."
12 exit 1
13 fi
14
15 for DOMAIN in $DOMAINS; do
16 echo
17 declare -A MX_A_RECORDS MX_RECORDS
18 MX_RECORDS_RAW="$(dig -t MX "$DOMAIN" +short)";
19 ### FQDN OF MX IS KEY, PRIORITY IS VALUE
20 eval MX_RECORDS=( "$(sed -r \
21 's/^([0-9]+)\s+([[:alnum:].-]+)/[\2]=\1/' \
22 <<< "$MX_RECORDS_RAW")" )
23 if [ ${#MX_RECORDS[@]} -gt 0 ]; then
24 for MX in "${!MX_RECORDS[@]}"; do
25 MX_A_RECORDS+=( [$MX]="$(dig -t A "$MX" +short)" );
26 done
27 fi
28
29 declare -a A_RECORDS
30 A_RECORDS=( $(dig -t A "$DOMAIN" +short) );
31
32 TXT_RECORDS_RAW="$(dig -t TXT "$DOMAIN" +short)";
33 TXT_RECORDS="$(grep -i -e spf <<< "$TXT_RECORDS_RAW")";
34
35 DMARC_RECORD="$(dig -t TXT "_dmarc.$DOMAIN" +short)";
36
37 cat <<-EOL
38 ### Domain: '$DOMAIN'
39 Number of MX-Records: ${#MX_RECORDS[@]}
40 EOL
41
42 if [ ${#MX_RECORDS[@]} -gt 0 ]; then
43 cat <<-EOL
44 $(for MX in "${!MX_RECORDS[@]}"; do
45 echo -e "@ MX ${MX_RECORDS[$MX]} $MX"
46 done \
47 |column -t)
48 $(for MX in "${!MX_A_RECORDS[@]}"; do
49 echo -e "$MX A ${MX_A_RECORDS[$MX]}"
50 done \
51 |column -t)
52 EOL
53 fi
54
55 cat <<-EOL
56 Number of A-Records: ${#A_RECORDS[@]}
57 EOL
58
59 if [ ${#A_RECORDS[@]} -gt 0 ]; then
60 cat <<-EOL
61 $(for A_RECORD in "${A_RECORDS[@]}"; do
62 echo "@ A $A_RECORD"
63 done \
64 |column -t)
65 EOL
66 fi
67
68 if [ -n "$TXT_RECORDS" ]; then
69 cat <<-EOL
70 $(while read -r TXT; do
71 echo -e "@ TXT $TXT"
72 done <<< "$TXT_RECORDS")
73 EOL
74 else
75 echo -e 'NO SPF;\t\tTXT record not found.'
76 fi
77
78 if [ "$DMARC_RECORD" ]; then
79 echo -e "@ TXT $DMARC_RECORD"
80 else
81 echo -e 'NO DMARC;\t_dmarc TXT record not found.'
82 fi
83
84
85 unset A_RECORDS MX_RECORDS MX_A_RECORDS TXT_RECORDS DMARC_RECORD
86 done
Install the script
1 install -o root -g root -m 755 query_mx.sh /usr/local/bin
Use it like
Compare custom config to postfix defaults
/usr/local/bin/postfix_compare_defaults.sh
1 #!/bin/bash
2 # Copyright Rockstable IT 2020
3 # License: GPLv2+
4 # Delivered as is, no warranty, no liability,…
5
6 set -o pipefail
7
8 ### INITIALIZE
9 CONFIG_DIR="/etc/postfix"
10 SELF="$(basename "$0")"
11
12 usage () {
13 cat <<-EOL
14
15 $SELF - Compare postfix configuration to defaults.
16
17 Synopsis:
18 $PROGAM [-h|--help] [-c|--config_dir "/absolute/path/to/directory"]
19
20 []: Optional
21 EOL
22 }
23
24
25 # Note that we use `"$@"' to let each command-line parameter expand to a
26 # separate word. The quotes around `$@' are essential!
27 # We need TEMP as the `eval set --' would nuke the return value of getopt.
28 TEMP=`getopt -o c:h --long config_dir:,help \
29 -n "$SELF" -- "$@"`
30
31 if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi
32
33 # Note the quotes around `$TEMP': they are essential!
34 eval set -- "$TEMP"
35
36 while true ; do
37 case "$1" in
38 -h|--help) usage; exit 0; shift ;;
39 -c|--config-dir) CONFIG_DIR="$2" ; shift 2 ;;
40 #-c|--c-long)
41 # # c has an optional argument. As we are in quoted mode,
42 # # an empty parameter will be generated if its optional
43 # # argument is not found.
44 # case "$2" in
45 # "") echo "Option c, no argument"; shift 2 ;;
46 # *) echo "Option c, argument \`$2'" ; shift 2 ;;
47 # esac ;;
48 --) shift ; break ;;
49 *) echo "Internal error!" ; exit 1 ;;
50 esac
51 done
52
53
54 if [ "$CONFIG_DIR" == "/etc/postfix" ]; then
55 echo "Comparing to default config_dir '/etc/postfix'";
56 CONF_CURRENT="$(postconf -n)"
57 elif [ -n "$CONFIG_DIR" ] && [ -d "$CONFIG_DIR" ]; then
58 echo "Comparing to specifed config_dir '$CONFIG_DIR'";
59 CONF_CURRENT="$(postconf -n -c "$CONFIG_DIR")"
60 else
61 cat <<-EOL
62
63 Error: Invalid argument to option '-c|--config_dir': '$CONFIG_DIR'.
64 Maybe:
65 * undefined
66 * not a directory
67 EOL
68 usage
69 exit 1
70 fi
71
72 while read LINE; do
73 KEY="$(sed -r 's/^([[:alnum:]_$-]+)\s*=.*$/\1/' <<< "$LINE")"
74 echo -e "\n= Key: '$KEY' ="
75 VALUE="$(sed -r 's/.*=\s*([^=]+)$/\1/' <<< "$LINE")"
76 VALUE="$(fold -s <<< "$VALUE")"
77 if [ -z "$VALUE" ] || grep -E '=$' <<< "$VALUE"; then
78 VALUE="NULL"
79 fi
80 echo -e "Current: '$VALUE'"
81 DEFAULT="$(postconf -d "$KEY" 2>&1 \
82 |sed -r 's/.*=\s*([^=]+)$/\1/')"
83 if grep -E 'unknown parameter' <<< "$DEFAULT"; then
84 echo "postconf -d '$KEY' -> warned unknown parameter - custom/typo?"
85 DEFAULT="NO DEFAULT"
86 elif [ -z "$DEFAULT" ] || grep -Eq '=$' <<< "$DEFAULT"; then
87 DEFAULT="NULL"
88 fi
89 DEFAULT="$(fold -s <<< "$DEFAULT")"
90 echo -e "Default: '$DEFAULT'"
91
92 done <<< "${CONF_CURRENT}"
Analyse aliases file
- Supports files with comments, empty lines and line continuations.
- Outputs information to each alias and a summary.
- You may specify a list of domains to be blacklisted.
- Checks are very extensible - adjust it to your needs.
Defaults to analyse /etc/aliases (Option: -a)
/usr/local/bin/postfix_test_aliases.sh
1 #!/bin/bash
2 # Copyright Rockstable IT 2020
3 # License: GPLv2+
4 # Delivered as is, no warranty, no liability,…
5
6 ### INITIALIZE
7 declare -a DOMAIN_BLACKLIST
8 BLACKLIST=false
9 FILE="/etc/aliases"
10 ALIAS_COUNT=0
11 ADDRESS_COUNT=0
12 KEEP=0
13 KEEP_FDA=0
14 KEEP_PURPOSE=0
15 REMOVE=0
16 REMOVE_BLACKLIST=0
17 REMOVE_REDUNDANT=0
18 LINE_NUMBER=0
19 PRINT="ALL"
20 PROGRAM="$(basename $0)"
21 SYNTAX_ERROR_COUNT=0
22
23 ### FUNCTIONS
24 split () {
25 local DELIM="$1"
26 local STRING="$2"
27 local IFS
28 local OIFS
29 ### ARRAY IS DECLARED LOCALLY (no -g)
30 declare -a ARRAY
31
32 OIFS="$IFS"
33 IFS="$DELIM"
34 ARRAY=( ${STRING// /} )
35 IFS="$OIFS"
36 echo "${ARRAY[@]}"
37 }
38
39 usage () {
40 cat <<-EOL
41 $PROGRAM - Analyse postfix aliases file
42
43 Synopsis:
44 $PROGRAM \\
45 [-a|--aliases /path/to/aliases] [-k|--keepable] [-r|--removeable [-s|--short] \\
46 [--] [domain1.tld] [domain2.tld2] […]
47
48 []: optional
49 (): mandatory
50
51 Most useful when blacklist domain is specified.
52 EOL
53 }
54
55 ### PARSE OPTIONS
56 # Note that we use `"$@"' to let each command-line parameter expand to a
57 # separate word. The quotes around `$@' are essential!
58 # We need TEMP as the `eval set --' would nuke the return value of getopt.
59 TEMP=`getopt -o a:hkrs --long aliases:,help,keepable,removeable,short \
60 -n "$PROGRAM" -- "$@"`
61
62 if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi
63
64 # Note the quotes around `$TEMP': they are essential!
65 eval set -- "$TEMP"
66
67 while true ; do
68 case "$1" in
69 -a|--aliases) FILE="$2" ; shift 2 ;;
70 -h|--help) usage; exit 0 ; shift ;;
71 -k|--keepable) PRINT="KEEPABLE" ; shift ;;
72 -r|--removeable) PRINT="REMOVABLE" ; shift ;;
73 -s|--short) SHORT=true ; shift ;;
74
75 #-c|--c-long)
76 # # c has an optional argument. As we are in quoted mode,
77 # # an empty parameter will be generated if its optional
78 # # argument is not found.
79 # case "$2" in
80 # "") echo "Option c, no argument"; shift 2 ;;
81 # *) echo "Option c, argument \`$2'" ; shift 2 ;;
82 # esac ;;
83 --) shift ; break ;;
84 *) echo "Internal error!" ; exit 1 ;;
85 esac
86 done
87 ##echo "Remaining arguments:"
88 ##for arg do echo '--> '"\`$arg'" ; done
89
90 DOMAIN_BLACKLIST=( "$@" )
91
92 if [ ${#DOMAIN_BLACKLIST[@]} -ge 1 ]; then
93 BLACKLIST=true
94 echo "Blacklisted domains: '${DOMAIN_BLACKLIST[@]}'"
95 fi
96
97 ### MAIN
98
99 # PREPARE FILE
100 ALIASES_RAW="$(cat "$FILE")"
101 ALIASES_CLEANED="$(sed -r '/^\s*(#.*)?$/d' <<< "$ALIASES_RAW")"
102 ALIASES_JOINED="$(sed -zr 's/\n\s+/ /g' <<< "$ALIASES_CLEANED")"
103
104 # ANALYSE LINES
105 while read LINE; do
106 ((ALIAS_COUNT++))
107 unset DEST_ADDRESS
108 unset FDA
109
110 if grep -qE '^@' <<< "$LINE"; then
111 FDA=true
112 fi
113
114 ALIAS="$(sed -r 's/(^[^: ]+):?\s.*/\1/' <<< "$LINE")"
115 DEST_STR="$(sed -r 's/(^[^: ]+):?\s+([^,]+)/\2/' <<< "$LINE")"
116 LINE_NUMBER="$(awk '/'"$ALIAS"'/{ print NR; exit }' <<< "$ALIASES_RAW")"
117
118 ### CHECK DESTINATION STRING
119 COUNT_WORDS="$(wc -w <<< "$DEST_STR")"
120 COUNT_COMMAS="$(tr -d -c ',' <<< "$DEST_STR" \
121 |awk '{ print length }')"
122 if [ "$COUNT_WORDS" -gt 2 ] && \
123 [ "$COUNT_COMMAS" -lt "$((COUNT_WORDS-1))" ]; then
124 SYNTAX="Check of line '$LINE_NUMBER' failed:"
125 SYNTAX+=" Words: '$COUNT_WORDS'"
126 SYNTAX+=", Commas: '$COUNT_COMMAS'"
127 SYNTAX+=", Commas expected: '$((COUNT_WORDS-1))'"
128 ((SYNTAX_ERROR_COUNT++))
129 else
130 SYNTAX="Fine."
131 fi
132
133 # GET LOCAL PART
134 ALIAS_LOCAL="${ALIAS/@*/}"
135 [ -z "$ALIAS_LOCAL" ] && ALIAS_LOCAL="NULL"
136
137 # GET DOMAIN PART
138 if grep -Eq '@[^@]+' <<< "${ALIAS}"; then
139 ALIAS_DOMAIN="${ALIAS/*@/}"
140 else
141 ALIAS_DOMAIN="NULL"
142 fi
143
144 ### bash4.4
145 #mapfile -d ';' -t LOCAL < <(printf '%s;' "${DEST/,\s+/,/}")
146
147 declare -a DEST_ADDRESS
148 DEST_ADDRESSES=( $(split "," "$DEST_STR") )
149
150 for DEST_ADDRESS in ${DEST_ADDRESSES[@]}; do
151 ((ADDRESS_COUNT++))
152 unset ACTION
153
154 # GET LOCAL PART
155 DEST_LOCAL="${DEST_ADDRESS/@*/}"
156 [ -z "$DEST_LOCAL" ] && DEST_LOCAL="NULL"
157
158 # GET DOMAIN PART
159 if grep -Eq '@[^@]+' <<< "${DEST_ADDRESS}"; then
160 DEST_DOMAIN="${DEST_ADDRESS/*@/}"
161 else
162 DEST_DOMAIN="NULL"
163 fi
164
165 ### ANALYSE
166 if [ $FDA ]; then
167 ACTION="keep"
168 REASON="fda"
169 fi
170
171 if [ $BLACKLIST ] && \
172 [ "$ALIAS_LOCAL" == "$DEST_LOCAL" ] && \
173 [ "$ALIAS_DOMAIN" == "NULL" ] && \
174 grep -Eq "$DEST_DOMAIN" <<< "${DOMAIN_BLACKLIST[@]}"; then
175 ACTION="remove"
176 REASON="blacklist"
177 elif [ "$ALIAS_LOCAL" == "$DEST_LOCAL" ] && \
178 [ "$ALIAS_DOMAIN" == "$DEST_DOMAIN" ]; then
179 ACTION="remove"
180 REASON="redundant"
181 else
182 ACTION="keep"
183 REASON="purpose"
184 fi
185
186 case "$ACTION" in
187 "keep") ((KEEP++));;
188 "remove") ((REMOVE++));;
189 esac
190
191 case "$REASON" in
192 "fda") ((KEEP_FDA++));;
193 "purpose") ((KEEP_PURPOSE++));;
194 "blacklist") ((REMOVE_BLACKLIST++));;
195 "redundant") ((REMOVE_REDUNDANT++));;
196 esac
197
198
199 ### OUTPUT
200 if [ "$PRINT" == "ALL" ] || \
201 ([ "$PRINT" == "REMOVABLE" ] && [ "$ACTION" == "remove" ]) || \
202 ([ "$PRINT" == "KEEPABLE" ] && [ "$ACTION" == "keep" ]) ; then
203 [ ! $SHORT ] && echo
204 echo "Alias: $ALIAS"
205 if [ ! $SHORT ]; then
206 echo "Line: $LINE_NUMBER"
207 echo -e "Syntax: $SYNTAX"
208 echo "Raw Line: $LINE"
209 echo "Alias local: $ALIAS_LOCAL"
210 echo "Alias domain: $ALIAS_DOMAIN"
211 echo "Dest Address: $DEST_ADDRESS"
212 echo "Dest Local: $DEST_LOCAL"
213 echo "Dest Domain: $DEST_DOMAIN"
214 echo "Action: $ACTION"
215 echo "Reason: $REASON"
216 [ $FDA ] && echo -e "Type:\t\tFull domain alias"
217 fi
218 fi
219 done
220 done <<< "$ALIASES_JOINED"
221
222 REMOVE1=$((REMOVE_REDUNDANT+REMOVE_BLACKLIST))
223
224 cat <<-EOL
225
226 = Summary =
227
228 $ALIAS_COUNT aliases have been processed.
229 $ADDRESS_COUNT destination addresses have been compared.
230 $KEEP alias-destination pairs should be kept.
231 $KEEP_FDA alias-destination pairs are full domain aliases (catch all).
232 $KEEP_PURPOSE alias-destination pairs serve a purpuse.
233 $REMOVE_REDUNDANT alias-destination pairs are redundant.
234 $REMOVE_BLACKLIST alias-destination pairs are blacklisted.
235 $REMOVE alias-destination pairs should be removed.
236 $SYNTAX_ERROR_COUNT syntax errors have been found.
237 EOL
Send test mail
With Sendmail
Send a test email from cli with sendmail
Or as a script with sendmail
/usr/local/bin/test_mail.sh
1 #!/bin/bash
2
3 SELF=${0#*\/}
4 DATE="$(date +%F)"
5 TIME="$(date +%T)"
6 FQDN="$(hostname -f)"
7 SUBJECT="Test Email from host '$FQDN' - '$DATE' '$TIME'"
8 FROM="$USER"
9 TO="root"
10 [ -n "$1" ] && FROM="$1"
11 [ -n "$2" ] && TO="$2"
12
13 {
14 cat <<-EOF
15 Subject: $SUBJECT
16
17 Please ignore this email.
18 It's is a simple testmail.
19
20 FQDN: "$FQDN"
21 FROM: "$FROM"
22 TO: "$TO"
23
24 EOF
25 } |sendmail -f "$FROM" "$DST"
mass_mail.sh
Or a version that sends to multiple receipients at once with mail. This is very useful during migrations and validations for repeated test.
PLEASE NOTE: The HEREDOCs should be indented with 'TAB' characters.
/usr/local/bin/mass_mail.sh
1 #!/bin/bash
2
3 SELF=${0#*\/}
4 DATE="$(date +%F)"
5 TIME="$(date +%T)"
6 FQDN="$(hostname -f)"
7 FROM="$USER"
8
9 declare -a USERS
10 USERS=(
11 "abuse"
12 "noc"
13 "security"
14 "postmaster"
15 "hostmaster"
16 "webmaster"
17 "root"
18 )
19
20 usage () {
21 cat <<-EOF
22 $SELF -d|--domain "domain.tld" [-f|--from "from-addr"] [user1 [user2 […]]]
23
24 [] optional
25 EOF
26 }
27
28 # Note that we use "$@" to let each command-line parameter expand to a
29 # separate word. The quotes around "$@" are essential!
30 # We need TEMP as the 'eval set --' would nuke the return value of getopt.
31 TEMP=$(getopt -o 'd:f:h' --long 'domain:,from:,help' -n "$SELF" -- "$@")
32
33 if [ $? -ne 0 ]; then
34 echo 'Terminating...' >&2
35 exit 1
36 fi
37
38 # Note the quotes around "$TEMP": they are essential!
39 eval set -- "$TEMP"
40 unset TEMP
41
42 while true; do
43 case "$1" in
44 '-h'|'--help')
45 usage
46 exit 0
47 shift
48 continue
49 ;;
50 '-d'|'--domain')
51 DOMAIN="$2"
52 shift 2
53 continue
54 ;;
55 '-f'|'--from')
56 FROM="$2"
57 shift 2
58 continue
59 ;;
60 #'-c'|'--c-long')
61 # # c has an optional argument. As we are in quoted mode,
62 # # an empty parameter will be generated if its optional
63 # # argument is not found.
64 # case "$2" in
65 # '')
66 # echo 'Option c, no argument'
67 # ;;
68 # *)
69 # echo "Option c, argument '$2'"
70 # ;;
71 # esac
72 # shift 2
73 # continue
74 #;;
75 '--')
76 shift
77 break
78 ;;
79 *)
80 echo 'Internal error!' >&2
81 exit 1
82 ;;
83 esac
84 done
85
86 if [ -z "$DOMAIN" ]; then
87 echo "No domain specified. Exiting…"
88 exit 1
89 fi
90
91 echo "Domain: '$DOMAIN'"
92 if [ "${#@}" -gt 0 ]; then
93 USERS=("$@}")
94 fi
95
96 echo "Local parts:"
97 for UNAME in "${USERS[@]}"; do
98 echo "--> '$UNAME'"
99 done
100
101 SUBJECT="Test Email for domain: '$DOMAIN' - '$DATE' '$TIME'"
102
103 if [ ! "$DOMAIN" ]; then
104 cat <<-EOF
105 Please enter a domain.
106 EOF
107 usage
108 exit 1
109 fi
110
111 cat <<-EOF |mail -s "$SUBJECT" "${USERS[@]/%/@$DOMAIN}"
112 Dear recipient,
113
114 This is an automated test email.
115
116 DOMAIN: "$DOMAIN".
117 FQDN: "$FQDN"
118 FROM: "$FROM"
119 TO: "$TO"
120
121 Kind Regards
122 Rockstable IT
123 EOF
Make the script executable
swaks - swiss army knife of smtp
Swaks is a featureful, flexible, scriptable, transaction-oriented SMTP test tool written and maintained by John Jetmore. It is free to use and licensed under the GNU GPLv2.
Features include:
- SMTP extensions including TLS, authentication, pipelining, PROXY, PRDR, and XCLIENT
- Protocols including SMTP, ESMTP, and LMTP
- Transports including UNIX-domain sockets, internet-domain sockets (IPv4 and IPv6), and pipes to spawned processes
- Completely scriptable configuration, with option specification via environment variables, configuration files, and command line
Compatibility Level
Postfix introduces a safety net that runs Postfix programs with backwards-compatible default settings after an upgrade.
Postfix compares the build in compatibility_level (at compile time) to the one specified in main.cf compatibility_level (default: 0). It determines settings in default state and takes actions if compatibility_level in main.cf is less than compiled in.
Sources:
Information gathered on Debian postfix 3.7.11-0+deb12u1.
The default value of compatibility_level is 0. There are currently 3 additional compatibility levels.
Show all defaults triggered on compatibility level (and sort them by level)
compatibility_level=1
compatibility_level=2
compatibility_level=3.6
1 lmtp_tls_fingerprint_digest = ${{$compatibility_level} <level {3.6} ? {md5} : {sha256}}
2 smtpd_tls_fingerprint_digest = ${{$compatibility_level} <level {3.6} ? {md5} : {sha256}}
3 smtp_tls_fingerprint_digest = ${{$compatibility_level} <level {3.6} ? {md5} : {sha256}}
4 respectful_logging = ${{$compatibility_level} <level {3.6} ? {no} : {yes}}
5 smtpd_relay_before_recipient_restrictions = ${{$compatibility_level} <level {3.6} ? {no} : {yes}}
The mechanism will log a warning whenever a "new" default setting could have an negative effect on your mail flow.
1 # postfix reload
2 postfix: Postfix is running with backwards-compatible default settings
3 postfix: See http://www.postfix.org/COMPATIBILITY_README.html for details
4 postfix: To disable backwards compatibility use "postconf compatibility_level=2" and "postfix reload"
5 postfix/postfix-script: refreshing the Postfix mail system
Summary of backwards-compatible default settings
- With compatibility_level 1
key: append_dot_mydomain
compatible value: yes
new: no
key: smtputf8_enable
compatible value: no
new: yes
key: smtpd_relay_restrictions
compatible value: (empty)
new: permit_mynetworks, permit_sasl_authenticated, defer_unauth_destination
chroot field in master.cf
compatible value: chroot=y
new (3.0): chroot=n
- With compatibility_level 2
key: mynetworks_style
compatible value: subnet
new (3.0): mynetworks_style=host
key: relay_domains
compatible value: $mydestination
new (3.0): empty
- With compatibility_level 3.6
key: lmtp_tls_fingerprint_digest
compatible value: md5
new: sha256
key: smtpd_tls_fingerprint_digest
compatible value: md5
new: sha256
key: smtp_tls_fingerprint_digest
compatible value: md5
new: sha256
key: respectful_logging
compatible value: no
new: yes
key: smtpd_relay_before_recipient_restrictions
compatible value: no
new: yes
Set compatibility level
This simply adds a line to<<br>> /etc/postfix/main.cf
Restrictions
- Probably not perfect, but a good starting point
- Deny early to save CPU time.
Restriction order
Please see:
Restrictions configuration
The lookup tables have been initialized like described in
#Initialize the configuration structure
The maps than than have been mapped using the
#Makefile.
Now the configuration is set active
/etc/postfix/main.cf
1 ### RESTRICTIONS
2
3 ### GENERIC RESTRICTIONS
4 #,check_policy_service servername
5 #,defer
6 #,defer_if_permit
7 #,defer_if_reject
8 #,permit
9 #,reject_multi_recipient_bounce
10 #,reject_plaintext_session
11 #,reject_unauth_pipelining
12 #,reject
13 #,sleep seconds
14 #,warn_if_reject
15
16 #smtpd_client_restrictions (default: empty)
17 smtpd_client_restrictions =
18 #check_ccert_access type:table
19 #,check_client_access type:table
20 #,check_client_a_access type:table
21 #,check_client_mx_access type:table
22 #,check_client_ns_access type:table
23 #,check_reverse_client_hostname_access type:table
24 #,check_reverse_client_hostname_a_access type:table
25 #,check_reverse_client_hostname_mx_access type:table
26 #,check_reverse_client_hostname_ns_access type:table
27 #,check_sasl_access type:table
28 #,permit_inet_interfaces
29 permit_mynetworks
30 ,permit_sasl_authenticated
31 #,permit_tls_all_clientcerts
32 #,permit_tls_clientcerts
33 #,reject_rbl_client rbl_domain=d.d.d.d
34 #,permit_dnswl_client dnswl_domain=d.d.d.d
35 #,reject_rhsbl_client rbl_domain=d.d.d.d
36 #,permit_rhswl_client rhswl_domain=d.d.d.d
37 #,reject_rhsbl_reverse_client rbl_domain=d.d.d.d
38 ,reject_unknown_client_hostname
39 ,reject_unknown_reverse_client_hostname
40
41 #smtpd_data_restrictions (default: empty)
42 smtpd_data_restrictions =
43 reject_unauth_pipelining
44
45 #smtpd_end_of_data_restrictions (default: empty)
46 smtpd_end_of_data_restrictions =
47
48 ### Extended Turn (of connection direction for dialup)
49 #smtpd_etrn_restrictions (default: empty)
50 smtpd_etrn_restrictions =
51 #check_etrn_access type:table
52
53 ### ENFORCE HELO AND TO APPLY RESTRICTIONS
54 smtpd_helo_required = yes
55 #smtpd_helo_restrictions (default: empty)
56 smtpd_helo_restrictions =
57 reject_invalid_helo_hostname
58 ,reject_non_fqdn_helo_hostname
59 #,check_helo_access type:table
60 #,check_helo_a_access type:table
61 #,check_helo_mx_access type:table
62 #,check_helo_ns_access type:table
63 #,reject_rhsbl_helo rbl_domain=d.d.d.d
64 #,reject_unknown_helo_hostname
65
66 #smtpd_recipient_restrictions (default: see postconf -d output -> empty)
67 smtpd_recipient_restrictions =
68 reject_non_fqdn_recipient
69 ,reject_unknown_recipient_domain
70 #,check_recipient_access type:table
71 #,check_recipient_a_access type:table
72 #,check_recipient_mx_access type:table
73 #,check_recipient_ns_access type:table
74 #,defer_unauth_destination
75 ,permit_mynetworks
76 ,warn_if_reject reject_unlisted_recipient
77 ### ASK DOVECOT IF VALID
78 ,warn_if_reject reject_unverified_recipient
79 ,reject_unauth_destination
80 #,permit_auth_destination
81 #,permit_mx_backup
82 #,reject_rhsbl_recipient rbl_domain=d.d.d.d
83
84 #smtpd_relay_restrictions (default: permit_mynetworks, permit_sasl_authenticated, defer_unauth_destination)
85 smtpd_relay_restrictions =
86 permit_mynetworks
87 ,permit_sasl_authenticated
88 #,defer_unauth_destination
89 ,reject_unauth_destination
90 ,permit
91
92 #smtpd_sender_restrictions (default: empty)
93 smtpd_sender_restrictions =
94 reject_non_fqdn_sender
95 #,reject_authenticated_sender_login_mismatch
96 #,reject_known_sender_login_mismatch
97 ,reject_sender_login_mismatch
98 #,reject_unauthenticated_sender_login_mismatch
99 ,check_sender_access hash:/etc/postfix/access_sender
100 ,reject_unknown_sender_domain
101 #,reject_unlisted_sender
102 #,warn_if_reject reject_unverified_sender
103 #,reject_unverified_sender
104 #,reject_rhsbl_sender rbl_domain=d.d.d.d
105 #,check_sender_a_access type:table
106 #,check_sender_mx_access type:table
107 #,check_sender_ns_access type:table
108
109
110 #################################################
111 ### RESTRICTIONS FOR MSAs (MAIL SUBMISSION AGENT)
112
113 msa_helo_restrictions =
114 reject_invalid_helo_hostname
115 ,reject_non_fqdn_helo_hostname
116 msa_client_restrictions =
117 permit_mynetworks
118 ,permit_sasl_authenticated
119 ,reject
120 msa_sender_restrictions =
121 permit_mynetworks
122 ,reject_non_fqdn_sender
123 ,reject_sender_login_mismatch
124 ,permit_sasl_authenticated
125 ,reject
126 msa_relay_restrictions =
127 reject_non_fqdn_recipient
128 ,reject_unknown_recipient_domain
129 ,permit_mynetworks
130 ,permit_sasl_authenticated
131 ,reject
132 msa_recipient_restrictions =
133
134
135 ### REASONING
136 unverified_recipient_reject_reason = Recipient address lookup failed
137 unverified_sender_reject_reason = Sender address lookup failed
138
139 ##########
140 ### CHECKS
141 header_checks = pcre:/etc/postfix/header_checks
142 smtp_header_checks = pcre:/etc/postfix/header_checks_outbound
143 body_checks = pcre:/etc/postfix/body_checks
Header and body checks
These files are usually utilize POSIX compatible regular expressions /pcre/ to match the header line. Please make sure to read
man 5 pcre_table
man 5 header_checks
man 1 postmap
Goal of these small ruleset on outgoing email via snmp_header_checks is to
- test removing other potentially sensitive info
- remove internal path of the email when sending
during SMTP delivery.
/etc/postfix/header_checks_outbound
1 # Remove various verbose strings
2 /^.*User-Agent: .*$/ IGNORE
3 /^X-Mailer: .*$/ IGNORE
4 /^X-ClamAV-Server: .*$/ IGNORE
5 /^X-Virus-Scanned: .*$/ IGNORE
6
7 # Do not indent the patterns between "if" and "endif".
8 if /^Received: +/
9 ### Remove "Received:" headers with local domains
10 /^Received: +from +([[:alnum:]-]+\.(localnet|lan) *)(\(([[:alnum:]\.-]+ +)*(\[[0-9\.]+\]\))?)?/ IGNORE
11 ### Remove "Received:" headers that end on a RFC1918 private IP
12 #/^Received: from .*\[(10|172\.(1[6-9]|2[0-9]|3[12])|192\.168)\.([0-9]+\.)+[0-9]+\]\)/ IGNORE
13 ### Remove "Received:" headers that end on a RFC1918 private IP - tighter regex
14 /^Received: +from +([[:alnum:]-\.]+ *)?\(([[:alnum:]-\.]+ *)?(\[(10|172\.(1[6-9]|2[0-9]|3[12])|192\.168)\.([0-9]+\.)+[0-9]+\])\)/ IGNORE
15 ### Remove "Received:" headers that end on a RFC1918 private IP - tighter regex with basic (nonsense) IPv6 matching
16 #/^Received: +from +([[:alnum:]-\.]+ *)?\(([[:alnum:]-\.]+ *)?(\[((10|172\.(1[6-9]|2[0-9]|3[12])|192\.168)\.([0-9]+\.)+[0-9]+|IPv6:[0-9a-f:]+)\])\)/ IGNORE
17 endif
18
19 # vim: syntax=perl
20
So it works only when sending to other servers.
Here is a small test file. ~/test_file.txt
1 ### TEST FILE FOR HEADER_CHECKS
2 Received: from localhost (localhost [127.0.0.1])
3 Received: from localhost (localhost [IPv6:::1])
4 Received: from test.example.org
5 Received: from test.example.org ([10.31.31.1])
6 Received: from test.example.org ([10.31.31.1])
7 Received: from test.example.org ([172.31.31.1])
8 Received: from test.example.org ([192.168.31.1])
9 Received: from test.example.org (unknown [172.31.31.1])
10 Received: from test.example.org (localhost [IPv6:::1])
11 Received: from test.example.org (test.example.org [172.31.31.1])
12 Received: from test.example.org (sub.test.example.org [172.31.31.1])
13 Received: from sub.test.example.org (sub.test.example.org [172.31.31.1])
14 Received: from test.example.org (ipv6.test.example.org [IPv6:fe80::31:0:1])
15 Received: from example.localnet (TEST-NET-1-1 [192.0.2.1])
16 Received: from test.example.lan (TEST-NET-1-1 [192.0.2.1])
17 Received: by test.example.org (Postfix, from userid 65534)
18 Received: by test.example.org (Postfix, from userid 999)
19 Received: by test.example.org (Postfix)
20 Received: from test.example.com ([127.0.0.1]) by test.example.com with Vendor SMTPSVC(16.4.2023.12);
Test the ruleset with the file
Choose your setup
Postfix
- local ALIAS: shared domains, UNIX system accounts
- virtual MAILBOX: separate domains, non-UNIX accounts
- virtual ALIAS: separate domains, UNIX system accounts
Local netiquette aliases
To keep maintenance limited, i suggest creating aliases to central mailboxes of a selected domain.
/etc/aliases
1 # See man 5 aliases for format
2 root: root@rockstable.it
3 clamav: root
4 mailer-daemon: postmaster
5
6 postmaster: postmaster@rockstable.it
7 hostmaster: hostmaster@rockstable.it
8 webmaster: webmaster@rockstable.it
9
10 abuse: abuse@rockstable.it
11 noc: noc@rockstable.it
12 security: security@rockstable.it
13 privacy: privacy@rockstable.it
newaliases
Transport
Define where the email goes next.
[] supress a DNS MX RR lookup.
/etc/postfix/transport
vmail setup
Dovecot as backend
Please prepare the MDA dovecot first by following dovecot#vmail_setup and return here later on.
/etc/postfix/main.cf
1 ### ALIASED DOMAINS AND ADDRESSES
2 ### POINTING TO REMOTE DOMAINS
3 #virtual_maps =
4 virtual_maps = hash:/etc/postfix/virtual
5 virtual_alias_maps = $virtual_maps
6 #virtual_alias_domains = $virtual_alias_maps
7 virtual_alias_domains = hash:/etc/postfix/virtual_alias_domains
8
9 ### MAILBOXED DOMAINS AND ADDRESSES WHERE THIS
10 ### MTA IS FINAL TARGET
11
12 ### ENSURE virtual_mailbox_maps IS EMPTY
13 ### OR smtpd_reject_unlisted_recipient (default: yes)
14 ### WILL ENFORCE EVERY USER TO BE LISTED IN THIS DB
15 virtual_mailbox_maps =
16 #virtual_mailbox_domains = $virtual_mailbox_maps
17 virtual_mailbox_domains = hash:/etc/postfix/virtual_mailbox_domains
18
19 ### virtual_transport CAN BE OVERRIDDEN BY /etc/postfix/transports
20 #virtual_transport = virtual
21 virtual_transport = lmtp:unix:private/dovecot-lmtp
Please make sure virtual_mailbox_maps is null otherwise smtpd_reject_unlisted_recipient (default: yes) will enforce that every user is listed in this database.
This causes error Recipient address rejected: User unknown in virtual mailbox table; in the most cases.
Address verfication is performed by address_verify_virtual_transport = $virtual_transport which is default. And dovecot will answer this question in the lmtp-protocol.
Hint:
- Never list a domain or address in multiple of the following databases at the same time:
mydestination
relay_domains
virtual_alias_domains
virtual_mailbox_domains
- Never list a domain or address in multiple of the following databases at the same time:
/etc/postfix/virtual_mailbox_domains
Query AD for virtual mailbox maps
If you don't have dovecot as a backend, you may use the following to proof that a mailbox exists by queriing AD/ldap.
Prepare your ADDC or ldap-server to support cryptography! If you use AD and bind to it with a username and password combination, you don't want your credentials to be transmitted in plain-text! The credentials should furthermore be system accounts, whose passwords do not expire.
Install the plugin postfix-ldap.
1 aptitude install postfix-ldap
Provide the AD CA-Certificate at /etc/postfix/ldap/cacert.pem.
/etc/postfix/ldap/virtual_mailbox_users
1 ### LOOKUP MAILBOX ADDRESSES IN AD
2 ### FOR OPTIONS PLEASE SEE: man 5 ldap_table
3
4 ### debuglevel (default: 0)
5 #debuglevel = 0
6 ### timeout (default: 10 seconds)
7 timeout = 4
8 ### version (default: 2)
9 version = 3
10 ### start_tls (default: no)
11 start_tls = no
12 ### tls_ca_cert_file (No default; set either this or tls_ca_cert_dir)
13 tls_ca_cert_file = /etc/postfix/ldap/cacert.pem
14 ### server_host (default: localhost)
15 server_host =
16 ldaps://dc1.example.com:636
17 ldaps://dc2.example.com:636
18 ### search_base (No default; you must configure this)
19 search_base = CN=Users,DC=example,DC=com
20 ### bind_dn (default: empty)
21 bind_dn = CN=postfix,CN=Users,DC=example,DC=com
22 ### bind_pw (default: empty)
23 bind_pw = secure_long_password
24 ### result_attribute (default: maildrop)
25 result_attribute = mail
26 #result_attribute = userPrincipalName
27
28 ### query_filter (default: mailacceptinggeneralid=%s)
29 #query_filter = (&(objectclass=person)(userPrincipalName=%s))
30 #query_filter = (&(mail=%s)(objectclass=person)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))
31 #query_filter = (&(&(mail=%s)(!(mail=HealthMailbox*)))(objectclass=person)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))
32 #query_filter = (&(&(|(mail=%s)(proxyAddresses=smtp:%s))(!(mail=HealthMailbox*)))(objectclass=person))
33 #query_filter = (&(|(objectclass=person)(objectclass=group))(&(|(mail=%s)(proxyAddresses=smtp:%s))(!(mail=HealthMailbox*))))
34 query_filter = (&(|(objectclass=person)(objectclass=group))(&(|(mail=*)(proxyAddresses=smtp:*))(!(|(mail=HealthMailbox*)(mail=FederatedEmail.*)(mail=Migration.*)(mail=MsExchDiscoveryMailbox*)(proxyAddresses=smtp:SystemMailbox*))))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))
Some notes to the query_filter:
Please adjust the query_filter to your needs. For example may want to filter the HealthMailbox.*@example.com mailboxes.
- attributes
- The canonical "email-address" is stored in the attributes
mail and proxyAddresses=SMTP: (with uppercase "SMTP")
Aliases are stored in proxyAddresses=smtp: with lowercase "smtp")
The userAccountControl in the query filter checks
that bit 2 (ACCOUNTDISABLE) is not set.
- Filters also "Freigabe Postfächer"
- The canonical "email-address" is stored in the attributes
- objectClasses
- Exchange "Public Folders"
are of objectclass=publicFolder
- Exchange Email distrubution lists/groups (ger. "Verteilergruppen")
are of objectclass=group
- Exchange "Freigabe Postfächer"
are of objectclass=user, have many proxyAddresses=smtp: aliases, but are disabled in userAccountControl.
- If you are using this,
don't filter by the disabled flag in userAccountControl
- If you are using this,
- Exchange "Public Folders"
/etc/postfix/ldap/virtual_mailbox_public_folders
1 ### LOOKUP PUBLIC FOLDER ADDRESSES IN AD
2 ### FOR OPTIONS PLEASE SEE: man 5 ldap_table
3
4 ### debuglevel (default: 0)
5 #debuglevel = 0
6 ### timeout (default: 10 seconds)
7 timeout = 4
8 ### version (default: 2)
9 version = 3
10 ### start_tls (default: no)
11 start_tls = no
12 ### tls_ca_cert_file (No default; set either this or tls_ca_cert_dir)
13 tls_ca_cert_file = /etc/postfix/ldap/cacert.pem
14 ### server_host (default: localhost)
15 server_host =
16 ldaps://dc1.example.com:636
17 ldaps://dc2.example.com:636
18 ### search_base (No default; you must configure this)
19 search_base = CN=Microsoft Exchange System Objects,DC=example,DC=com
20 ### bind_dn (default: empty)
21 bind_dn = CN=postfix,CN=Users,DC=example,DC=com
22 ### bind_pw (default: empty)
23 bind_pw = secure_long_password
24 ### result_attribute (default: maildrop)
25 result_attribute = mail
26
27 ### query_filter (default: mailacceptinggeneralid=%s)
28 #query_filter = (&(mail=%s)(objectclass=publicFolder)(!(msExchHideFromAddressLists=TRUE)))
29 query_filter = (&(|(mail=%s)(proxyAddresses=smtp:%s))(objectclass=publicFolder)(!(msExchHideFromAddressLists=TRUE)))
Please make sure, to use correct unix-permissions so postfix can read (only) the credentials by group permissions.
You can now reference it as a lookup in
/etc/postfx/main.cf
1 …
2 ### ALIASED DOMAINS AND ADDRESSES
3 ### POINTING TO LOCAL AND REMOTE DOMAINS
4 virtual_alias_maps = hash:/etc/postfix/virtual_alias
5 virtual_alias_domains = hash:/etc/postfix/virtual_alias_domains
6
7 ### MAILBOXED DOMAINS AND ADDRESSES WHERE THIS
8 ### MTA IS FINAL TARGET
9
10 #virtual_mailbox_domains = $virtual_mailbox_maps
11 virtual_mailbox_domains = hash:/etc/postfix/virtual_mailbox_domains
12
13 ### smtpd_reject_unlisted_recipient (default: yes)
14 ### ENFORCE EVERY USER TO BE LISTED IN THIS DB
15 #virtual_mailbox_maps =
16 virtual_mailbox_maps =
17 proxy:ldap:/etc/postfix/ldap/virtual_mailbox_users
18 ,proxy:ldap:/etc/postfix/ldap/virtual_mailbox_public_folders
19 #,hash:/etc/postfix/virtual_mailbox_users
20 …
The proxy: delegates these lookups to the postfix proxymap daemon (man 8 proxymap). This may be interesting if you:
- are using a chrooted environment.
- want to limit the number of connections to the backend by the number of proxymap processes (Default: 100).
- want to provide a single-updater functionality (to i.e. file-based tables).
query_ldap.sh
Quick script to aid setup.
/usr/local/bin/query_ldap.sh
1 #!/bin/bash
2
3 ### CHECKS
4 if ! which ldapsearch; then
5 echo "ldap-utils are missing. Exiting…"
6 exit 1
7 fi
8
9 ### INIT
10 HOST="hostname.domain.tld"
11 BASE_DN="dc=domain,dc=tld"
12 BIND_DN="CN=Postfix,OU=Users,DC=domain,DC=tld"
13 FILTER='(&(|(objectclass=person)(objectclass=group))(&(|(mail=*)(proxyAddresses=smtp:*))(!(|(mail=HealthMailbox*)(mail=FederatedEmail.*)(mail=Migration.*)(mail=MsExchDiscoveryMailbox*)(proxyAddresses=smtp:SystemMailbox*))))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))'
14 ATTRIBUTES="objectClass=\* dn mail proxyAddresses"
15
16 ### QUICK CLI OPTIONS
17 if [ -n "$1" ]; then
18 OPTIONS="$@"
19 else
20 OPTIONS="'$FILTER' $ATTRIBUTES"
21 fi
22
23 ### MAIN
24 ldapsearch \
25 -h "$HOST" \
26 -xWD "$BIND_DN" \
27 -b "$BASE_DN" \
28 $OPTIONS
Virtual aliases
/etc/postfix/virtual_alias_domains
/etc/postfix/virtual
1 ### /etc/postfix/virtual
2 # SEPARATE DOMAINS, NON-UNIX-ACCOUNTS, ONLY ALIASES
3 # SEE:
4 # * /etc/postfix/virtual_mailbox_domains
5 # * /etc/postfix/virtual_alias_domains
6 # DOCS:
7 # * /usr/share/doc/postfix/VIRTUAL_README.gz
8 # * man 5 virtual
9 #
10 # IF RHS RESULT/DST IS A DOMAINLESS USER $myorigin IS APPENDED.
11
12 ### FULL DOMAIN ALIASES
13 @rockstable.org @rockstable.it
man -P "less -p '^RESULT ADDRESS REWRITING'" 5 virtual
The lookup result is subject to address rewriting:
- When the result has the form @otherdomain, the result becomes the same user in otherdomain. This works only for the first address in a multi-address lookup result.
- …
When using full-domain aliases, catch-alls or wildcards … you name it, please be sure that to not become a source of back-scatter. The mail-server will accept any mail and send DSNs to the often forged senders.
Statically generated netiquette aliases (script)
/usr/local/sbin/postfix_virtual_netiquette.sh
1 #!/bin/bash
2
3 while read DOMAIN; do
4 echo '###'"$DOMAIN"
5 ALIASES=( abuse hostmaster noc postmaster privacy root security webmaster )
6 DOMAIN_PRI="primary.tld"
7 for LOCAL in "${ALIASES[@]}"; do
8 echo "$LOCAL@$DOMAIN $LOCAL@$DOMAIN_PRI"
9 done
10 done <<< "$(awk '{print $1}' /etc/postfix/virtual_alias_domains \
11 |grep -v -e '^\s*#' -e '^\s*$' )" \
12 |column -t \
13 |sed 's/###/\n###/'
Use it in
/etc/postfix/main.cf
1 ### ALIASED DOMAINS AND ADDRESSES
2 ### POINTING TO REMOTE DOMAINS
3 #virtual_maps =
4 virtual_maps =
5 hash:/etc/postfix/virtual_netiquette
6 ,hash:/etc/postfix/virtual
7 virtual_alias_maps = $virtual_maps
8 #virtual_alias_domains = $virtual_alias_maps
9 virtual_alias_domains = hash:/etc/postfix/virtual_alias_domains
Create the database
Reload the postfix mailsystem
1 postfix reload
Dynamically filtered netiquette aliases (pcre)
If you want to translate the netiquette aliases of every domain on a given server you may minimize the management-effort even further using regular expressions.
Install postfix support for Perl compatible regular expressions (PCRE)
1 apt install postfix-pcre
The very sophisticated regex is from
https://www.regular-expressions.info/email.html
Thanks a lot for your explainations and your well thought examples!
It was adepted a bit to match only the explicitly specified local parts (with a local extension separated by +).
/etc/postfix/virtual_netiquette_pcre
This solution has the problem, that if you writing to another 'webmaster@…' will ultimativly land in your mailbox.
So we have to filter the domain part, too. :-/
Gather all domains and join them with '|'
So we leave away all the magic on length checking and simply write.
/etc/postfix/virtual_netiquette_pcre
Use it in
/etc/postfix/main.cf
1 ### ALIASED DOMAINS AND ADDRESSES
2 ### POINTING TO REMOTE DOMAINS
3 #virtual_maps =
4 virtual_maps =
5 pcre:/etc/postfix/virtual_netiquette_pcre
6 ,hash:/etc/postfix/virtual
7 virtual_alias_maps = $virtual_maps
8 #virtual_alias_domains = $virtual_alias_maps
9 virtual_alias_domains = hash:/etc/postfix/virtual_alias_domains
Create the database
1 postmap /etc/postfix/virtual_netiquette_pcre
You may test it with the #statically generated netiquette aliases.
1 ### EITHER WITH A EXISTING FILE AS INPUT
2 awk '{print $1}' \
3 |postmap -q - pcre:/etc/postfix/virtual_netiquette_pcre \
4 |sort \
5 |column -t
6 ### OR WITH A DYNAMICALLY CREATED INPUT
7 postfix_virtual_netiquette.sh \
8 |awk '{print $1}' \
9 |postmap -q - pcre:/etc/postfix/virtual_netiquette_pcre \
10 |sort \
11 |column -t
Reload the postfix mailsystem
1 postfix reload
Address verify cache
1 ### ADDRESS VERIFY CACHE
2 address_verify_map = btree:$data_directory/verify_cache
3 #address_verify_negative_cache = yes
4 address_verify_negative_cache = no
5 #address_verify_negative_expire_time = 3d
6 #address_verify_negative_refresh_time = 3h
7 #address_verify_positive_expire_time = 31d
8 #address_verify_positive_refresh_time = 7d
9 address_verify_virtual_transport = $virtual_transport
You can look inside into the verify cache with the following command:
1 db_dump -p /var/lib/postfix/verify_cache
Crypto
Certificates
Please take a look at LetsEncrypt
Hardening
1 ### IF SYNTAX HIGHLIGHTING IS MISSING:
2 ## -> "make vim_syntax"
3
4 ciphers_insecure = RC4
5 cipher_suite_minimum = high
6 protocols_insecure = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1
7 tls_security_level = may
8
9
10 ### Cipherlists
11 ##tls_export_cipherlist = aNULL:-aNULL:ALL:+RC4:@STRENGTH
12 ##tls_high_cipherlist = aNULL:-aNULL:ALL:!EXPORT:!LOW:!MEDIUM:+RC4:@STRENGTH
13 ##tls_low_cipherlist = aNULL:-aNULL:ALL:!EXPORT:+RC4:@STRENGTH
14 ##tls_medium_cipherlist = aNULL:-aNULL:ALL:!EXPORT:!LOW:+RC4:@STRENGTH
15 ##tls_null_cipherlist = eNULL:!aNULL
16
17
18 ### LMTP Client
19 #lmtp_enforce_tls (default: no)
20 #lmtp_sasl_tls_security_options (default: $lmtp_sasl_security_options)
21 #lmtp_sasl_tls_verified_security_options (default: $lmtp_sasl_tls_security_options)
22 #lmtp_starttls_timeout (default: 300s)
23 #lmtp_tls_CAfile (default: empty)
24 #lmtp_tls_CApath (default: empty)
25 #lmtp_tls_block_early_mail_reply (default: empty)
26 #lmtp_tls_cert_file (default: empty)
27 #lmtp_tls_ciphers (default: export)
28 lmtp_tls_ciphers = $cipher_suite_minimum
29 #lmtp_tls_dcert_file (default: empty)
30 #lmtp_tls_dkey_file (default: $lmtp_tls_dcert_file)
31 #lmtp_tls_eccert_file (default: empty)
32 #lmtp_tls_eckey_file (default: empty)
33 #lmtp_tls_enforce_peername (default: yes)
34 #lmtp_tls_exclude_ciphers (default: empty)
35 lmtp_tls_exclude_ciphers = $ciphers_insecure
36 #lmtp_tls_fingerprint_cert_match (default: empty)
37 #lmtp_tls_fingerprint_digest (default: ${{$compatibility_level} <level {3.6} ? {md5} : {sha256}})
38 lmtp_tls_fingerprint_digest = sha1
39 #lmtp_tls_force_insecure_host_tlsa_lookup (default: no)
40 #lmtp_tls_key_file (default: $lmtp_tls_cert_file)
41 #lmtp_tls_loglevel (default: 0)
42 #lmtp_tls_mandatory_ciphers (default: empty)
43 lmtp_tls_mandatory_ciphers = $cipher_suite_minimum
44 #lmtp_tls_mandatory_exclude_ciphers (default: empty)
45 lmtp_tls_mandatory_exclude_ciphers = $ciphers_insecure
46 #lmtp_tls_mandatory_protocols (default: !SSLv2)
47 lmtp_tls_mandatory_protocols = $protocols_insecure
48 #lmtp_tls_note_starttls_offer (default: no)
49 #lmtp_tls_per_site (default: empty)
50 #lmtp_tls_policy_maps (default: empty)
51 #lmtp_tls_protocols (default: empty)
52 lmtp_tls_protocols = $protocols_insecure
53 #lmtp_tls_scert_verifydepth (default: 9)
54 #lmtp_tls_secure_cert_match (default: nexthop)
55 #lmtp_tls_security_level (default: empty)
56 # possible values: none, may, encrypt, dane, dane-only,
57 # fingerprint, verify, secure
58 lmtp_tls_security_level = $tls_security_level
59 #lmtp_tls_session_cache_database (default: empty)
60 #lmtp_tls_session_cache_timeout (default: 3600s)
61 #lmtp_tls_trust_anchor_file (default: empty)
62 #lmtp_tls_verify_cert_match (default: hostname)
63 #lmtp_use_tls (default: no) <- deprecaded with 2.3 -> smtpd_tls_security_level
64
65
66 ### SMTP Client
67 #smtp_enforce_tls (default: no)
68 #smtp_sasl_tls_security_options (default: $smtp_sasl_security_options)
69 #smtp_sasl_tls_verified_security_options (default: $smtp_sasl_tls_security_options)
70 #smtp_starttls_timeout (default: 300s)
71 #smtp_tls_CAfile (default: empty)
72 #smtp_tls_CApath (default: empty)
73 #smtp_tls_block_early_mail_reply (default: no)
74 #smtp_tls_cert_file (default: empty)
75 #smtp_tls_cipherlist (default: empty) <- obsolete
76 #smtp_tls_ciphers (default: export)
77 smtp_tls_ciphers = $cipher_suite_minimum
78 #smtp_tls_dcert_file (default: empty)
79 #smtp_tls_dkey_file (default: $smtp_tls_dcert_file)
80 #smtp_tls_eccert_file (default: empty)
81 #smtp_tls_eckey_file (default: $smtp_tls_eccert_file)
82 #smtp_tls_enforce_peername (default: yes)
83 #smtp_tls_exclude_ciphers (default: empty)
84 smtp_tls_exclude_ciphers = $ciphers_insecure
85 #smtp_tls_fingerprint_cert_match (default: empty)
86 #smtp_tls_fingerprint_digest (default: (default: ${{$compatibility_level} <level {3.6} ? {md5} : {sha256}}))
87 smtp_tls_fingerprint_digest = sha1
88 #smtp_tls_force_insecure_host_tlsa_lookup (default: no)
89 #smtp_tls_key_file (default: $smtp_tls_cert_file)
90 #smtp_tls_loglevel (default: 0)
91 #smtp_tls_mandatory_ciphers (default: medium)
92 smtp_tls_mandatory_ciphers = $cipher_suite_minimum
93 #smtp_tls_mandatory_exclude_ciphers (default: empty)
94 smtp_tls_mandatory_exclude_ciphers = $ciphers_insecure
95 #smtp_tls_mandatory_protocols (default: !SSLv2)
96 smtp_tls_mandatory_protocols = $protocols_insecure
97 #smtp_tls_note_starttls_offer (default: no)
98 #smtp_tls_per_site (default: empty)
99 #smtp_tls_policy_maps (default: empty)
100 #smtp_tls_protocols (default: !SSLv2)
101 smtp_tls_protocols = $protocols_insecure
102 #smtp_tls_scert_verifydepth (default: 9)
103 #smtp_tls_secure_cert_match (default: nexthop, dot-nexthop)
104 #smtp_tls_security_level (default: empty)
105 # possible values: none, may, encrypt, dane, dane-only,
106 # fingerprint, verify, secure
107 smtp_tls_security_level = $tls_security_level
108 #smtp_tls_session_cache_database (default: empty)
109 #smtp_tls_session_cache_timeout (default: 3600s)
110 #smtp_tls_trust_anchor_file (default: empty)
111 #smtp_tls_verify_cert_match (default: hostname)
112 #smtp_use_tls (default: no) <- deprecaded with 2.3 -> smtp_tls_security_level
113
114
115 ### SMTPD Server
116 #smtpd_client_new_tls_session_rate_limit (default: 0)
117 #smtpd_enforce_tls (default: no)
118 #smtpd_sasl_tls_security_options (default: $smtpd_sasl_security_options)
119 #smtpd_starttls_timeout (default: see postconf -d output)
120 #smtpd_tls_CAfile (default: empty)
121 #smtpd_tls_CApath (default: empty)
122 #smtpd_tls_always_issue_session_ids (default: yes)
123 #smtpd_tls_ask_ccert (default: no)
124 #smtpd_tls_auth_only (default: no)
125 #smtpd_tls_ccert_verifydepth (default: 9)
126 #smtpd_tls_cert_file (default: empty)
127 #smtpd_tls_cipherlist (default: empty) <- obsolete
128 #smtpd_tls_ciphers (default: export)
129 smtpd_tls_ciphers = $cipher_suite_minimum
130 #smtpd_tls_dcert_file (default: empty)
131 #smtpd_tls_dh1024_param_file (default: empty)
132 #smtpd_tls_dh512_param_file (default: empty)
133 #smtpd_tls_dkey_file (default: $smtpd_tls_dcert_file)
134 #smtpd_tls_eccert_file (default: empty)
135 #smtpd_tls_eckey_file (default: $smtpd_tls_eccert_file)
136 #smtpd_tls_eecdh_grade (default: see postconf -d output)
137 #smtpd_tls_exclude_ciphers (default: empty)
138 smtpd_tls_exclude_ciphers = $ciphers_insecure
139 #smtpd_tls_fingerprint_digest (default: ${{$compatibility_level} <level {3.6} ? {md5} : {sha256}})
140 smtpd_tls_fingerprint_digest = sha1
141 #smtpd_tls_key_file (default: $smtpd_tls_cert_file)
142 #smtpd_tls_loglevel (default: 0)
143 #smtpd_tls_mandatory_ciphers (default: medium)
144 smtpd_tls_mandatory_ciphers = $cipher_suite_minimum
145 #smtpd_tls_mandatory_exclude_ciphers (default: empty)
146 smtpd_tls_mandatory_exclude_ciphers = $ciphers_insecure
147 #smtpd_tls_mandatory_protocols (default: !SSLv2)
148 smtpd_tls_mandatory_protocols = $protocols_insecure
149 #smtpd_tls_protocols (default: none)
150 smtpd_tls_protocols=$protocols_insecure
151 #smtpd_tls_received_header (default: no)
152 #smtpd_tls_req_ccert (default: no)
153 #smtpd_tls_security_level (default: empty)
154 # possible values: none, may, encrypt
155 smtpd_tls_security_level = $tls_security_level
156 #smtpd_tls_session_cache_database (default: empty)
157 #smtpd_tls_session_cache_timeout (default: 3600s)
158 #smtpd_tls_wrappermode (default: no)
159 #smtpd_use_tls (default: no) <- deprecaded with 2.3 -> smtpd_tls_security_level
You can delete all these comments in vim to get a clean version by:
1 :g/^#[^#]/d
1 ### Customs
2 ciphers_insecure = RC4:aNULL
3 cipher_suite_minimum = high
4 protocols_insecure = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1
5 tls_security_level = may
6
7
8 ### Cipherlists
9 ##tls_export_cipherlist = aNULL:-aNULL:ALL:+RC4:@STRENGTH
10 ##tls_high_cipherlist = aNULL:-aNULL:ALL:!EXPORT:!LOW:!MEDIUM:+RC4:@STRENGTH
11 ##tls_low_cipherlist = aNULL:-aNULL:ALL:!EXPORT:+RC4:@STRENGTH
12 ##tls_medium_cipherlist = aNULL:-aNULL:ALL:!EXPORT:!LOW:+RC4:@STRENGTH
13 ##tls_null_cipherlist = eNULL:!aNULL
14
15
16 ### LMTP Client
17 lmtp_tls_ciphers = $cipher_suite_minimum
18 lmtp_tls_exclude_ciphers = $ciphers_insecure
19 #lmtp_tls_fingerprint_digest = sha256
20 lmtp_tls_mandatory_ciphers = $cipher_suite_minimum
21 lmtp_tls_mandatory_exclude_ciphers = $ciphers_insecure
22 lmtp_tls_mandatory_protocols = $protocols_insecure
23 lmtp_tls_protocols = $protocols_insecure
24 lmtp_tls_security_level = $tls_security_level
25
26
27 ### SMTP Client
28 smtp_tls_ciphers = $cipher_suite_minimum
29 smtp_tls_exclude_ciphers = $ciphers_insecure
30 #smtp_tls_fingerprint_digest = sha265
31 smtp_tls_mandatory_ciphers = $cipher_suite_minimum
32 smtp_tls_mandatory_exclude_ciphers = $ciphers_insecure
33 smtp_tls_mandatory_protocols = $protocols_insecure
34 smtp_tls_protocols = $protocols_insecure
35 smtp_tls_security_level = $tls_security_level
36
37
38 ### SMTPD Server
39 smtpd_tls_ciphers = $cipher_suite_minimum
40 smtpd_tls_exclude_ciphers = $ciphers_insecure
41 #smtpd_tls_fingerprint_digest = sha256
42 smtpd_tls_mandatory_ciphers = $cipher_suite_minimum
43 smtpd_tls_mandatory_exclude_ciphers = $ciphers_insecure
44 smtpd_tls_mandatory_protocols = $protocols_insecure
45 smtpd_tls_protocols=$protocols_insecure
46 smtpd_tls_security_level = $tls_security_level
Perfect forward secrecy (PFS)
Diffie-Hellman-Parameters
First generate Diffie-Hellmann parameters and return here afterwards: OpenSSL#Generate or renew files with Diffie-Hellman-Parameters
Enable PFS in postfix
Defaults
1 smtpd_tls_dh1024_param_file =
2 smtpd_tls_dh512_param_file =
3 smtpd_tls_eecdh_grade = auto
4 tls_eecdh_auto_curves = X25519 X448 prime256v1 secp521r1 secp384r1
5 tls_eecdh_strong_curve = prime256v1
6 tls_eecdh_ultra_curve = secp384r1
7 tlsproxy_tls_dh1024_param_file = $smtpd_tls_dh1024_param_file
8 tlsproxy_tls_dh512_param_file = $smtpd_tls_dh512_param_file
9 tlsproxy_tls_eecdh_grade = $smtpd_tls_eecdh_grade
Hardened
Test
testssl.sh via ansible
Examples
Submission
Prepare postfix for dovecot submission:
Because dovecot-submissiond binds to tcp/587 we need to add an alternate submission port to /etc/services.
Now we configure /etc/postfix/master.cf to fire up a submission-service bound to localhost.
Postgrey
Wenn eine Email an den Mail-Server versendet zugestellt wird, trägt "postgrey" ein Tripel aus (Client, Sender, Recipient) zusammen mit einem Timestamp in die Datenbank "/var/lib/postgrey/postgrey.db" ein, sollte dieses noch nicht dort zu finden sein. Postgrey versendet einem solchen unbestätigten Client dann ein Return-Code 450 "Try again later". Ist ein konfigurierbares Delay (Default: 300s) erreicht, so ist es dem Client erneut gestattet die Email einzuliefern. Die Datenbank wird standardmäßig von Triplets befreit, zu welchen es seit 35d keinen Kontakt mehr gab. Postgrey stellt Whitelists für clients und recipients bereit.
Ich halte postgrey im Produktivbetrieb für absolut einsetzbar, denn nur die erste Email eines Triplets wird um 5min verzögert.
Evtl. kann das Bereinigungsintervall auf einen höheren Wert gesetzt werden, um seltene Prozesse zu beschleunigen oder interne Server von denen ständig wechselnde Email-Adressen kommt, in die Whitelist eingetragen werden.
About Postgrey
Configuration
Database unter /var/lib/postgrey/postgrey.db
- Defaults
- delay: 300s
- max-age: 35d
- bind-socket: 127.0.0.1:10023
/etc/default/postgrey
1 # postgrey startup options, created for Debian
2
3 # you may want to set
4 # --delay=N how long to greylist, seconds (default: 300)
5 # --max-age=N delete old entries after N days (default: 35)
6 # see also the postgrey(8) manpage
7
8 POSTGREY_OPTS="--inet=127.0.0.1:10023"
9
10 # the --greylist-text commandline argument can not be easily passed through
11 # POSTGREY_OPTS when it contains spaces. So, insert your text here:
12 #POSTGREY_TEXT="Your customized rejection message here"
13
Inclusion via restriction in /etc/postfix/main.cf
Whitelists
ls -1 /etc/postgrey/*
Whitelist format for recipient addresses
domain.addr "domain.addr" domain and subdomains. name@ "name@.*" and extended addresses "name+blabla@.*". name@domain.addr "name@domain.addr" and extended addresses. /regexp/ anything that matches "regexp" (the full address is matched).
Whitelist format for client addresses
domain.addr "domain.addr" domain and subdomains. IP1.IP2.IP3.IP4 IP address IP1.IP2.IP3.IP4. You can also leave off one number, in which case only the first specified numbers will be checked. IP1.IP2.IP3.IP4/MASK CIDR-syle network. Example: 192.168.1.0/24 /regexp/ anything that matches "regexp" (the full address is matched).
policy-weightd
policyd-weight(8) is a SMTP policy daemon written in perl(1) for postfix(1). It evaluates based on RBL/RHSBL results, HELO and MAIL FROM domain and subdomain arguments and the client IP address the possibility of forgery or SPAM. It is designed to be called before the SMTP DATA command at the RCPT TO stage. This way it is a) possible to reject a mail attempt before the body has been received and b) to keep multirecipient mail intact, i.e. provide the functionality of selective usage based on recipients.
Configuration
/etc/policyd-weight.conf
1 #policyd-weight uses a perl(1) style configuration file which it reads
2 #on startup. The cache re-reads the configuration after $MAINTE‐
3 #NANCE_LEVEL (default: 5) queries. If -f is not specified, it searches
4 #for configuration files on following locations:
5
6
7 #CACHE SETTINGS
8 # $CACHESIZE (default: 2000)
9 # Set the minimum size of the SPAM cache.
10 # $CACHEMAXSIZE (default: 4000)
11 # Set the maximum size of the SPAM cache.
12 # $CACHEREJECTMSG
13 # (default: 550 temporarily blocked because of previous errors)"
14 # Set the SMTP status code and a explanatory message for rejected
15 # mails due to cached results
16 # $NTTL (default: 1)
17 # The client is penalized for that many retries.
18 $NTTL=10;
19 # $NTIME (default: 30)
20 # The $NTTL counter will only be decremented if the client waits
21 # at least $NTIME seconds.
22 $NTIME=5;
23 # $POSCACHESIZE (default: 1000)
24 # Set the minimum size of the HAM cache.
25 # $POSCACHEMAXSIZE (default: 2000)
26 # Set the maximum size of the HAM cache.
27 # $PTTL (default: 60)
28 # After that many queries the HAM entry must succeed one run
29 # through the RBL checks again.
30 # $PTIME (default: 3h)
31 # after $PTIME in HAM Cache the client must pass one time the RBL
32 # checks again. Values must be nonfractal. Accepted time-units:
33 # s(econds), m(inutes), h(ours), d(ays)
34 # $TEMP_PTIME (default: 1d)
35 # The client must pass this time the RBL checks in order to be
36 # listed as hard-HAM. After this time the client will pass immedi‐
37 # ately for PTTL within PTIME. Values must be non-fractal.
38 # Accepted time-units: s(econds), m(inutes), h(ours), d(ays)
39
40
41 #DEBUG SETTINGS
42 # $DEBUG (default: 0)
43 # Turn debugging on (1) or off (0)
44
45
46
47 #DNS SETTINGS
48 # $DNS_RETRIES (default: 2)
49 # How many times a single DNS query may be repeated
50 # $DNS_RETRY_IVAL (default: 2)
51 # Retry a query without response after that many seconds
52 # $MAXDNSERR (default: 3)
53 # If that many queries fail, the mail is accepted with $MAXDNSER‐
54 # RMSG.
55 # In total DNS queries this means: $MAXDNSERR * $DNS_RETRIES
56
57
58 #MISC SETTINGS
59 # $MAINTENANCE_LEVEL (default: 5)
60 # After that many policy requests the cache (and in daemon mode
61 # child processes) checks for configuration file changes
62 # $MAXIDLECACHE (default: 60)
63 # After that many seconds of being idle the cache checks for con‐
64 # figuration file changes.
65 # $PIDFILE (default: /var/run/policyd-weight.pid)
66 # Path and filename to store the master pid (daemon mode)
67 # $LOCKPATH (default: /tmp/.policyd-weight/)
68 # Directory where policyd-weight stores sockets and lock-
69 # files/directories. Its argument must contain a trailing slash.
70 # $SPATH (default: $LOCKPATH.'/polw.sock')
71 # Path and filename which the cache has to use for communication.
72 # $TCP_PORT (default: 12525)
73 # TCP port on which the policy server listens (daemon mode)
74 # $BIND_ADDRESS (default: '127.0.0.1')
75 # IP Address on which policyd-weight binds. Currently either only
76 # one or all IPs are supported. Specify 'all' if you want to lis‐
77 # ten on all IPs.
78 # $SOMAXCONN (default: 1024)
79 # Maximum connections which policyd-weight accepts. This is set
80 # high enough to cover most scenarios.
81 # $USER (default: polw)
82 # Set the user under which policyd-weight runs
83 # $GROUP (default: $USER)
84 # Set the group under which policyd-weight runs
85
86
87 #OUTPUT AND LOG SETTINGS
88 # $ADD_X_HEADER (default: 1)
89 # Insert a X-policyd-weight: header with evaluation messages.
90 # 1 = on, 0 = off
91 # $LOG_BAD_RBL_ONLY (default: 1)
92 # Insert only RBL results in logging strings if the RBL score
93 # changes the overall score. Thus RBLs with a GOOD SCORE of 0
94 # don't appear in logging strings if the RBL returned no BAD hit.
95 # 1 = on, 0 = off
96 # $MAXDNSBLMSG (default: 550 Your MTA is listed in too many DNSBLs)
97 # The message sent to the client if it was reject due to $MAXDNS‐
98 # BLHITS and/or $MAXDNSBLSCORE.
99 # $REJECTMSG (default: 550 Mail appeared to be SPAM or forged. Ask your
100 # Mail/DNS-Adminisrator to correct HELO and DNS MX settings or to get
101 # removed from DNSBLs)
102 # Set the SMTP status code for rejected mails and a message why
103 # the action was taken
104
105
106
107 #RESOURCE AND OPTIMIZATIONS
108 # $CHILDIDLE (default: 120)
109 # How many seconds a child may be idle before it dies (daemon
110 # mode)
111 # $MAX_PROC (default: 50)
112 # Process limit on how many processes policyd-weight will spawn
113 # (daemon mode)
114 # $MIN_PROC (default: 2)
115 # Minimum child processes which are kept alive in idle times (dae‐
116 # mon mode)
117 # $PUDP (default: 0)
118 # Set persistent UDP connections used for DNS queries on (1) or
119 # off (0).
120
121
122
123 #SCORE SETTINGS
124 # Positive values indicate a bad (SPAM) score, negative values indicate a
125 # good (HAM) score.
126 # @bogus_mx_score (2.1, 0)
127 # If the sender domain has neither MX nor A records or these
128 # records resolve to a bogus IP-Address (for instance private net‐
129 # works) then this check asigns the full score of bogus_mx_score.
130 # If there is no MX but an A record of the sender domain then it
131 # receives a penalty only if DNSBL-listed.
132 # Log Entries:
133 # BOGUS_MX
134 # The sender A and MX records are bogus or empty.
135 # BAD_MX
136 # The sender domain has an empty or bogus MX record and the
137 # client is DNSBL listed.
138 # Related RFCs:
139 # [1918] Address Allocation for Private Internets
140 # [2821] Simple Mail Transfer Protocol (Sect 3.6 and Sect 5)
141 # @client_ip_eq_helo_score (1.5, -1.25)
142 # Define scores for the match of the reverse record (hostname)
143 # against the HELO argument. Reverse lookups are done, if the for‐
144 # ward lookups failed and are not trusted.
145 # Log Entries:
146 # REV_IP_EQ_HELO
147 # The Client's PTR matched the HELO argument.
148 # REV_IP_EQ_HELO_DOMAIN
149 # Domain portions of Client PTR and HELO argument matched.
150 # RESOLVED_IP_IS_NOT_HELO
151 # Client PTRs found but did not match HELO argument.
152 # @helo_score (1.5, -2)
153 # Define scores for the match of the Client IP and its /24 subnet
154 # against the A records of HELO or MAIL FROM domain/host. It also
155 # holds the bad score for MX verifications.
156 # Log Entries:
157 # CL_IP_EQ_HELO_NUMERIC
158 # Client IP matches the [IPv4] HELO.
159 # CL_IP_EQ_FROM_IP
160 # Client IP matches the A record of the MAIL FROM sender
161 # domain/host.
162 # CL_IP_EQ_HELO_IP
163 # Client IP matches the A record of the HELO argument.
164 # CL_IP_NE_HELO
165 # The IP and the /24 subnet did not match A/MX records of
166 # HELO and MAIL FROM arguments and their subdomains.
167 # @helo_from_mx_eq_ip_score (1.5, -3.1)
168 # Define scores for the match of Client IP against MX records.
169 # Positive (SPAM) values are used in case the MAIL FROM matches
170 # not the HELO argument AND the client seems to be dynamic AND the
171 # client is no MX for HELO and MAIL FROM arguments. The total
172 # DNSBL score is added to its bad score.
173 # Log Entries:
174 # CL_IP_EQ_FROM_MX
175 # Client IP matches the MAIL FROM domain/host MX record
176 # CL_IP_EQ_HELO_MX
177 # Client IP matches the HELO domain/host MX record
178 # CLIENT_NOT_MX/A_FROM_DOMAIN
179 # Client is not a verified HELO and doesn't match A/MX records
180 # of MAIL FROM argument
181 # CLIENT/24_NOT_MX/A_FROM_DOMAIN
182 # Client's subnet does not match A/MX records of the MAIL FROM
183 # argument
184 # $dnsbl_checks_only (default: 0)
185 # Disable HELO/RHSBL verifications and the like. Do only RBL
186 # checks.
187 # 1 = on, 0 = off
188 # @dnsbl_score (default: see below)
189 # A list of RBLs to be checked. If you want that a host is not
190 # being evaluated any further if it is listed on several lists or
191 # a very trustworthy list you can control a immediate REJECT with
192 # $MAXDNSBLHITS and/or $MAXDNSBLSCORE. A list of RBLs must be
193 # build as follows:
194 # @dnsbl_score = (
195 # RBLHOST1, HIT SCORE, MISS SCORE, LOG NAME,
196 # RBLHOST2, HIT SCORE, MISS SCORE, LOG NAME,
197 # ...
198 # );
199 # The default is:
200 # @dnsbl_score = (
201 # "dynablock.njabl.org", 3.25, 0, "DYN_NJABL",
202 # "dnsbl.njabl.org", 4.25, -1.5, "BL_NJABL",
203 # "bl.spamcop.net", 1.75, -1.5, "SPAMCOP",
204 # "sbl-xbl.spamhaus.org", 4.35, -1.5, "SBL_XBL_SPAMHAUS",
205 # "list.dsbl.org", 4.35, 0, "DSBL_ORG",
206 # "ix.dnsbl.manitu.net", 4.35, 0, "IX_MANITU",
207 # "relays.ordb.org", 3.25, 0, "ORDB_ORG"
208 # );
209 # @rhsbl_score (default: see below)
210 # Define a list of RHSBL host which are queried for the sender
211 # domain. Results get additionally scores of 0.5 * DNSBL results
212 # and @rhsbl_penalty_score. A list of RHSBL hosts to be queried
213 # must be build as follows:
214 # @rhsbl_score = (
215 # RHSBLHOST1, HIT SCORE, MISS SCORE, LOG NAME,
216 # RHSBLHOST2, HIT SCORE, MISS SCORE, LOG NAME,
217 # ...
218 # );
219 # The default is:
220 # @rhsbl_score = (
221 # "rhsbl.ahbl.org", 1.8, 0, "AHBL",
222 # "dsn.rfc-ignorant.org", 3.2, 0, "DSN_RFCI",
223 # "postmaster.rfc-ignorant.org", 1 , 0, "PM_RFCI",
224 # "abuse.rfc-ignorant.org", 1, 0, "ABUSE_RFCI"
225 # );
226 # @rhsbl_penalty_score (3.1, 0)
227 # This score will be added to each RHSBL hit if following crite‐
228 # rias are met:
229 # Sender has a random local-part (i.e. yztrzgb@example.tld)
230 # or MX records of sender domain are bogus
231 # or FROM matches not HELO
232 # or HELO is untrusted (Forward record matched, reverse record
233 # did not match)
234 # $MAXDNSBLHITS (default: 2)
235 # If the client is listed in more than $MAXDNSBLHITS RBLs it will
236 # be rejected immediately with $MAXDNSBLMSG and without further
237 # evaluation. Results are cached by default.
238 # $MAXDNSBLSCORE (default: 8)
239 # If the BAD SCOREs of @dnsbl_score listed RBLs reach a level
240 # greater than $MAXDNSBLSCORE the client will be rejected immedi‐
241 # ately with $MAXDNSBLMSG and without further evaluation. Results
242 # are cached by default.
243 # $REJECTLEVEL (default: 1)
244 # Score results equal or greater than this level will be rejected
245 # with $REJECTMSG
246
Defaults
1 ## This is a POSIX shell fragment sourced by /etc/init.d/policyd-weight
2 ## variable: DAEMON_OPTS
3 ## daemon options to policyd-weight, possible options:
4 ##
5 ## Options
6 ## -D Don't detach master - run master in foreground
7 ## -d Debug, don't daemonize, log to STDOUT
8 ## -f /path/to/file Specify a configuration file
9 ## -h This help
10 ## -k Kill cache instance
11 ## -s Show cache entries and exit. With -d show debug
12 ## cache entries
13 ## -v Show version and exit
14 ##
15 ## default: unset
16 DAEMON_OPTS="-f /etc/policyd-weight.conf"
Inclusion via restriction in /etc/postfix/main.cf
rspamd
For installation see rspamd
/etc/postfix/main.cf
1 ### RSPAMD
2
3 #smtpd_milters = unix:/var/lib/rspamd/milter.sock
4 # or for TCP socket
5 smtpd_milters = inet:localhost:11332
6 non_smtpd_milters = inet:localhost:11332
7
8 # skip mail without checks if something goes wrong
9 milter_default_action = accept
10
11 # 6 is the default milter protocol version;
12 # prior to Postfix 2.6 the default protocol was 2.
13 # milter_protocol = 6
14
15 milter_default_action = accept
16 milter_mail_macros = i {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}
17 #milter_mail_macros = i {auth_authen} {client_addr} {client_name} {mail_addr}
Test cases
An installation should be well tested. This is a catalogue to gather and remind the cases and scenarios.
- Reboot safety
- Availability
- Ports
- Receive
- Netiquette (e.g. postmaster)
- Generic user
- Send
- Internal
- External
- Relay
- Reject
- Relay
- Invalid receipient
- Queue
- Mass mail
- Remote MX unreachable
- Security
- Filtering
- Valid crypto (ciphers, certs, …)
- Anti spam (rspamd)
- DKIM signing
- Greylisting
- Anti-virus (Eicar, …)
- Anti-macro (OLEFY, …)