Port Knocking and Other Uses of 'Recent Match'

Tom Eastep

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover, and with no Back-Cover Texts. A copy of the license is included in the section entitled “GNU Free Documentation License”.

2007/10/28



Table of Contents

What is Port Knocking?
Implementing Port Knocking in Shorewall
Limiting Per-IP Connection Rate
How Limit is Implemented

Note

The feature described in this article require 'Recent Match' in your iptables and kernel. See the output of shorewall show capabilities to see if you have that match.

What is Port Knocking?

Port knocking is a technique whereby attempting to connect to port A enables access to port B from that same host. For the example on which this article is based, see http://www.soloport.com/iptables.html which should be considered to be part of this documentation.

Implementing Port Knocking in Shorewall

In order to implement this solution, your iptables and kernel must support the 'recent match' extension (see FAQ 42).

In this example:

  1. Attempting to connect to port 1600 enables SSH access. Access is enabled for 60 seconds.

  2. Attempting to connect to port 1601 disables SSH access (note that in the article linked above, attempting to connect to port 1599 also disables access. This is an port scan defence as explained in the article).

To implement that approach:

  1. Add an action named SSHKnock (see the Action documentation). Leave the action.SSHKnock file empty.

  2. Create /etc/shorewall/SSHKnock with the following contents.

    If using Shorewall-shell:

    if [ -n "$LEVEL" ]; then
        log_rule_limit $LEVEL $CHAIN SSHKnock ACCEPT "" "$TAG" -A -p tcp --dport 22   -m recent --rcheck --name SSH
        log_rule_limit $LEVEL $CHAIN SSHKnock DROP   "" "$TAG" -A -p tcp --dport ! 22
    fi
    run_iptables -A $CHAIN -p tcp --dport 22   -m recent --rcheck --seconds 60 --name SSH          -j ACCEPT
    run_iptables -A $CHAIN -p tcp --dport 1599 -m recent                       --name SSH --remove -j DROP
    run_iptables -A $CHAIN -p tcp --dport 1600 -m recent                       --name SSH --set    -j DROP
    run_iptables -A $CHAIN -p tcp --dport 1601 -m recent                       --name SSH --remove -j DROP

    If using Shorewall-perl:

    use Shorewall::Chains;
    
    if ( $level ) {
        log_rule_limit( $level, 
                        $chainref, 
                        'SSHKnock',
                        'ACCEPT',
                        '',
                        $tag,
                        'add',
                        '-p tcp --dport 22   -m recent --rcheck --name SSH ' );
    
        log_rule_limit( $level,
                        $chainref,
                        'SSHKnock',
                        'DROP',
                        '',
                        $tag,
                        'add',
                        '-p tcp --dport ! 22 ' );
    }
    
    add_rule( $chainref, '-p tcp --dport 22   -m recent --rcheck --seconds 60 --name SSH          -j ACCEPT' );
    add_rule( $chainref, '-p tcp --dport 1599 -m recent                       --name SSH --remove -j DROP' );
    add_rule( $chainref, '-p tcp --dport 1600 -m recent                       --name SSH --set    -j DROP' );
    add_rule( $chainref, '-p tcp --dport 1601 -m recent                       --name SSH --remove -j DROP' );
    
    1;
  3. Now if you want to protect SSH access to the firewall from the Internet, add this rule in /etc/shorewall/rules:

    #ACTION          SOURCE            DEST           PROTO       DEST PORT(S)
    SSHKnock         net               $FW            tcp         22,1599,1600,1601

    If you want to log the DROPs and ACCEPTs done by SSHKnock, you can just add a log level as in:

    #ACTION          SOURCE            DEST           PROTO       DEST PORT(S)
    SSHKnock:info    net               $FW            tcp         22,1599,1600,1601
  4. If you wish to use SSHKnock with a forwarded connection, you must be using Shorewall 2.3.1 or later for fullest protection. Assume that you forward port 22 from external IP address 206.124.146.178 to internal system 192.168.1.5. In /etc/shorewall/rules:

    #ACTION          SOURCE            DEST            PROTO       DEST PORT(S)  SOURCE      ORIGINAL
    #                                                                            PORT(S)     DEST
    DNAT-            net               loc:192.168.1.5 tcp         22            -           206.124.146.178
    SSHKnock         net               $FW             tcp         1599,1600,1601
    SSHKnock         net               loc:192.168.1.5 tcp         22            -           206.124.146.178

    Note

    You can use SSHKnock with DNAT on earlier releases provided that you omit the ORIGINAL DEST entry on the second SSHKnock rule. This rule will be quite secure provided that you specify 'norfc1918' on your external interface.

