Analysis And Usage of SSHGuard

· klm's blog

SSHGuard is a software to block unwanted SSH login attempts. It is similar to the fail2ban software, but easier to useand a lot smaller.

Original post is here: eklausmeier.goip.de

To ban annoying ssh access to your Linux box you can use fail2ban. Or, alternatively, you can use SSHGuard. SSHGuard's installed size is 1.3 MB on Arch Linux. Its source code, including all C-files, headers, manuals, configuration, and makefiles is 100 KLines. ~~In contrast,~~For fail2ban the Python source code of version 0.11.2 is 31 KLines, not counting configuration files, manuals, and text files; its installed size is 3.3 MB. fail2ban is way slower than SSHGuard. For example, on one machine fail2ban used 7 minutes of CPU time, where SSHGuard used 11 seconds. I have written on fail2ban in "Blocking Network Attackers", "Chinese Hackers", and "Blocking IP addresses with ipset".

SSHGuard is a package in Arch Linux, and there is a Wiki page on it.

1. Internally SSHGuard maintains three lists:

  1. whitelist: allowed IP addresses, given by configuration
  2. blocklist: list of IP addresses which are blocked, but which can become unblocked after some time, in-memory only
  3. blacklist: permanently blocked IP addresses, stored in cleartext in file

SSHGuard's main function is summarized in below excerpt from its shell-script /bin/sshguard.

1eval $tailcmd | $libexec/sshg-parser | \
2    $libexec/sshg-blocker $flags | $BACKEND &
3wait

There are four programs, where each reads from stdin and writes to stdout, and does a well defined job. Each program stays in an infinite loop.

  1. $tailcmd reads the log, for example via tail -f, which might contain the offending IP address
  2. sshg-parser parses stdin for offending IP's
  3. sshg-blocker writes IP addresses
  4. $BACKEND is a firewall shell script which either uses iptables, ipset, nft, etc.

sshg-blocker in addition to writing to stdout, also writes to a file, usually /var/db/sshguard/blacklist.db. This is the blacklist file. The content looks like this:

11613412470|100|4|39.102.76.239
21613412663|100|4|62.210.137.165
31613415749|100|4|39.109.122.173
41613416009|100|4|80.102.214.209
51613416139|100|4|106.75.6.234
61613418135|100|4|42.192.140.183

The first entry is time in time_t format, second entry is service, in our case always 100=ssh, third entry is either 4 for IPv4, or 6 for IPv6.

SSHGuard handles below services:

 1enum service {
 2    SERVICES_ALL            = 0,    //< anything
 3    SERVICES_SSH            = 100,  //< ssh
 4    SERVICES_SSHGUARD       = 110,  //< SSHGuard
 5    SERVICES_UWIMAP         = 200,  //< UWimap for imap and pop daemon
 6    SERVICES_DOVECOT        = 210,  //< dovecot
 7    SERVICES_CYRUSIMAP      = 220,  //< cyrus-imap
 8    SERVICES_CUCIPOP        = 230,  //< cucipop
 9    SERVICES_EXIM           = 240,  //< exim
10    SERVICES_SENDMAIL       = 250,  //< sendmail
11    SERVICES_POSTFIX        = 260,  //< postfix
12    SERVICES_OPENSMTPD      = 270,  //< OpenSMTPD
13    SERVICES_COURIER        = 280,  //< Courier IMAP/POP
14    SERVICES_FREEBSDFTPD    = 300,  //< ftpd shipped with FreeBSD
15    SERVICES_PROFTPD        = 310,  //< ProFTPd
16    SERVICES_PUREFTPD       = 320,  //< Pure-FTPd
17    SERVICES_VSFTPD         = 330,  //< vsftpd
18    SERVICES_COCKPIT        = 340,  //< cockpit management dashboard
19    SERVICES_CLF_UNAUTH     = 350,  //< HTTP 401 in common log format
20    SERVICES_CLF_PROBES     = 360,  //< probes for common web services
21    SERVICES_CLF_LOGIN_URL  = 370,  //< CMS framework logins in common log format
22    SERVICES_OPENVPN        = 400,  //< OpenVPN
23    SERVICES_GITEA          = 500,  //< Gitea
24};

2. A typical configuration file might look like this:

1LOGREADER="LANG=C /usr/bin/journalctl -afb -p info -n1 -t sshd -o cat"
2THRESHOLD=10
3BLACKLIST_FILE=10:/var/db/sshguard/blacklist.db
4BACKEND=/usr/lib/sshguard/sshg-fw-ipset
5PID_FILE=/var/run/sshguard.pid
6WHITELIST_ARG=192.168.178.0/24

Furthermore one has to add below lines to /etc/ipset.conf:

1create -exist sshguard4 hash:net family inet
2create -exist sshguard6 hash:net family inet6

Also, /etc/iptables/iptables.rules and /etc/iptables/ip6tables.rules need the following link to ipset respectively:

