Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Locked thread
Masked Pumpkin
May 10, 2008
I've got a server that has to allow multiple a reasonably high smtpd_connection_rate_limit as it's managing (sasl_authenticated) mail for multiple domains, each with many users behind a single (ISP allocated, DHCP) IP. As a result, when an account gets hacked and used to send spam, it can take several minutes for the alert regarding a suddenly increased mailq size to come to our attention and get fixed. While the postfix service could simply be stopped when a high mailq is detected, that's not an elegant solution, and so I decided to try my hand at a bash script to run every minute and fix the problem:

code:
#!/bin/bash

#This script will check for the top senders in X lines of postfix maillog, 
#judging by sasl_username. If a given username appears too often on that list, 
#the script will find the most common originating IP and ban it with iptables. 
#It will then clean the mailq of any emails that apply with that address, and 
#(hopefully) send an email to a given address to alert them of the action

###############################################################################
#User specified variables:

#Maximum number of messages from any one user in the last 1500 lines of code
MAXMESSAGES="50"

#Path to your mail.log file
MAILLOGPATH="/var/log/maillog"

#Email Address to receive notification - must be properly set up in your /etc/msmtp.conf
ADMINEMAIL="youremail@gmail.com"
###############################################################################

if [ -f /tmp/topips ];			#First, some cleanup
then
	rm /tmp/topips
fi
if [ -f /tmp/topqueues ];
then
	rm /tmp/topqueues
fi

NUMMSGS=`tail -n1500 $MAILLOGPATH | grep sasl_username | awk '{print $9}' | sort | uniq -c | sort -n | tail -n1 | awk '{print $1}'`	#I really shouldn't have to do this twice?

if [ "$NUMMSGS" -ge "$MAXMESSAGES" ];
then
	USER=`tail -n1500 $MAILLOGPATH | grep sasl_username | awk '{print $9}' | sort | uniq -c | sort -n | tail -n1 | cut -d= -f2`
	echo $USER
	
	#Find the queue IDs for any offending messages with that username, and feed the IP into /tmp/topips
	mailq | grep $USER | awk '{print $1}'| while read LINE; do
		IP=`postcat -q $LINE | grep -E "^Received:(.)*[.]*\[[0-9]*.[0-9]*.[0-9]*.[0-9]*\]\)" | grep -oE "[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*" | uniq | tail -n1 >> /tmp/topips`;
		echo $LINE >> /tmp/topqueues;
	done

	#Change that user's password
	mysql -u POSTFIXUSER -pPASSWORD -e 'UPDATE postfix.USERS SET password = "ChangedPass!34NotEvenMD5" WHERE username = "$USER";'

	#Find the most regularly occurring IP in that file - if it exists
	if [ -f /tmp/topips ];
	then
		TOPIP=`uniq /tmp/topips -c | sort | tail -n1 | awk '{print $2}'`
		echo IP to ban is $TOPIP
		iptables -I INPUT 1 -p tcp -s $TOPIP -j DROP		#This could be set to ACCEPT while testing, drop later
	
		#Now we can clear the mailq of all the offending messages
		cat /tmp/topqueues | while read LINE; do
			#postsuper -d $LINE
			echo $LINE will be deleted;
		done

		#Now let's send off an email to let us know what's happened
		echo The user $USER was blocked for sending $NUMMSGS emails from $TOPIP | msmtp --file=/etc/msmtp.conf --account=gmail $ADMINEMAIL
	else
		echo $USER has had all emails delivered already, no IP to ban
	fi

	#And lastly, clean up after ourselves
	if [ -f /tmp/topips ];                  #First, some cleanup
	then
        	rm /tmp/topips
	fi
	if [ -f /tmp/topqueues ];
	then
        	rm /tmp/topqueues
	fi
else
	echo Highest messages at $NUMMSGS, going back to sleep
fi


My lack of experience with bash scripting should be evident - it feels like I'm making multiple unnecessary calls (for example, to find the highest user and then the highest number of messages). The use of temporary files instead of arrays also doesn't feel right. On the other hand, the script does work - it's caught a handful of chancers in the last few weeks I've been testing it.

I'd like to try and get this script as polished as possible to aid with my own study of bash, and if anyone else finds it useful they're welcome to it. Otherwise, am I perhaps barking up the wrong tree entirely?

Adbot
ADBOT LOVES YOU

Masked Pumpkin
May 10, 2008
Thank you so much! Based on your advice, I've updated and tweaked the script somewhat -

Changelist:
  • Mailq is now called instead of trawling through the log file - this still requires multiple calls to mailq and subsequently to postcat, which I'd like to trim down if I can
  • MAXMESSAGES is now also checked with MAXMESSAGESPERUSER, to help avoid blocking a user who happens to have just 5 messages in the queue when the server is very congested for whatever reason
  • The password change query to MySQL wasn't being processed properly thanks to the way the $USER variable was passed to it - fixed
  • Exit codes now used to find bad messages in the queue and remove them, and also to check whether an IP hasn't already been banned
  • Removed checking for the existence of temporary files
  • IP variable actually set and used now - a good thing, since we were previously getting a DNS name, which works but is not ideal for IPTables
  • Found a bad if/then check, fixed and cleaned up indenting to reflect properly
  • Integrated fail2ban for temporary blocking, if it's installed (and the sendmail-auth jail is configured)

