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
A very nice post, indeed web hosting is the primary constituent when you are starting a website. People rarely care about the web hosts but it is one of the most important parameter in the performance of website. I appreciate your post with such informative material. I just subscribed to your post and awaiting for more good posts on web hosting over the coming days. Thanks
The biggest issue with policyd and php mail is that the normal mail function actually doesn’t go over SMTP. This completely bypasses the smtp restrictions in postfix and lets users run wild. Your solutions is a great one when the mailserver is on the same box as the webserver. Kudos.
Hi,
this is a very interesting post and I’m thinking about implementing something similar.
One thing I noticed is that you have two init scripts and run two instances of postscript. I don’t think this would be necessary. You can just add an instance in master.cf on a different port (or IP) and override whatever setting is necessary (queue directory, etc) using -o switches. I use this for for before-queue-mail-filtering already.
Anyway, wanted to ask whether after a while this works for you or whether you have found any issues with it.
Thanks
Hello Michael,
you are probably right about the init scripts. It seemed a cleaner solution when I did it but probably I’ll change my configs to what you’re suggesting.
After 1+ year in production this system works nicely and clients haven’t complained at all…so this makes it quite successful.
Interesting, I’m going to set this up soon too and will try to share if I come across anything that’s worth knowing.
One other thing I wanted to ask: your article mentions briefly the policyd configuration and then you say:
“Since all emails will be relayed through localhost there’s no point in throttling per host, what is needed is throttling per envelope sender.”
Is that something you’ve managed to set up (throttling per envelope sender) (if not I was also considering more exotic ideas like traffic shaping using tc)?
@Michael, yeah it’s easy to throttle per sender, just add a record to the ‘throttle’ table in the policyd database:
[…] […]
Hi There,
I’ve been trying to find this workaround for a while by myself but didn’t get the policyd working as php would send out via sendmail / pickup, thank you so much for this howto. Please note that I’ve had to set
alternate_config_directories = /etc/postfix2525
in the main postfix config /etc/postfix/main.cf
Cheers
Hi,
I’m facing with a very similar problem as you & I was very happy when I found this article.
However, I could not manage to make my system work as expected.
More details, I need to config & setup Mail spam filters for our system that uses Postfix & PolicyD. I have the same need as you before when I want to limit quota / filter spam… for emails come from mail() function.
I follow all your steps with success tests as you described, but finally I could not get mails come from mail() go though PolicyD. These are even could not delivered to outside 🙁
Errors I got as below:
Oct 10 07:07:02 46-105-35-45 locally/pickup[8156]: 573CE24586: uid=1001 from=
Oct 10 07:07:02 46-105-35-45 postfix/cleanup[10435]: 573CE24586: message-id=
Oct 10 07:07:02 46-105-35-45 postfix/qmgr[2093]: 573CE24586: from=, size=432, nrcpt=1 (queue active)
Oct 10 07:07:02 46-105-35-45 locally/pickup[8156]: 5884424585: uid=1001 from=
Oct 10 07:07:02 46-105-35-45 postfix/cleanup[10435]: 5884424585: message-id=
Oct 10 07:07:02 46-105-35-45 postfix/qmgr[2093]: 5884424585: from=, size=432, nrcpt=1 (queue active)
Oct 10 07:07:02 46-105-35-45 postfix/error[10419]: 573CE24586: to=, relay=none, delay=0.01, delays=0.01/0/0/0, dsn=4.3.0, status=deferred (mail transport unavailable)
Oct 10 07:07:02 46-105-35-45 postfix/error[10421]: 5884424585: to=, relay=none, delay=0, delays=0/0/0/0, dsn=4.3.0, status=deferred (mail transport unavailable)
Oct 10 07:07:02 46-105-35-45 locally/pickup[8156]: 5B1272458A: uid=1001 from=
Oct 10 07:07:02 46-105-35-45 postfix/cleanup[10435]: 5B1272458A: message-id=
Oct 10 07:07:02 46-105-35-45 postfix/qmgr[2093]: 5B1272458A: from=, size=432, nrcpt=1 (queue active)
Oct 10 07:07:02 46-105-35-45 postfix/error[10431]: 5B1272458A: to=, relay=none, delay=0.01, delays=0/0/0/0, dsn=4.3.0, status=deferred (mail transport unavailable)
Oct 10 07:07:02 46-105-35-45 locally/pickup[8156]: 5DF7E2458C: uid=1001 from=
Oct 10 07:07:02 46-105-35-45 postfix/cleanup[10435]: 5DF7E2458C: message-id=
Oct 10 07:07:02 46-105-35-45 postfix/qmgr[2093]: 5DF7E2458C: from=, size=432, nrcpt=1 (queue active)
Oct 10 07:07:02 46-105-35-45 postfix/error[10426]: 5DF7E2458C: to=, relay=none, delay=0.01, delays=0.01/0/0/0, dsn=4.3.0, status=deferred (mail transport unavailable)
Oct 10 07:07:02 46-105-35-45 locally/pickup[8156]: 60EE92458E: uid=1001 from=
Oct 10 07:07:02 46-105-35-45 postfix/cleanup[10435]: 60EE92458E: message-id=
Oct 10 07:07:02 46-105-35-45 postfix/qmgr[2093]: 60EE92458E: from=, size=432, nrcpt=1 (queue active)
Oct 10 07:07:02 46-105-35-45 postfix/error[10421]: 60EE92458E: to=, relay=none, delay=0.03, delays=0.01/0/0/0.02, dsn=4.3.0, status=deferred (mail transport unavailable)
All config stuffs are very similar with yours:
$ netstat -antp | grep 2525
tcp 0 0 127.0.0.1:12525 0.0.0.0:* LISTEN 2089/master
tcp 0 0 127.0.0.1:2525 0.0.0.0:* LISTEN 20507/master
/etc/postfix2525/master.cf
– The same as you gave
/etc/postfix2525/main.cf
– The same as you gave
–
postconf -Mf
smtp inet n – – – – smtpd
-o smtpd_recipient_restrictions=$other_restrictions
-o smtpd_sasl_auth_enable=yes
pickup fifo n – – 60 1 pickup
-o syslog_name=locally
-o content_filter=$other_restrictions
-o smtpd_sasl_auth_enable=yes
cleanup unix n – – – 0 cleanup
qmgr fifo n – n 300 1 qmgr
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
relay unix – – – – – smtp
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
maildrop unix – n n – – pipe
flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient}
uucp unix – n n – – pipe
flags=Fqhu user=uucp argv=uux -r -n -z -a$sender – $nexthop!rmail
($recipient)
ifmail unix – n n – – pipe
flags=F user=ftn argv=/usr/lib/ifmail/ifmail -r $nexthop ($recipient)
bsmtp unix – n n – – pipe
flags=Fq. user=bsmtp argv=/usr/lib/bsmtp/bsmtp -t$nexthop -f$sender
$recipient
scalemail-backend unix – n n – 2 pipe
flags=R user=scalemail argv=/usr/lib/scalemail/bin/scalemail-store
${nexthop} ${user} ${extension}
mailman unix – n n – – pipe
flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py
${nexthop} ${user}
submission inet n – – – – smtpd
-o smtpd_recipient_restrictions=$other_restrictions
-o smtpd_sasl_auth_enable=yes
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}
Any help would be greatly appreciated 🙂
try and send an email from cli connecting to port 2525…Step 6. Does it work ? if not, you have to solve that problem first.
Hi,
i use these 2 simple lines in iptables to get the outgoing email restricted.
iptables -I FORWARD -p tcp –dport 25 -m connlimit –connlimit-above 15 -j DROP
iptables -I FORWARD -p tcp –dport 25 -m state –state new -j LOG –log-prefix “SMTP:”
this will restrict it to 15 msg
Any comment is welcome 🙂
Hello , there is some errors with the new version in debian wheezy 7 ,
postfix/postfix-script: error: unknown command: ‘-c’
postfix/postfix-script: fatal: usage: postfix start (or stop, reload, abort, flush, check, status, set-permissions, upgrade-configuration)
The solution is to change this line : if start-stop-daemon –start –exec ${DAEMON} — ${DAEMON_OPTIONS} quiet-quick-start; then
by this one : if start-stop-daemon –start –exec ${DAEMON} ${DAEMON_OPTIONS} quiet-quick-start; then