Recent Changes - Search:

PSIgroup

Cluster

My Pages?

AutoGate

SSH access ports to servers are under continuous attack from automated scanners. The recipe you find below provides a first level of defence, allowing you to keep the SSH port normally closed to all but the commonly trusted addresses (like your LAN, those of other selected institutes...) but still provide your remote users with an access path that does not require anything more than a browser and the SSH client, and so can be used also where VPN clients may not be available.

DISCLAIMER: the code below is released under the GNU Public License v3 or above. As you can read in the license, this means that nor I, nor my employer take any responsibility for it. It may or may not suit your needs. It is not meant as a main security line, its only role is to spare you from reading LogWatch reports containing hundreds of trivial automated login attempts, and concentrate on the actual threats. Make always sure that your users have non-trivial passwords (but don't force them to change too often, or they'll just write it on a sticky note under the keyboard) and also check that their SSH keys are not compromised. Apply the Defence in depth ideas as much as you can afford.

iptables setup

The system works by keeping the SSH port mostly closed in iptables, with one list of static exceptions, and one of dynamic exceptions:

Chain INPUT (policy DROP)
target     prot opt source               destination         
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           state RELATED,ESTABLISHED 
SSH        tcp  --  0.0.0.0/0            myserver        tcp dpt:22 

Chain SSH (1 references)
target     prot opt source               destination         
ACCEPT     all  --  mylan                0.0.0.0/0
ACCEPT     all  --  friendlan            0.0.0.0/0
SSH_DYN    tcp  --  0.0.0.0/0            0.0.0.0/0
LOG        all  --  0.0.0.0/0            0.0.0.0/0        limit: avg 10/sec burst 5 LOG flags 0 level 6 prefix `IPTABLES: DropSSH ' 
DROP       all  --  0.0.0.0/0            0.0.0.0/0           

Chain SSH_DYN (1 references)
target     prot opt source               destination         
ACCEPT     tcp  --  79.32.120.16         0.0.0.0/0           

The choice of a separate SSH_DYN chain allows to easily add or remove rules for client IPs, without risks of disturbing the rest of the iptables configuration. The management of this chain is done by means of a PHP script in tandem with a small daemon.

A minimal iptables configuration for AutoGate may be created with the following iptables commands:

/sbin/iptables -N SSH
/sbin/iptables -N SSH_DYN
/sbin/iptables -I INPUT 1 -d myserver -p tcp --dport 22 -j SSH
/sbin/iptables -F SSH
/sbin/iptables -A SSH -s mylan -j ACCEPT
/sbin/iptables -A SSH -j SSH_DYN
/sbin/iptables -A SSH -m limit --limit 10/sec -j LOG --log-prefix "IPTABLES: DropSSH " --log-level 6
/sbin/iptables -A SSH -j REJECT
/sbin/iptables -F SSH_DYN

Note that this inserts the rule for SSH as first of the INPUT chain; you may want to place it elsewhere in the chain. Use iptables -L -vn --line-numbers to see the line number to use.
Don't forget to save permanently this configuration after testing it (service iptables save, on RedHat/Fedora distributions)

AutoGate web frontend

The PHP script is accessible via HTTP, simply requesting a chosen URL from a web browser or a command line HTTP client like wget or curl:

https://myserver.example.com/service/autogate.php

Of course, I would suggest you use a different URL on your server...

The PHP script does not need any elevated privileges to write, in a dedicated directory, files named as the client IP; it is sufficient for the directory to have the appropriate Unix permissions and (if you use SELinux) :

[sergio@myserver ~]$ ls -laZ /var/local/autogate
drwxr-x---  apache wheel  user_u:object_r:httpd_sys_content_t .
drwxr-xr-x  root   root   system_u:object_r:var_t          ..
-rw-r--r--  apache apache user_u:object_r:httpd_sys_content_t 79.32.120.16
-rw-r--r--  root   root   user_u:object_r:httpd_sys_content_t update-timestamp

The client IP is provided by the Apache httpd itself, so it should be safe, but for good measure it is validated against a regexp. If the script identifies the IP as belonging to a private or local subnet (10.*.*.*,172.16.*.*,192.168.*.* or 127.*.*.*) it will assume that also the SSH connection will come from the same IP of the proxy.
It would be trivial to add the option for the client to specify the IP that should be added in the chain; but unless your users suffer from very special/obtuse networks in remote locations, I would suggest to avoid it.

The file is given a modification time in the future, which is used as an expiry time. After expiry time, the daemon will remove the file from the directory and the corresponding rule from the SSH_DYN chain. The default grace time is 30 minutes, but a different amount, up to a configurable maximum, can be specified by the user, appending a time=N HTTP GET parameter to the request URL, like

https://myserver.example.com/service/autogate.php?time=90

It would be easy to add a "password" parameter to the script. I would anyway suggest you don't do it, for a few reasons:

  1. many users store "web passwords" unprotected in their browser;
  2. a common password for all users would need to be changed for all if it's compromised even just for one user, so the user would be reluctant to report the incident to avoid disturbing everybody;
  3. to protect the password, you would need to deny plain HTTP access, and enforce HTTPS only; HTTPS may not be available to the remote users in some extreme cases;
    This may not be too relevant because in these cases probably also SSH would not be available.
  4. It would give you a false sense of security; it should instead always be clear that AutoGate cannot ever be a primary defence measure.

Also using a simple user authentication is something to be avoided, for the reasons above, plus:

  1. if you use the system login password of the user, it compromise would be a much more serious problem
  2. if you give each a different password for this, you would have to maintain them, and this leads to more work
<?php
  /**
   * This writes a file named as the client's IP in a directory
   * The directory is read by a daemon (autogate_daemon.sh)
   * that updates a specific iptables chain.
   * The daemon also creates the directory
   * with the appropriate permissions.
   */


$dir="/var/local/autogate";

if (isset($_GET['time'])) {
  $gracetime=$_GET['time'];
  if ( $gracetime>24*60) $gracetime=24*60;
 } else {
  $gracetime=30;
 }

if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  $ProxyIP=$_SERVER['REMOTE_ADDR'];
  $ClientIP=$_SERVER['HTTP_X_FORWARDED_FOR'];
  //$ClientIP="127.0.0.1"; // for testing
  //print "Proxy: ".$ProxyIP."\nClient: ".$ClientIP."\n";
  // check and ignore private addresses
  if (
      strpos($ClientIP,"127.")===0
      || strpos($ClientIP,"10.")===0
      || strpos($ClientIP,"192.168.")===0
      || strpos($ClientIP,"172.")===0
      && preg_match('/^172\.(?:1[6-9]|2[0-9]|3[01])\./',$ClientIP) // 172.16/12
      ) {
    $ClientIP=$ProxyIP;
  }
 } else {
  $ClientIP=$_SERVER['REMOTE_ADDR'];            
 }
//$ClientIP="x.0.0.0"; // for testing
if ( preg_match('/^[\d]+\.[\d]+\.[\d]+\.[\d]+$/',$ClientIP)==0 ) {
  print "Not a valid IP address: '".$ClientIP."'\n";
 } else {
  $FileName=$dir."/".$ClientIP."";
  if ($gracetime<=0) {
    unlink($FileName);
    echo "Removed $ClientIP\n";
  } else {
  $F=fopen($FileName,"w");
  if ($F!=false) {
    fwrite($F,$ClientIP."\n");
    fclose($F);
    $time = time() + $gracetime*60;
    touch ($FileName,$time,time());
    echo "Added $ClientIP until ".strftime('%F %H:%M %Z',$time)."\n";
  } else {
    echo "Cannot record $ClientIP\n";
  }
  }
 }
//phpinfo();
?>

AutoGate daemon

The AutoGate daemon is actually a shell script which may be started from rc.local or from inittab. It polls the chosen directory and, if the directory has a newer timestamp than the pid file, or if it's older than a chosen age (5 minutes) it processes its contents. First of all it will delete any file with a modification time older than the current system time; then it will flush the iptables chain and re-add all the surviving IPs.

At startup the daemon will take care of creating the directory with appropriate permissions, and the iptables chain.

This version of the daemon also support the usage of "AutoGate slave servers", which we'll discuss below.

#!/bin/bash
# generate an iptables chain from the file names = IPs in a directory

dir=/var/local/autogate
stamp=$dir/update-timestamp
pid=/var/run/autogate.pid
chain="SSH_DYN"
cleanmin=5  # cleanup every n minutes
slaves="" # slave autogate servers
slave_port=1111
DEBUG=false
#DEBUG=true

if [ "$EUID" != '0' ]; then
    echo "$0 must be executed suid"
    exit 0
fi

# kill other instances
#cat $pid
[ -s $pid ] && kill $(cat $pid)

if [ "$1" ]; then
    MODE=$1
    shift
else
    MODE="master"
fi

if [ $MODE = "master" ]; then
    mkdir -p $dir
    chown apache:wheel $dir
    chmod o-rwx $dir
    chcon --no-dereference -R -t httpd_user_content_t  $dir
fi

# spawn me with no console output
if [ "x$1" != "x--" ]; then
    echo "Spawning $0"
    $0 $MODE -- 1> /dev/null 2> /dev/null &
    exit 0
fi

function tell_slaves {
    local GRACE=$1
    shift
    for SLV in $slaves; do
        for FILE in $*; do
            IP=$(basename $FILE)
            echo -e "$IP\n$GRACE" | nc -w5 $SLV $slave_port # > /dev/null
        done
    done
}

function cleanOldFiles() {
    [ $MODE = "master" ] || return
    [ $DEBUG = true ] && echo "Cleaning $chain on "$(date)
    old=$(find $dir -mmin +0 -a -name "?*.?*.?*.?*")
    if [ "$old" ]; then
        [ $DEBUG = true ] && echo "Removing $old from $chain on "$(date)
        logger -t $chain "Removing $old"
        tell_slaves 0 $old
        rm -f $old
        touch $dir
    fi
}

function updateChain() {
    [ $DEBUG = true ] && echo "Updating $chain on "$(date)
    echo $$ > $pid
    iptables -F $chain
    for f in $dir/*.*.*.*; do
        ip=$(basename $f)
        if [ "$ip" != "*.*.*.*" ]; then
            [ $DEBUG = true ] && echo "Adding $ip to $chain"
            logger -t $chain "Adding $ip"
            #iptables -A "$chain" -p tcp -s "$ip" --dport ssh -j ACCEPT
            iptables -A "$chain" -p tcp -s "$ip" -j ACCEPT
            tell_slaves 1 $ip
        fi
    done
    #iptables -L $chain -v
}

iptables -N $chain
tell_slaves RESET 0.0.0.0
cleanOldFiles
updateChain
#iptables -L SSH_DYN -vn
N=0
while true; do
    #ls -lad  --full-time $dir $pid $stamp
    if [ $dir -nt $pid ]; then
        cleanOldFiles
        updateChain
        #iptables -L SSH_DYN -vn
    fi
    sleep 1
    if [ $[++N] -gt $[cleanmin*60] ]; then
        cleanOldFiles
        N=0
    fi
done

User script

For command-line users on Apple OsX or Linux, this wrapper script may be a helpful replacement of ssh:

#!/bin/bash
curl http://myserver.example.com/service/autogate.php?time=$[2*60]
if [ "$@" ]; then
   sleep 1
   exec ssh $@
fi

AutoGate slave servers

If you want to use autogate to secure a single server, you can stop reading here, you already have all that you need to setup. If instead you have a single web server, and you want to allow your users to also access other local servers, without having to install httpd and open HTTP ports on each of them, then proceed below.

Please note that if you have servers which don't have other reasons to run an HTTP service, you may not want to install it just for this purpose because, unless you use a non-standard port, the existence of an open HTTP port tends to attract much undesired attention. And it's preferable to use standard ports because your users at a remote location may be denied connections to non-standard ports, and consequently be unable to use the AutoGate system.

For the reasons above, I enhanced my old single-server AutoGate to have a single web front-end, the "master" that can open SSH access for a user on a number of other servers, the "slaves".

Please note that this master-slave system is not meant to be used in an exposed network. Beyond the basic restriction on the master IP, no access control is provided to guarantee that the request comes from the master daemon and not from a rogue user on the master server, or from a spoofed IP.

The slave servers run an inetd (or xinetd, or equivalent) server, listening on a chosen TCP port. The autogate daemon running on the master uses NetCat (nc) to communicate to the service on the slaves, over a TCP socket. The autogate daemon needs to be configured with the list of slaves, and the port number.

This is the xinetd definition of the service:

# /etc/xinetd.d/autogate_slave
service autogate_slave
{
disable = no
only_from = masterserver_IP
type = UNLISTED
port = 1111
log_type = SYSLOG daemon
#log_on_success = HOST PID EXIT
log_on_failure = HOST
socket_type = stream
protocol = tcp
wait = no
user = root
server = /usr/local/sbin/autogate_slave.sh
}

And this below is the actual slave server, which is called by xinetd, and gets its stdin and stdout connected to the TCP stream. From the input stream it reads one line with the IP, and a "grace" time. If the grace time is zero, the IP is removed from the chain; if it is "RESET", the chain will be flushed and all IPs removed.

#!/bin/bash
chain=SSH_DYN

read ip
# sanitise input
IP=$(echo "$ip" | head -1 | egrep '^[0-9]?[0-9]?[0-9]\.[0-9]?[0-9]?[0-9]\.[0-9]?[0-9]?[0-9]\.[0-9]?[0-9]?[0-9]$')
if [ -z "$IP" ]; then
    echo "FAIL Empty or bad IP $ip"
    exit 1
fi
unset ip
#echo "IP: $IP"

read grace
# sanitise input
GRACE=$(echo "$grace" | head -1 | egrep '^RESET$|^[0-9]+$')
if [ -z "$GRACE" ]; then
    echo "FAIL Empty or bad GRACETIME $grace"
    exit 1
fi
unset grace
#echo "GRACETIME: $GRACE"

# rule number in the chain, if present
IPTN=$(/sbin/iptables -L SSH_DYN -n --line-numbers | grep $IP | cut -d" " -f1)

#logger -t $chain "Req $GRACE $IP @$IPTN"

if [ $GRACE = "RESET" ]; then
    /sbin/iptables -N $chain 2>/dev/null
    /sbin/iptables -F $chain
    logger -t $chain "Resetting"
        echo "OK reset"
else
        if [ $GRACE -gt $[24*60] ]; then
        GRACE=$[24*60]
        fi
        if [ $GRACE -le 0 ]; then
        if [ "$IPTN" ]; then
                        /sbin/iptables -D $chain $IPTN
                        logger -t $chain "Removing $IP"
                        echo "OK rm"
                else
                        echo "FAIL missing"
                fi
                #rm -f $DIR/$IP
        else
        if [ -z "$IPTN" ]; then
                        /sbin/iptables -A $chain -s "$IP" -j ACCEPT
                        logger -t $chain "Adding $IP"
                        echo "OK add"
                else
                        echo "IGN present"
                fi
                #date --date "+$GRACE minutes"
                #T1=$(date --date "+$GRACE minutes" +%Y%m%d%H%M.%S)
                #echo "UNTIL: $T1"
                #touch -t $T1 $DIR/$IP
        fi
fi
#ls --full-time $DIR/$IP
#iptables -L SSH_DYN -n --line-numbers

I made an attempt at using lighter UDP sockets for this xinetd service, but the recipes I had found googling did not work; they provided sufficient information instead to make this TCP version.

Please note that the slave daemon does not create the SSH_DYN chain, so it must be configured manually.

Author

For all questions, please ask Sergio

Edit - History - Print - Recent Changes - Search
Page last modified on June 12, 2009, at 09:40 pm