For another way to implement Port Knocking, see the Manual Chain documentation.

Limiting Per-IP Connection Rate

Important

Debian users. This feature is broken in the Debian version 3.0.7 of Shorewall (and possibly in other versions). The file /usr/share/shorewall/Limit was inadvertently dropped from the .deb. That file may be obtained from Shorewall SVN and installed manually.

Beginning with Shorewall 3.0.4, Shorewall has a 'Limit' action. Limit is invoked with a comma-separated list in place of a logging tag. The list has three elements:

  1. The name of a 'recent' set; you select the set name which must conform to the rules for a valid chain name. Different rules that specify the same set name will use the same set of counters.

  2. The number of connections permitted in a specified time period.

  3. The time period, expressed in seconds.

Connections that exceed the specified rate are dropped.

For example,to use a recent set name of SSHA, and to limiting SSH to 3 per minute, use this entry in /etc/shorewall/rules:

#ACTION                SOURCE            DEST           PROTO       DEST PORT(S)
Limit:none:SSHA,3,60   net               $FW            tcp         22

If you want dropped connections to be logged at the info level, use this rule instead:

#ACTION                SOURCE            DEST           PROTO       DEST PORT(S)
Limit:info:SSHA,3,60   net               $FW            tcp         22

To summarize, you pass four pieces of information to the Limit action:

  • The log level. If you don't want to log, specify "none".

  • The name of the recent set that you want to use ("SSHA" in this example).

  • The maximum number of connections to accept (3 in this example).

  • The number of seconds over which you are willing to accept that many connections (60 in this example).

How Limit is Implemented

For those who are curious, the Limit action is implemented in Shorewall 3.0 and Shorewall 3.2 as follows:

  • The file /usr/share/shorewall/action.Limit is empty.

  • The file /usr/share/shorewall/Limit is as follows:

    set -- $(separate_list $TAG)
    
    [ $# -eq 3 ] || fatal_error "Rule must include <set name>,<max connections>,<interval> as the log tag"
    
    run_iptables -A $CHAIN -m recent --name $1 --set
    
    if [ -n "$LEVEL" ]; then
        run_iptables -N $CHAIN%
        log_rule_limit $LEVEL $CHAIN% $1 DROP "" "" -A
        run_iptables -A $CHAIN% -j DROP
        run_iptables -A $CHAIN -m recent --name $1 --update --seconds $3 --hitcount $(( $2 + 1 )) -j $CHAIN%
    else
        run_iptables -A $CHAIN -m recent --update --name $1 --seconds $3 --hitcount $(( $2 + 1 )) -j DROP
    fi
    
    run_iptables -A $CHAIN -j ACCEPT

In Shorewall 3.3, Limit is made into a built-in action; basically that means that the above code now lives inside of Shorewall rather than in a separate file.

For completeness, here's the above /usr/share/shorewall/Limit for use with Shorewall-perl:

my @tag = split /,/, $tag;

fatal_error 'Limit rules must include <set name>,<max connections>,<interval> as the log tag (' . join( ':', 'Limit', $level eq '' ? 'none' : $level , $tag ) . ')'
    unless @tag == 3;

my $set = $tag[0];

for ( @tag[1,2] ) {
    fatal_error 'Max connections and interval in Limit rules must be numeric (' . join( ':', 'Limit', $level eq '' ? 'none' : $level, $tag ) . ')' unless /^\d+$/
}

my $count = $tag[1] + 1;

add_rule $chainref, "-m recent --name $set --set";

if ( $level ) {
    my $xchainref = new_chain 'filter' , "$chainref->{name}%";
    log_rule_limit $level, $xchainref, $tag[0], 'DROP', '', '', 'add', '';
    add_rule $xchainref, '-j DROP';
    add_rule $chainref,  "-m recent --name $set --update --seconds $tag[2] --hitcount $count -j $xchainref->{name}";
} else {
    add_rule $chainref, "-m recent --update --name $set --seconds $tag[2] --hitcount $count -j DROP";
}

add_rule $chainref, '-j ACCEPT';

1;