Search:  
Gentoo Wiki

SSH/Swatch


Image:OpenSSH-logo.png

SSH Basics

Tips & Tricks

Other Gentoo-wiki SSH

edit

What is Swatch?

Swatch is a Perl-based system log watcher.
http://swatch.sourceforge.net/

Introduction

Swatch is designed to watch system logs for particular strings and can react on them. Due to this swatch is able to protect SSHD from a brute force attack.

Installation

The latest version of swatch (3.1.1) has multiple problems:

So, we can't use the official source tree to protect sshd. We will patch it before installing it.

--- The latest release of swatch on sourceforge is 3.2.1. That version seems to work with the syntax used on this page? Incidentally, 3.2.1 is the latest ~x86 masked version on portage. ---

Make sure you have a PORTDIR_OVERLAY variable in your /etc/make.conf:

PORTDIR_OVERLAY="/usr/local/portage"

Copy the official ebuild into your overlay:

mkdir -p /usr/local/portage/app-admin/swatch/files
cp -r /usr/portage/app-admin/swatch/ /usr/local/portage/app-admin/swatch/

Create the patch file:

File: /usr/local/portage/app-admin/swatch/files/swatch-3.1.1-threshold-exit.patch
diff -Naur swatch-3.1.1/lib/Swatch/Throttle.pm swatch-3.1.1-patched/lib/Swatch/Throttle.pm
--- swatch-3.1.1/lib/Swatch/Throttle.pm	2004-07-19 22:14:54.000000000 +0200
+++ swatch-3.1.1-patched/lib/Swatch/Throttle.pm	2006-01-02 17:06:14.000000000 +0100
@@ -95,6 +95,7 @@
 	      @_
 	     );
 
+  my @delay = split(/:/, "0:$opts{DELAY}");
   my @dmyhms;
   my $key;
   my $cur_rec;
@@ -134,30 +135,61 @@
     $rec->{FIRST} = [ @dmyhms ];
     $rec->{LAST} = [ @dmyhms ];
     $rec->{HOLD_DHMS} = $opts{HOLD_DHMS} if defined $opts{HOLD_DHMS};
-    $rec->{COUNT} = 1;
+    $rec->{COUNT} = 0;
     $LogRecords{$key} = $rec;
-    return $msg;
-  } else {
-    $cur_rec = $LogRecords{$key};
-    $cur_rec->{COUNT}++;
-    if (defined $opts{THRESHOLD} and $cur_rec->{COUNT} == $opts{THRESHOLD}) {
-      ## threshold exceeded ##
-      chomp $msg;
-      $msg = "$msg (threshold $opts{THRESHOLD} exceeded)";
-      $cur_rec->{COUNT} = 0;
-    } elsif (defined $opts{HOLD_DHMS} 
-	     and past_hold_time($cur_rec->{LAST},
-				\@dmyhms, $opts{HOLD_DHMS})) {
+  }
+ 
+  ## Get current record ##
+  $cur_rec = $LogRecords{$key};
+  $cur_rec->{COUNT}++;
+
+  ## delay only ##
+  if( defined $opts{DELAY} and not defined $opts{THRESHOLD} ) {
+    if( past_hold_time($cur_rec->{LAST}, [ @dmyhms ], [ @delay ]) ) {
       ## hold time exceeded ##
       chomp $msg;
       $msg = "$msg (seen $cur_rec->{COUNT} times)";
-      $cur_rec->{COUNT} = 0;
+      $cur_rec->{COUNT} = 1;
       $cur_rec->{LAST} = [ @dmyhms ];
     } else {
       $msg = '';
     }
-    $LogRecords{$key} = $cur_rec if exists($LogRecords{$key});  ## save any new values ##
+  
+  ## threshold only ##
+  } elsif( defined $opts{THRESHOLD} and not defined $opts{DELAY} ) {
+    if( $cur_rec->{COUNT} == $opts{THRESHOLD}) {
+      ## threshold exceeded ##
+      chomp $msg;
+      $msg = "$msg (threshold $opts{THRESHOLD} exceeded)";
+      $cur_rec->{COUNT} = 0;
+    } else {
+      $msg = '';
+    }
+
+  ## threshold AND delay ##
+  } elsif( defined $opts{THRESHOLD} and defined $opts{DELAY} ) {
+    if( not past_hold_time($cur_rec->{LAST}, [ @dmyhms ], [ @delay ]) ) {
+      if( $cur_rec->{COUNT} == $opts{THRESHOLD} ) {
+        ## threshold exceeded during delay ##
+	chomp $msg;
+	$msg = "$msg (threshold $opts{THRESHOLD} exceeded during delay $opts{DELAY})";
+
+	## TODO: Tenir compte du parametre repeat ici ##
+	$cur_rec->{COUNT} = 0;
+	$cur_rec->{LAST} = [ @dmyhms ];
+      } else {
+        $msg = '';
+      }
+    } else {
+      $cur_rec->{COUNT} = 1;
+      $cur_rec->{LAST} = [ @dmyhms ];
+      $msg = '';
+    }
   }
+  
+  ## save any new values ##
+  $LogRecords{$key} = $cur_rec if exists($LogRecords{$key});
+  
   return $msg;
 }
 
