Tightening up security, restricting by country

Lately we’ve noticed that more and more traffic to our servers involves attempted brute-force logins. That means that a remote computer is connecting to our server and trying to guess usernames and passwords, with different variations of both, and hoping they’ll get in.

Things got to the point where we were seeing hundreds of thousands of login attempts daily for accounts like ‘admin’, ‘hortnet’, ‘wordpress’, variations of my name, and so on. That’s crazy! The attacks were also coming in bursts, so there might be a 30-second lull followed by thirty attempts within a few seconds. We weren’t overly concerned because we have other protections in place like two-factor authentication, but there were so many failed logins that our server performance was starting to suffer.

The first thing we did is what everyone else does: install software that blocks multiple failed attempts. OSSEC is great for that, but it only catches people after they’ve jiggled the door handle a few times. Even with OSSEC installed we were still seeing 50,000 attempts daily.

After studying the logs, a commonality became apparent. Almost all of the attacks were coming from other countries. Russia and China were the worst offenders, but just about every country around the globe was represented. What if we could limit administrative access only to computers based in the US? Nobody outside of the US needs to get in, and MaxMind maintains a database of IP to country code mappings that would work perfectly. Fortunately, they’re gracious enough to share a free version that will meet our needs.

We crafted some scripts and discovered that limiting access from only US systems reduced login attempts by 99.9 percent.

The first thing we did was install ipset on our server and configure the system to use it and our scripts (this is RedHat/CentOS-centric). Cut and paste the section below into your shell.

sh -c "PERL_MM_USE_DEFAULT=1 perl -MCPAN -e 'install Net::CIDR'"
yum install -y ipset perl-Net-CIDR perl-libwww-perl
echo "create -exist US hash:net family inet hashsize 2048 maxelem 65536" >> /etc/sysconfig/ipset
mkdir -p /var/cache/ipset
mkdir -p /usr/services/GeoIP
echo "ipset restore < /var/cache/ipset/us.conf" >> /etc/rc.local
echo "15 12 * * 2 root /usr/services/GeoIP/createUSipset" >> /etc/cron.d/ipset
/etc/rc.d/init.d/ipset start

This creates a set named ‘US’ in ipset that will store the IP ranges of all known US IPs. Note that the actual population of the set is done via /etc/rc.local — this is because /etc/sysconfig/ipset can’t contain a ‘restore’ command. We could store all of our IPs directly in /etc/sysconfig/ipset, but if you want to use ipset for anything different it becomes complicated. Also, note that our cron entry only downloads on Tuesday because that’s the day that MaxMind releases database changes.

Anyhow, once the stuff above is done, paste the section below into your shell to create the script that downloads the database.

cat <<'EOF' > /usr/services/GeoIP/createUSipset

use Net::CIDR;
use LWP::UserAgent;
use v5.10;

my $ua    = LWP::UserAgent->new;
my $dir   = qq(/usr/services/GeoIP);
my $file  = qq(GeoIPCountryCSV.zip);
my $url   = qq(https://geolite.maxmind.com/download/geoip/database/$file);
my $cache = qq(/var/cache/ipset/us.conf);

exit if ! ($ua->mirror($url, qq($dir/$file)))->is_success;

open (IPSET, "|ipset restore");
say IPSET qq(create -exist US-new hash:net family inet);
open (COUNTRY, "zcat $dir/$file|");
while (<COUNTRY>) {
   next if $_ !~ /"US"/;
   my ($ipstart, $ipend) = ($_ =~ /^"([^"]+)","([^"]+)",/);
   foreach my $i (Net::CIDR::range2cidr($ipstart .  "-" .  $ipend)) {
      say IPSET qq(add US-new $i);
close COUNTRY;
close IPSET;

system qq(ipset swap US US-new);
system qq(ipset save US | sed 's/create US/create -exist US/' > $cache);
system qq(ipset destroy US-new);

chmod ug+rx /usr/services/GeoIP/createUSipset

Now you have a set populated with US IP addresses — all we have left is to actually use it with our iptables rules. Here’s an example that blocks all non-US traffic to ssh, then saves the changes for a reboot:

iptables -A INPUT -m set --set US src -p tcp -m tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 22 -j DROP

The critical part above is the ‘-m set –set US’ bit; it tells iptables to compare the connecting IP against the set contained within ipset.

Leave a Reply

Your email address will not be published. Required fields are marked *