19/12/2011
Rate limit outgoing emails from PHP web applications using postfix and policyd
One of the worst things a webmaster or a anyone else that runs some web application can do, is to constantly send “informative newsletters” to people. Most CMS applications make it really easy to send such emails. These are 99% spam, and as such there are many good reasons that you should limit the amount of such outgoing “newsletters” coming out of your email server. Else there’s a good chance you might get added to a blacklist, and you don’t want your legitimate clients to have their emails blocked because of some irresponsible people. I recently had to deploy such a solution to a hosting server that serves multiple (>300) domains. The server already ran postfix, so I had to implement something useful around it.
The problem with postfix is that you can’t really rate-limit the outgoing queue per sender domain/address. There are only generic settings that control the general mail server’s capabilities of sending emails. What I wanted though is to have the ability to restrict specific domains to some specific email message count per day. This is something that a postfix addon named postfix-policyd can do by deferring/greylisting, but still just on the incoming queue. One would think that the problems would be solved by just applying this, but truth is that they don’t. Applying a defer/greylisting policy on the incoming queue is fine while the client on the remote side is another SMTP server that can happily store the deferred email on its queue and retry some minutes/hours later. What happens though if the SMTP client is a PHP application that connects through the mail() function ? There you have no queue and if you defer a message at the SMTP server it will get forever lost, PHP can’t resend it. So the solution would be to apply an intermediate SMTP queue between PHP and the primary SMTP server, that is another local postfix installation that would only serve as a queue that relays emails to the primary.
Using a “simple” diagram sending an email from PHP should follow this path upon a successful installation:
PHP mail() –(sendmail binary)–> intermediate_POSTFIX –(SMTP relay)–> POSTFIX –(smtpd_sender_restrictions)–> POLICYD –(pickup)–> POSTFIX –(SMTP)–> REMOTE SERVER
Here are the steps I took on a Debian Squeeze server to install this little monster.
1. Create a new postfix configuration directory for the new intermediate postfix instance
I named my intermediate postfix config dir as postfix2525, name comes from the port that it will listen on but you can definitely be more creative.
# mkdir /etc/postfix2525 # cp -av /etc/postfix /etc/postfix2525
Remove everything from /etc/postfix2525/main.cf and just add the following lines:
data_directory = /var/lib/postfix2525 queue_directory = /var/spool/postfix2525 relayhost = 127.0.0.1:12525
This defines a new data and queue directory and instructs this postfix to relay all emails through another one that listens on the localhost, the primary one, on port 12525. More about this port later when you will create some special config on the primary postfix.
Remove previous contents of /etc/postfix2525/master.cf and just add these lines:
127.0.0.1:2525 inet n - - - 2 smtpd -o syslog_name=postfix2525 pickup fifo n - - 60 1 pickup cleanup unix n - - - 0 cleanup qmgr fifo n - n 300 1 qmgr #qmgr fifo n - - 300 1 oqmgr tlsmgr unix - - - 1000? 1 tlsmgr rewrite unix - - - - - trivial-rewrite bounce unix - - - - 0 bounce defer unix - - - - 0 bounce trace unix - - - - 0 bounce verify unix - - - - 1 verify flush unix n - - 1000? 0 flush proxymap unix - - n - - proxymap proxywrite unix - - n - 1 proxymap smtp unix - - - - - smtp # When relaying mail as backup MX, disable fallback_relay to avoid MX loops relay unix - - - - - smtp -o smtp_fallback_relay= # -o smtp_helo_timeout=5 -o smtp_connect_timeout=5 showq unix n - - - - showq error unix - - - - - error retry unix - - - - - error discard unix - - - - - discard local unix - n n - - local virtual unix - n n - - virtual lmtp unix - - - - - lmtp anvil unix - - - - 1 anvil scache unix - - - - 1 scache
Obviously the most important part here is the first line. It defines that this postfix instance will listen for SMTP connections on localhost, port 2525 and it’s syslog output name will be postfix2525 so that it’s easier to tell apart which SMTP instance spits which errors.
After this is done you need to run the following command that will create all necessary directories with their proper permissions.
# postfix -c /etc/postfix2525/ check
Also make sure you add the following line to the main.cf file of your main postfix installation:
alternate_config_directories = /etc/postfix2525
You will also need a new init script. Since the script by itself is quite big and there are only a few lines that actually differ, I will post my diff here:
--- /etc/init.d/postfix 2011-05-04 21:17:47.000000000 +0200 +++ /etc/init.d/postfix2525 2011-12-19 19:22:09.000000000 +0100 @@ -17,8 +17,10 @@ # Description: postfix is a Mail Transport agent ### END INIT INFO +CONFDIR=/etc/postfix2525 PATH=/bin:/usr/bin:/sbin:/usr/sbin DAEMON=/usr/sbin/postfix +DAEMON_OPTIONS="-c /etc/postfix2525" NAME=Postfix TZ= unset TZ @@ -28,13 +30,13 @@ test -f /etc/default/postfix && . /etc/default/postfix -test -x $DAEMON && test -f /etc/postfix/main.cf || exit 0 +test -x $DAEMON && test -f /etc/postfix2525/main.cf || exit 0 . /lib/lsb/init-functions #DISTRO=$(lsb_release -is 2>/dev/null || echo Debian) running() { - queue=$(postconf -h queue_directory 2>/dev/null || echo /var/spool/postfix) + queue=$(postconf -c $CONFDIR -h queue_directory 2>/dev/null || echo /var/spool/postfix2525) if [ -f ${queue}/pid/master.pid ]; then pid=$(sed 's/ //g' ${queue}/pid/master.pid) # what directory does the executable live in. stupid prelink systems. @@ -66,7 +68,7 @@ fi # see if anything is running chrooted. - NEED_CHROOT=$(awk '/^[0-9a-z]/ && ($5 ~ "[-yY]") { print "y"; exit}' /etc/postfix/master.cf) + NEED_CHROOT=$(awk '/^[0-9a-z]/ && ($5 ~ "[-yY]") { print "y"; exit}' /etc/postfix2525/master.cf) if [ -n "$NEED_CHROOT" ] && [ -n "$SYNC_CHROOT" ]; then # Make sure that the chroot environment is set up correctly. @@ -111,7 +113,7 @@ umask $oldumask fi - if start-stop-daemon --start --exec ${DAEMON} -- quiet-quick-start; then + if start-stop-daemon --start --exec ${DAEMON} -- ${DAEMON_OPTIONS} quiet-quick-start; then log_end_msg 0 else log_end_msg 1 @@ -123,7 +125,7 @@ RUNNING=$(running) log_daemon_msg "Stopping Postfix Mail Transport Agent" postfix if [ -n "$RUNNING" ]; then - if ${DAEMON} quiet-stop; then + if ${DAEMON} ${DAEMON_OPTIONS} quiet-stop; then log_end_msg 0 else log_end_msg 1
If everything went well up to now you should be able to start your new postfix instance and check that it is actually running.
# /etc/init.d/postfix2525 start # netstat -antp | grep 2525 tcp 0 0 127.0.0.1:2525 0.0.0.0:* LISTEN 6138/master
2. Configure main postfix to accept emails from the intermediate
Edit /etc/postfix/master.cf and add this line at the bottom:
127.0.0.1:12525 inet n - - - - smtpd -o smtp_fallback_relay= -o smtpd_client_restrictions= -o smtpd_helo_restrictions= -o smtpd_recipient_restrictions=permit_mynetworks,reject -o smtpd_data_restrictions= -o receive_override_options=no_unknown_recipient_checks
This defines a special port for the main postfix instance that has (or maybe it hasn’t actually) some special restrictions.
Actually you will have to change this line later on upon installing postfix-policyd, but this should be good enough for now, in order for you to do some testing.
Restart postfix
# /etc/init.d/postfix restart # netstat -antp | grep 2525 tcp 0 0 127.0.0.1:12525 0.0.0.0:* LISTEN 26799/master tcp 0 0 127.0.0.1:2525 0.0.0.0:* LISTEN 6138/master
The intermediate postfix listens on 127.0.0.1:2525 and the main one has another special listening port on 127.0.0.1:12525.
3. Test your intermediate postfix instance
You can do this in a gazillion different ways. One of my favorite ways to test SMTP connectivity is through telnet (—> shows data entry):
# telnet localhost 2525 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. 220 server.mydomain.gr ESMTP Postfix ---> EHLO koko.gr 250-server.mydomain.gr 250-PIPELINING 250-SIZE 10240000 250-VRFY 250-ETRN 250-ENHANCEDSTATUSCODES 250-8BITMIME 250 DSN ---> MAIL FROM: lala@koko.gr 250 2.1.0 Ok ---> RCPT TO: koko@destination.gr 250 2.1.5 Ok ---> DATA 354 End data with <CR><LF>.<CR><LF> ---> THIS IS A TEST ---> . 250 2.0.0 Ok: queued as C41E21C84FF ---> quit
If you were keeping an eye on syslog messages you should have seen some connection messages both from postfix2525 and from postfix. If everything went well your email _should_ have arrived at it’s destination. If this is true then your primary postfix instance now works as a relay for your intermediate queue.
Don’t read the next parts of this post if you haven’t previously managed this step!
4. Install and configure postfix-policyd
# aptitude install postfix-policyd
To run policyd you need to create a database and import policyd SQL schema to it. Your distro has probably already taken care of the previous step, if it hasn’t…do it manually and think about changing distro!
Then edit the config file usually located at /etc/postfix-policyd.conf. The options I chose to play with were the following:
SENDERTHROTTLE=1 SENDER_THROTTLE_SASL=1 SENDER_THROTTLE_HOST=0
Since all emails will be relayed through localhost there’s no point in throttling per host, what is needed is throttling per envelope sender.
You should manually review your desired limits though. I won’t post mine here because everyone has different needs and there’s no sane config for everyone.
Start postfix-policyd
# /etc/init.d/postfix-policyd start
If you get weird startup errors like:
postfix-policyd: fatal: didn't find priority 'LOG_IFOO', exiting
Edit /etc/postfix-policyd.conf, find the following line:
SYSLOG_FACILITY="LOG_MAIL | LOG_INFO"
and change it to (mind the removed spaces):
SYSLOG_FACILITY="LOG_MAIL|LOG_INFO"
5. Configure main postfix instance to use postifix-policyd
Edit /etc/postfix/main.cf and add this:
webclient_restrictions = check_policy_service inet:127.0.0.1:10031
Then edit /etc/postfix/master.cf again and change the line you had previously added to the bottom of the file with this:
127.0.0.1:12525 inet n - - - - smtpd -o smtp_fallback_relay= -o smtpd_client_restrictions= -o smtpd_helo_restrictions= -o smtpd_recipient_restrictions=permit_mynetworks,reject -o smtpd_data_restrictions= -o receive_override_options=no_unknown_recipient_checks -o smtpd_sender_restrictions=${webclient_restrictions}
The difference is
-o smtpd_sender_restrictions=${webclient_restrictions}
which practically instructs postfix to use postfix-policyd for emails that arrive on port 12525, which is the port that the intermediate postfix instance uses to relay all emails.
6. Test your intermediate postfix instance again
If everything went well, the main postfix instance should now be able to enforce sender policies. Try sending a new email through the intermediate postfix again, yes using telnet, and you should pickup some new log lines at your syslog:
Dec 19 21:56:40 myserver postfix-policyd: connection from: 127.0.0.1 port: 45635 slots: 0 of 4096 used
Dec 19 21:56:40 myserver postfix-policyd: rcpt=5, greylist=new, host=127.0.0.1 (unknown), from=lala@koko.gr, to=koko@lalala.gr, size=348
Dec 19 21:56:40 myserver postfix/smtpd[9168]: NOQUEUE: reject: RCPT from unknown[127.0.0.1]: 450 4.7.1: Sender address rejected: Policy Rejection- Please try later.; from= to= proto=ESMTP helo=
Dec 19 21:56:40 myserver postfix/smtp[8970]: C41E21C84FF: to=, relay=127.0.0.1[127.0.0.1]:12525, delay=20, delays=20/0/0.01/0, dsn=4.7.1, status=deferred (host 127.0.0.1[127.0.0.1] said: 450 4.7.1 : Sender address rejected: Policy Rejection- Please try later. (in reply to RCPT TO command))
The above means that greylisting through policyd works.
7. make PHP use your new intermediate postfix instance
PHP on linux by default uses the sendmail binary to send emails via the mail() function. That would use the main postfix instance though, so one needs to edit /etc/php/apache2/php.ini and change the following line:
sendmail_path = "sendmail -C /etc/postfix2525 -t -i"
The -C directive instructs sendmail to use the alternate config dir, so that emails will be sent to the new intermediate postfix instance and then to the main one, passing through policyd of course.
To check the queue size of the intermediate postfix:
# postqueue -p -c /etc/postfix2525/
If any PHP applications that are hosted have explicit SMTP server/port directives, then be sure to notify your clients/developers that they _MUST_ use localhost:2525 to send their emails to and not the default localhost:25. This is one of the shortcomings of the above method, if someone manually sets up his application to use the default localhost:25 his emails will get right through. But being a good sysadmin, you should monitor such behavior and punish those users accordingly!
That’s about it…with the above configuration and some tweaking to the thresholds you have very good chances of avoiding getting blacklisted because someone decided to send a few thousand spams emails. And most importantly, your normal mail service will continue to work flawlessly, no matter how big the queue of the intermediate mail server is.
Enjoy!
Reference for policyd: http://policyd.sourceforge.net/readme.html
Filed by kargig at 23:53 under Internet,Linux
Tags: antispam, blacklist, debian, email, Linux, mail, php, policyd, postfix, postfix-policyd, relay, sendmail, smtp, spam
12 Comments | 81,749 views