Any other comments/suggestions are greatly appreciated - I've learned a lot about bash from this, but I can't help but feel that the biggest thing I've learned is to not use bash if I can use PHP or Python (both of which I know reasonably well and allow for variable management that doesn't drive you to drink).

code:
#!/bin/bash
#This script will check for the top senders in X lines of postfix maillog, judging by sasl_username. 
#If a given username appears too often on that list, the script will find the most common originating 
#IP and ban it with iptables. It will then clean the mailq of any emails that apply with that address, 
#and (hopefully) send an email to a given address to alert them of the action.
#AMENDED - This script will keep an eye on the postfix queue - if it grows too big, it'll look for the 
#highest (sasl_auth) sender in that queue - if that sender is above a threshold, it'll change that 
#users password, block the IP, and remove all of those messages from the mailq

###############################################################################
#User specified variables:
#Maximum number of messages from any one user in the last 1500 lines of code
MAXMESSAGES="50"

#Maximum number of messages from any one user
MAXMESSAGESPERUSER="20"

#Path to your mail.log file
MAILLOGPATH="/var/log/maillog"

#Email Address to receive notification - must be properly set up in your /etc/msmtp.conf
ADMINEMAIL="youremail@gmail.com"

#If you have Fail2Ban installed (with the sendmail-auth jail enabled) then change this to 1 so an IP 
#can be banned but automatically unbanned after whatever bantime you have set in Fail2Ban
FAIL2BAN="1"
###############################################################################

#First, some cleanup
rm /tmp/topips
rm /tmp/topuser

NUMMSGS=`mailq | tail -n1 | awk '{print $5}'`

if [ "$NUMMSGS" -ge "$MAXMESSAGES" ];
then
        echo Number of messages in queue at $NUMMSGS, checking for single high user;
        mailq | grep -P "^[0-9A-f]{10}" | awk '{print $1}' | while read LINE; do
                postcat -q $LINE | grep -E "Authenticated sender: [A-Za-z0-9]*@[A-Za-z0-9\.]*\)" | grep -oE "[a-zA-Z0-9]*@[a-zA-Z0-9\.]*" >> /tmp/topuser;
        done
        USERMSGS=`uniq -c /tmp/topuser |sort -n |tail -n1| awk '{print $1}'`;

        if [ "$USERMSGS" -ge "$MAXMESSAGESPERUSER" ];
        then
                echo Highest user at $USERMSGS, taking action;
                USER=`uniq -c /tmp/topuser |sort -n |tail -n1| awk '{print $2}'`;
                echo User is $USER;

                #Find the queue IDs for any offending messages with that username, and feed the 
		#IP into /tmp/topips - Sadly, mailq will only give us the provided from_address, 
		#not the sasl_sender, so this must be run through postcat
                mailq | grep -P "^[0-9A-f]{10}" | awk '{print $1}' | while read LINE; do
                        postcat -q $LINE | grep -iE "Authenticated sender: $USER";
                        if [ $? -eq 0 ];
                        then
                                postcat -q $LINE | grep -E "^Received:(.)*[.]*\[[0-9]*.[0-9]*.[0-9]*.[0-9]*\]\)" | grep -oE "[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*" | uniq | tail -n1 >> /tmp/topips;
                        fi
                        done

                BADIP=`uniq -c /tmp/topips |sort -n |awk '{print $2}'`
                echo Offending IP is $BADIP

                #Change that user's password
                echo Changing password for $USER
                mysql -u POSTFIXUSER -pPASSWORD -e "UPDATE postfix.mailbox SET password = 'ChangedPass!23324234' WHERE username = \"$USER\";"

                #Ban the IP, first checking it's not being blocked already
                if [ "$FAIL2BAN" -eq "1" ];
                then
                        echo Jailing $BADIP with fail2ban;
                        fail2ban-client sendmail-auth banip $BADIP;
                else
                        iptables -L|grep -E "DROP       tcp  --  $BADIP[ ]*anywhere";
                        if [ $? -eq 1 ];
                        then
                                echo banning IP $BADIP with iptables;
                                iptables -I INPUT 1 -p tcp -s $BADIP -j DROP;
                        fi
                fi

                #Now we can clear the mailq of all the offending messages
                mailq | grep -P "^[0-9A-f]{10}" | awk '{print $1}' | while read LINE; do
                        postcat -q $LINE | grep -iE "Authenticated sender: $USER"
                        if [ $? -eq 0 ];
                        then
                                echo $LINE will be deleted;
                                #postsuper -d $LINE;
                        fi
                        done

                #Now let's send off an email to let us know what's happened
                echo The user $USER was blocked for sending $NUMMSGS emails from $BADIP | msmtp --file=/etc/msmtp.conf --account=gmail $ADMINEMAIL

                #And lastly, clean up after ourselves
                rm /tmp/topips
                rm /tmp/topuser
        else
                echo Not enough messages from one user in queue
        fi
else
        echo Highest messages at $NUMMSGS, going back to sleep
fi

  • Locked thread