SSH/Swatch
From Gentoo Linux Wiki
[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.