1-A INPUT -m set --match-set sshguard4 src -j DROP
2-A INPUT -m set --match-set sshguard6 src -j DROP

3. Firewall script sshg-fw-ipset, called "BACKEND", is essentially:

 1fw_init() {
 2    ipset -quiet create -exist sshguard4 hash:net family inet
 3    ipset -quiet create -exist sshguard6 hash:net family inet6
 4}
 5
 6fw_block() {
 7    ipset -quiet add -exist sshguard$2 $1/$3
 8}
 9
10fw_release() {
11    ipset -quiet del -exist sshguard$2 $1/$3
12}
13
14...
15
16while read -r cmd address addrtype cidr; do
17    case $cmd in
18        block)
19            fw_block "$address" "$addrtype" "$cidr";;
20        release)
21            fw_release "$address" "$addrtype" "$cidr";;
22        flush)
23            fw_flush;;
24        flushonexit)
25            flushonexit=YES;;
26        *)
27            die 65 "Invalid command";;
28    esac
29done

The "BACKEND" is called from sshg-blocker as follows:

 1static void fw_block(const attack_t *attack) {
 2    unsigned int subnet_size = fw_block_subnet_size(attack->address.kind);
 3
 4    printf("block %s %d %u\n", attack->address.value, attack->address.kind, subnet_size);
 5    fflush(stdout);
 6}
 7
 8static void fw_release(const attack_t *attack) {
 9    unsigned int subnet_size = fw_block_subnet_size(attack->address.kind);
10
11    printf("release %s %d %u\n", attack->address.value, attack->address.kind, subnet_size);
12    fflush(stdout);
13}

SSHGuard is using the list-implementation SimCList from Michele Mazzucchi.

4. sshg-parser uses flex (=lex) and bison (=yacc) for evaluating log-messages. An introduction to flex and bison is here. Tokenization for ssh using flex is:

1"Disconnecting "[Ii]"nvalid user "[^ ]+" "           { return SSH_INVALUSERPREF; }
2"Failed password for "?[Ii]"nvalid user ".+" from "  { return SSH_INVALUSERPREF; }

Actions based on tokens using bison is:

 1%token SSH_INVALUSERPREF SSH_NOTALLOWEDPREF SSH_NOTALLOWEDSUFF
 2
 3msg_single:
 4    sshmsg            { attack->service = SERVICES_SSH; }
 5  | sshguardmsg       { attack->service = SERVICES_SSHGUARD; }
 6  . . .
 7  ;
 8
 9/* attack rules for SSHd */
10sshmsg:
11    /* login attempt from non-existent user, or from existent but non-allowed user */
12    ssh_illegaluser
13    /* incorrect login attempt from valid and allowed user */
14  | ssh_authfail
15  | ssh_noidentifstring
16  | ssh_badprotocol
17  | ssh_badkex
18  ;
19
20ssh_illegaluser:
21    /* nonexistent user */
22    SSH_INVALUSERPREF addr
23  | SSH_INVALUSERPREF addr SSH_ADDR_SUFF
24    /* existent, unallowed user */
25  | SSH_NOTALLOWEDPREF addr SSH_NOTALLOWEDSUFF
26  ;

Once an attack is noticed, it is just printed to stdout:

1static void print_attack(const attack_t *attack) {
2    printf("%d %s %d %d\n", attack->service, attack->address.value,
3           attack->address.kind, attack->dangerousness);
4}

5. For exporting fail2ban's blocked IP addresses to SSHGuard one would use below SQL:

1select ip from (select ip from bans union select ip from bips)

to extract from /var/lib/fail2ban/fail2ban.sqlite3.

6. In case one wants to unblock an IP address, which got blocked inadvertently, you can simply issue

1ipset del sshguard4 <IP-address>

in case you are using ipset as "BACKEND". If this IP address is also present in the blacklist, you have to delete it there as well. For that, you must stop SSHGuard.

Added 18-Aug-2024: I had a number of problems with SSHGuard.

  1. Sometimes, when the entire machine is shut down, SSHGuard "hangs", i.e., it needs additional time to fully stop
  2. Sometimes, SSHGuard does not add ipset entries

To counter that I now use below shell script, which also adds logging:

 1#!/bin/sh
 2# Connect sshguard programs together with tee -a
 3
 4SL=/dev/null
 5SL=/tmp/sshguard.log
 6
 7/usr/bin/journalctl -afb -p info -n1 -t sshd -t sshd-session -o cat	\
 8	| tee -a $SL	\
 9	| /usr/lib/sshguard/sshg-parser	\
10	| tee -a $SL	\
11	| /usr/lib/sshguard/sshg-blocker -a 10 -b 10:/var/db/sshguard/blacklist.db -p 3600 -w 192.168.0.0/16	\
12	| tee -a $SL	\
13	| /usr/local/bin/sshg-fw-ipset.klm	\
14	| tee -a $SL