SSH/Swatch

From Gentoo Linux Wiki

Jump to: navigation, search

Image:OpenSSH-logo.png

SSH Basics

Tips & Tricks

Other Gentoo-wiki SSH

edit

[edit] What is Swatch?

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

[edit] 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.

[edit] Installation

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

  • Man pages says that it can execute a command when a log entry is viewed "X" times within "Y" seconds. That's wrong. These two parameters are exclusive. You can't use a threshold AND a delay !
  • It can't stop itself easily (must use kill -9...).

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
}

[edit] 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

[edit] Configuration

[edit] 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

[edit] 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

[edit] 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
  • The "key" tell swatch how to identify the log line. We can't use the whole string here, because the same attacker (ie the same IP) will probably try multiple user names. The key can refer to one of the parens set in the search string (here, $4 = the IP).
  • The "threshold" is the number of times swatch need to see the "key" to execute the actions below.
  • The "delay" is the validity of each "key". When older that 'delay', the "key" is discarded. Use a syntax like HH:MM:SS.

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"

[edit] 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

[edit] Optional Steps

[edit] 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

[edit] Other thoughts

  • This script didn't provide a way to purge iptables, require addition cron job to do the work. There is also risk of blowing up iptables under DDoS.
  • Swatch runs as perl5.8.6 in ps
  • See denyhosts for a Python based solution which uses /etc/hosts.deny.

[edit] 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.

Personal tools