diff -Naur swatch-3.1.1/swatch swatch-3.1.1-patched/swatch
--- swatch-3.1.1/swatch	2004-07-19 22:14:54.000000000 +0200
+++ swatch-3.1.1-patched/swatch	2006-01-03 14:36:19.000000000 +0100
@@ -758,11 +758,17 @@
 my \$swatch_flush_interval = 300;
 my \$swatch_last_flush = time;
 
+## when running tail, we will have to send it a TERM signal before closing ourself
+my \$tail_pid = -1;
+
 use IO::Handle;
 STDOUT->autoflush(1);;
 
 sub goodbye {
   \$| = 0;
+  if( \$tail_pid != -1 ) {
+    kill('TERM', \$tail_pid);
+  }
 ];
 
   if ($opt_read_pipe) {
@@ -891,7 +897,8 @@
       }
        $code = qq/
 my \$filename = '$filename';
-if (not open(TAIL, \"$tail_cmd_name $tail_cmd_args \$filename|\")) {
+\$tail_pid = open(TAIL, \"$tail_cmd_name $tail_cmd_args \$filename|\");
+if (not \$tail_pid) {
     die "$0: cannot read run \\"$tail_cmd_name $tail_cmd_args \$filename\\": \$!\\n";
 }
 
@@ -927,6 +934,7 @@
     my $code;
     $code = q[
 }
+## TODO: Add close !!!
 ];
     return $code;
 } 

Be careful: This patch make swatch do what we want in this WiKi's context ! But i can't say that it doesn't make something else go wrong !

We will manage some of swatch's main arguments into /etc/conf.d. Add the following file:

File: /usr/local/portage/app-admin/swatch/files/swatch.conf.d
# Config file for /etc/init.d/swatch

# Place where swatch will generate his temporary scripts
SWATCH_SCRIPTDIR=/var/tmp/swatch

# File to monitor
SWATCH_TAILFILE=/var/log/messages

# Tail arguments
SWATCH_TAILARGS='--follow=name -n 0'

And a sample swatch.conf file. Just copy/paste this content in the new file. We'll see what's here later...

File: /usr/local/portage/app-admin/swatch/files/swatch.conf
# Global swatch filter file
# Usefull to protect sshd.
# IP will be black listed after 3 unsuccessfull attempts whithin a minute
# dont forget to create swatch_rejects in iptables before uncommenting

# To ignore a IP-range
# ignore /216\.239\.37\./

# Invalid SSH Login Attempts
watchfor /(: [iI]nvalid [uU]ser )(.*)( from )(.*)$/
	throttle threshold=3,delay=0:1:0,key=$4
	# exec "/sbin/iptables -A swatch_rejects -s $4 -j DROP"

# Failed SSH Login Attempts
watchfor /(: [fF]ailed password for )(.*)( from )(.*)( port )(.*)$/
	throttle threshold=3,delay=0:1:0,key=$4
	# exec "/sbin/iptables -A swatch_rejects -s $4 -j DROP"

Be careful here ! We use our own new syntax provided by our patch... We are probably not compatible with other versions of swatch.

And finaly, add a startup script:

File: /usr/local/portage/app-admin/swatch/files/swatch
#!/sbin/runscript
# Copyright 1999-2006 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
# $Header: $

opts="${opts} reload"

depend() {
	after logger
}

start() {
	ebegin "Starting Swatch"
	
	if [ ! -d ${SWATCH_SCRIPTDIR} ]; then
		mkdir ${SWATCH_SCRIPTDIR}
	fi
	
	swatch --script-dir=${SWATCH_SCRIPTDIR} \
		--tail-file=${SWATCH_TAILFILE} \
		--config-file=/etc/swatch.conf \
		--pid-file=/var/run/swatch.pid \
		--tail-args="${SWATCH_TAILARGS}" \
		--daemon \
		>> /var/log/swatch.log \
		2>> /var/log/swatch-err.log
	eend $?
}

stop() {
	ebegin "Stopping Swatch"
	kill `cat /var/run/swatch.pid`
	eend $?
}

restart() {
	svc_stop
	sleep 2
	svc_start
}

reload() {
	# Doesn't work !!! The signal must be sent to the monitor process, not to the script itself !
	kill -HUP `cat /var/run/swatch.pid`
}

We will also update the current ebuild:

File: /usr/local/portage/app-admin/swatch/swatch-3.1.1.ebuild
# Copyright 1999-2005 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
# $Header: /var/cvsroot/gentoo-x86/app-admin/swatch/swatch-3.1.1.ebuild,v 1.5 2005/11/28 12:06:51 mcummings Exp $

inherit eutils perl-app

DESCRIPTION="Perl-based system log watcher"
HOMEPAGE="http://swatch.sourceforge.net/"
SRC_URI="mirror://sourceforge/swatch/${P}.tar.gz"

LICENSE="GPL-2"
SLOT="0"
KEYWORDS="~x86 ~ppc ~amd64"
IUSE=""

DEPEND="${DEPEND}
	dev-perl/DateManip
	dev-perl/Date-Calc
	dev-perl/TimeDate
	dev-perl/File-Tail
	>=perl-core/Time-HiRes-1.12"

src_unpack() {
	unpack ${P}.tar.gz
	cd ${S}
	epatch "${FILESDIR}/${P}-threshold-exit.patch"
}

src_install() {
	perl-module_src_install
	
	doinitd "${FILESDIR}"/swatch
	newconfd "${FILESDIR}"/swatch.conf.d swatch
	cp "${FILESDIR}"/swatch.conf ${D}/etc/swatch.conf
}

emerging

The 3.1.1 version is currently masked. Unmask it by adding the following line in your /etc/portage/package.keywords:

echo "app-admin/swatch ~x86" >> /etc/portage/package.keywords

-- niv: I had to to generate a digest here --

ebuild /usr/local/portage/app-admin/swatch/swatch-3.1.1.ebuild digest

And emerge it:

emerge -va swatch

Configuration

IpTables

First we create a new iptables chain to store all the blocked ip's for easier removal if necessary, "swatch_rejects" where swatch (and only swatch) will append rules to.

iptables -N swatch_rejects

Then we create an unconditional jump to this rule in your INPUT chain. I also place it at rule #5 in INPUT - below all of my own rules that accept my IP's (so you don't accidently lock yourself out!)

iptables -I INPUT 5 -j swatch_rejects

Be sure to save your iptables setup, so these steps aren't necessary everytime you reboot.

/etc/init.d/iptables save

Now, to see what swatch has been doing to your firewall, simply do:

iptables -L swatch_rejects

Swatch general configuration

You can edit general swatch parameters in the file /etc/conf.d/swatch. The only interresting information here is the name of the log file to tail

SWATCH_TAILFILE=/var/log/messages

Swatch Rules

The main configuration file is /etc/swatch/swatch.conf. Here is a good example:

File: /etc/swatch/swatch.conf
# Global swatch filter file

# To ignore a IP-range
ignore /216\.239\.37\./

# Invalid SSH Login Attempts
watchfor /(: [iI]nvalid [uU]ser )(.*)( from )(.*)$/
        throttle threshold=3,delay=0:1:0,key=$4
        mail addresses=admin\@domain.com,subject="SSH:\ Invalid\ User\ Access-IPTables\ Rule\ Added"
        exec "/sbin/iptables -A swatch_rejects -s $4 -j DROP"

# Failed SSH Login Attempts
watchfor /(: [fF]ailed password for )(.*)( from )(.*)( port )(.*)$/
        throttle threshold=3,delay=0:1:0,key=$4
        mail addresses=admin\@domain.com,subject="SSH:\ Invalid\ User\ Access-IPTables\ Rule\ Added"
        exec "/sbin/iptables -A swatch_rejects -s $4 -j DROP"

# Invalid SSH Login Attempts. Another one - just formed differently
watchfor /([aA]uthentication [fF]ailure for [iI]llegal [uU]ser )(.*)( from )(.*)$/
        throttle threshold=3,delay=0:1:0,key=$4
        mail addresses=admin\@domain.com,subject="SSH:\ Invalid\ User\ Access-IPTables\ Rule\ Added"
        exec "/sbin/iptables -A swatch_rejects -s $4 -j DROP"

A little explanation of whats being done:

ignore /216\.239\.37\./

This is to ignore, in this case, a IP-range. Very usefull to minimize the possibility that you lock yourself out.

watchfor /(: [iI]nvalid [uU]ser )(.*)( from )(.*)$/
watchfor /(: [fF]ailed password for )(.*)( from )(.*)( port )(.*)$/
watchfor /([aA]uthentication [fF]ailure for [iI]llegal [uU]ser )(.*)( from )(.*)$/

This is to search our logs for the string between //. The parens in the first watchfor are important - they break up the log file line into chucks that are used for $1,$2,$3, ... $n. In this case, for example, $1 is ": Invalid User "; $2 is all the junk in the first (.*); $3 is " from "; and $4 is all the junk in the second (.*) -- which happens to be the IP address you want. Note: the $ at the end signifies end of line. Also, note that the $4 works in both the first and third watchfor code block -- this is pure coincidence and you may need to change the $4 to a different paren set if you are working with your own custom watchfor block.

throttle threshold=3,delay=0:1:0,key=$4

Note: If this does work in 3.1.1, it may not work with loggers such as metalog, because it says instead "Last output repeated N times" So naturally swatch won't find multiple occurances often in those cases.

mail addresses=admin\@domain.com,subject="SSH:\ Invalid\ User\ Access-IPTables\ Rule\ Added"

Mail a user stating that a new rule has been added to iptables.

exec "/sbin/iptables -A swatch_rejects -s $4 -j DROP"

Add the offending ip to "swatch_rejects" and drop all future incoming packets from that address. If you are using shorewall, you can define in this way:

exec "/sbin/shorewall drop $4"

Starting Swatch

another example of /etc/init.d/swatch

 #!/sbin/runscript
 # maat'092007
 depend() {
       use net
 }
 start() {
       ebegin "Starting swatch"
       start-stop-daemon --start --make-pidfile --pidfile /var/run/swatch.pid --background --exec /usr/bin/swatch -- --config-file=/etc/swatch.conf --pid-file=/var/run/swatch.pid
       eend $?
 }
 stop() {
       ebegin "Stopping swatch"
       start-stop-daemon --stop --name perl5.8.8 --user root
       start-stop-daemon --stop --pidfile /var/run/swatch.pid --user root
       eend $?
 }

To start swatch:

/etc/init.d/swatch start

Be sure to add it to you default runlevel (after you've tested things of course.)

rc-update add swatch default

Optional Steps

Limit SSHD Access

Deny access to everyone, including root, except user1 and user2.

Edit /etc/ssh/sshd_config:

File: /etc/ssh/sshd_config
AllowUsers user1 user2
PermitRootLogin no

Other thoughts

References

http://www.trustix.org/wiki/index.php/Swatch

important missing line in the /etc/init.d/swatch description above:

       swatch --script-dir=${SWATCH_SCRIPTDIR} \
               --tail-file=${SWATCH_TAILFILE} \
               --config-file=/etc/swatch/swatch.conf \
      =====>>  --awk-field-syntax \
               --pid-file=/var/run/swatch.pid \
               --tail-args="${SWATCH_TAILARGS}" \
               --daemon \
               >> /var/log/swatch.log \
               2>> /var/log/swatch-err.log

In Swatch Rules, I had to change key=$4 to key=$x for this to work as intended, x being different values for the different rules.

Retrieved from "http://www.gentoo-wiki.info/SSH/Swatch"

Last modified: Fri, 29 Aug 2008 05:03:00 +0000 Hits: 39,805