Manuscript submission to SysAdmin Magazine Author Information Martin Krzywinski Genome Sciences Centre 600 W 10th Ave Vancouver BC V5Z 4E6 Canada tel 604.877.6086 martink@bcgsc.ca Author Biography Martin Krzywinski is a bioinformatics research scientist at the Genome Sciences Centre in Vancouver, British Columbia, Canada. He spends his time applying Perl to problems in physical mapping and data processing automation. In his spare time he can be found encouraging his cat to stick to her diet. PORT KNOCKING Network Authentication Across Closed Ports Two thousand years ago information security was already a concern to Julius Caesar, who is said to have been one of the first people to use cryptography to secure his dispatches. To encrypt his messages, he applied a circular shift to each letter moving it three places further in the alphabet. In the day when few were literate, the Caesar cipher was probably sufficient. In fact, evidence exists to indicate that cryptography was used by Hebrew scribes as early as 600-500 BC in the form of a reverse-alphabet cipher named ATBASH. Well before the inception of the first computer network, information security was a preoccupation for those responsible for safeguarding data. Today, with the exponential growth of information systems and of the internet population, effectively protecting information requires increasingly sophisticated methods. How do we keep our data protected from unauthorized access while keeping it accessible online to ourselves and to those we trust? The proliferation of the Slammer worm suggests that in some cases vulnerabilities in network applications are patched well after they are detected and a large number of automated discovery and attack tools are freely available. Keeping network applications updated and having a conservatively configured firewall can serve as the first line of defense. Equally important are intrusion detection systems (IDS), which are designed to spot known, derivative or anticipated attacks. While no connected system can be rendered safe, short of severing the network cable (sometimes the right solution), a properly configured firewall/IDS combination is an effective bulwark. However, packet filtering firewalls, such as the Linux kernel-based IPCHAINS/IPTABLES, filter on the basis of IP address and ports and do not contain an authentication mechanism to discriminate users. As a result, applications behind open ports remain vulnerable to attacks by malevolent individuals from unfiltered IP addresses. Port knocking is a firewall-based user authentication system which uses closed ports for authentication, making it possible to connect to a networked computer which has no open ports. Communication across closed ports is mediated through the firewall log, in which all connection attempts are recorded. The log is monitored for specific port sequences which encode information used to modify firewall rules, which are changed to open or close ports for a specific IP address. The system can be extended to implement the transfer of arbitrary information across closed ports. Port knocking is inherently stealthy because it is not possible to determine whether a host is listening to port knocks and because the authentication information flows in the form of connection attempts. Authentication Mechanism The application of the port knocking mechanism is illustrated in this section. A number of examples are used to show how knock sequences can be used to encode information. In the simplest case, a small number of ports are used to encode short knock sequences which map to specific modifications of the firewall rules. For example, a host with IP address FIREWALL may have all privileged ports closed, while ports 100-109 are monitored for connection attempts. The IPCHAINS command to achieve this is ipchains -A input -p tcp -s 0/0 -d FIREWALL/32 100:109 -j DENY -l ipchains -A input -p tcp -s 0/0 -d FIREWALL/32 1:1024 -j DENY Any connections to ports 100-109 will be silently ignored, but logged. The client will not receive ICMP error packets and therefore cannot determine whether an authentication process is taking place. Now suppose a client from IP address IPCLIENT makes a series of connection attempts to FIREWALL's ports 102,100,100,103, in that order. The firewall log will contain entries like Feb 12 00:13:26 ... input DENY eth1 PROTO=6 IPCLIENT:64137 FIREWALL:102 ... Feb 12 00:13:27 ... input DENY eth1 PROTO=6 IPCLIENT:64138 FIREWALL:100 ... Feb 12 00:13:27 ... input DENY eth1 PROTO=6 IPCLIENT:64139 FIREWALL:100 ... Feb 12 00:13:28 ... input DENY eth1 PROTO=6 IPCLIENT:64140 FIREWALL:103 ... Using a service daemon which monitors the firewall log, the port sequence 102,100,100,103 can be extracted and associated with the incoming IP address. This specific port sequence could act as a trigger to manipulate the firewall rules. For example, it could set off the following rule ipchains -I input -p tcp -s IPCLIENT/32 -d FIREWALL/32 22 -j ACCEPT which would have the effect of opening port ssh/22 to TCP traffic from IPCLIENT. The client which performed the knock sequence would now be able to connect to the ssh application and carry out a session. After the session ends, another knock sequence would be sent to close the port. The manner in which information is encoded in the port sequence is arbitrary. In the example above, a discrete mapping between a sequence and an action was illustrated. Just as well, the sequence could contain the port and the length of time it is held open. For example, the sequence could be 103,101,107 100+a,100+b,100+c,100+d 100+e 100+(a+b+c+d+e mod 10) 107,101,103 header payload time checksum footer The header and footer would indicate to the monitoring daemon that a sequence is about to start and end. Four numbers in the sequence payload encode the port to be opened. A time value indicates whether to (1) open the port, (2) open the port and then close it after some time has passed, or (3) close the port. In this example, 105 could mean open port abcd and keep it opened for 5*10=50 minutes. A final checksum field validates the authenticity of the sequence. A more sophisticated sequence would be encrypted and would contain the IP address for which ports are opened. This type of knock sequence provides a two-fold advantage. First, anyone intercepting the sequence cannot directly use it unless they manage to decrypt it, replace the client IP with their own and then re-encrypt it. Second, ports can be opened for a given IP address by using a different host, since the IP address of the client performing the knock is no longer used. This type of knock sequence could encrypt the following data, stored in 7 unsigned chars, with the range of the port limited to 0-255 for simplicity: IPbyte1 IPbyte2 IPbyte3 IPbyte4 port time checksum For example, if port 22 was to be opened to the client from IP address 142.103.205.1 for 15 minutes, the decrypted sequence would be 142 103 205 1 22 15 233 where the checksum is (142+103+205+1+22+15) mod 255 = 233. Using Perl, the encryption can be carried out using the cipher block chaining module Crypt::CBC and, to choose one algorithm, Crypt::Blowfish. In this example, the cipher object is created without using an initialization vector (IV). The IV is random data used to seed the algorithm to ensure that each encryption of the same data results in a different cipher text. Removing the IV reduces the length of the cipher text, and therefore of the encrypted knock sequence. In practise, an IV would be used, but in this example it is skipped. use constant KEY => "knock"; use constant CIPHER => "Blowfish"; $cipher = Crypt::CBC->new({key=>KEY,cipher=>CIPHER, iv=>"01234567",prepend_iv => 0}); The knock data is stored as an array, with the checksum as the last element. The sum() function from Math::VecStat is used to calculate the checksum. @data = (142,103,205,1,22,15) push(@data,sum(@data) % 255); # 142, 103, 205, 1, 22, 15, 233 The data is packed into a string, encrypted and then encoded into a list of unsigned chars. $ciphertext = $cipher->encrypt(pack("C*",@data)); @cipherpack = unpack("C*",$ciphertext); # 221, 169, 50, 219, 86, 117, 62, 187 Finally, the unsigned chars are mapped onto actual port values. For example ports 745-1000 could be used. use constant PORTMIN => 745; @knocks = map {PORTMIN+$_} @cipherpack; # 966, 914, 795, 964, 831, 862, 807, 932 The result is an encrypted 8 port knock sequence which contains the IP address, port, time flag and checksum. Once the knock sequence is received by the daemon monitoring the log, it is decrypted. Malformed sequences can be detected because the decryption step would not produce the expected data structure. Benefits of port knocking The primary feature of port knocking is that it allows for stealthy authentication into a networked host which has no open ports. The method is stealthy because it is not possible to directly determine whether the host is listening for port knocks. While port probes could be employed to try to guess which ports are being monitored and the format of the knock sequence, success is highly improbable and such brute-force attempts can be easily discovered with IDS tools. Since information is flowing in the form of connection attempts, rather than typical packet data payload, without knowing that this system is in place it is unlikely that this authentication method would be detected by packet sniffing methods. To minimize the risk of a functional knock sequence being reconstructed by a third party, the sequence should contain the client IP address and be encrypted. The system is flexible because the authentication scheme is built into the knock sequence. Existing applications, such as ssh, which perform their own authentication do not need to be changed. One-time port knocks can be implemented by adjusting the way particular sequences are interpreted. For example, a sequence could correspond to a request that a port be opened for only a specific length of time and then never opened again to the same IP. Furthermore, a one time pad could be used to encrypt the sequence, making it undecipherable by those without the pad. Disadvantages of port knocking As for any security system, the disadvantages present themselves as inconveniences, preferably small, which must be endured while the system is in use. These inconveniences should ideally pose less of a burden than the risk associated with not using the system. Port knocking requires a client script to perform the knock. This client and any associated data should be considered a secret and kept on removable media, such as a USB key. The use of the client imposes an overhead for each connection and users would require instruction on its appropriate use. For obvious reasons, port knocking cannot be used to protect ports of public applications, such as web or mail services. It is not reasonable to expect that each mail or web client will perform a port knock. This would require that everyone knows the secret knock, or how to construct one. If port knocking is used on a host to protect some ports (e.g., ssh, telnet, ftp) while others remain open for the public (e.g., web, mail) two options remain. Public services should preferably be delegated to a bastion host in the DMZ and isolated from the host on which sensitive information is stored. If the delegation is not possible, all public services must be regularly updated or patched to minimize vulnerability. Though some applications remain open to the public, the protection of others with port knocking would limit the number of possible types of attack. The implementation of any system which manipulates firewall rules in an automated fashion must be robust to prevent legitimate users from being locked out. If knocks cannot be interpreted properly or if the service daemon crashes, the networked host may become isolated. Appropriate safeguards should be implemented to avoid such a scenario. Implementation Both the client and server scripts are available at http://mkweb.bcgsc.ca/portknocking. knockclient A prototype client is shown in Listing 1. Lincoln Stein's Crypt::CBC module is used as proxy to Crypt::Blowfish to carry out encryption. The unencrypted knock sequence is comprised of 7 values: four IP bytes, a port (limited to the range 0-255 in this implementation), a time flag and a checksum (mod 255). The time flag determines how the daemon will react: 0 to open the port, 255 to close the port and any other value in the range 1-254 to open the port and then close it after that many minutes. The knock on the firewall (IP=FIREWALL) to open port ssh/22 on IP=IPCLIENT and have the port closed after 15 minutes would be executed by calling the client as follows knockclient -c IPCLIENT -r FIREWALL -p 22 -t 15 The client packs the list of 7 integers, performs the encryption and unpacks the string into unsigned chars (0-255). These values are then mapped onto a sequence of ports in the range 745-1000. knockdaemon The accompanying knock daemon is shown in Listing 2. This script will work with any firewall compatible with IPCHAINS syntax. The File::Tail module is used to continually look for new lines in the firewall log. Lines corresponding to connection attempts to ports 745-1000 are parsed for the remote IP and port number. Because the client produces encrypted sequences of 8 digits, a queue of this length is used to store the ports. Each connecting IP has its own queue. Every time the queue size reaches 8 an attempt is made to decrypt it. If the decryption is successful, and the checksum is correct, appropriate action is taken and the queue is cleared. If the decryption fails, the oldest queue port element is removed. The firewall rules are manipulated by a system call to the IPCHAINS binary, although the IPChains Perl module by Jonathan Schatz can also be used. The 'at' queue system provides a simple way to trigger closing a port after a specified amount of time. The interface to this is provided by Jose Rodrigues' Schedule::At module. Conclusion Port knocking is a stealthy network authentication system which uses closed ports to carry out identification of trusted users. This method provides the means of establishing a connection to an application running on a completely isolated system on which no ports are initially open. The use of port knocking to safeguard information makes it possible to dramatically increase the level of protection without sacrificing accessibility. Resources Port Knocking Page http://mkweb.bcgsc.ca/portknocking Randomness Recommendations for Security (RFC 1750) ftp://ftp.rfc-editor.org/in-notes/rfc1750.txt Codes and Ciphers: Julius Caesar, the Enigma and the Internet Robert Churchhouse, Cambridge University Press, 2002 Crypto 101, by Kurt Seifreit SysAdmin Journal, May 2002 Secure by Design, by William Kramp SysAdmin Magazine, May 1999 Registered Ports List (RFC 3232) ftp://ftp.rfc-editor.org/in-notes/rfc3232.txt http://www.iana.org IPCHAINS/IPTABLES http://www.netfilter.org An Introduction to Using Linux as a Multipurpose Firewall, by Jeff Regan Linux Journal, March 2000 http://www.linuxjournal.com/article.php?sid=3546 #Listing 1 use strict; use Math::VecStat qw(sum); use Crypt::CBC; use IO::Socket::INET; use Getopt::Long; use vars qw($opt_client $opt_remote $opt_port $opt_time); GetOptions("client=s","remote=s","port=i","time=i"); $opt_client || die "specify IP address (-client) for which the port will be opened."; $opt_remote || die "specify IP address (-remote) of server listening to knocks"; $opt_port || die "specify remote port (-port)"; $opt_time = (defined $opt_time)?$opt_time:5; use constant PORTMIN => 745; use constant KEY => "knock"; # keyphrase for the encryption use constant CIPHER => "Blowfish"; # encryption algorithm my $cipher = Crypt::CBC->new({key => KEY, iv => "01234567", prepend_iv => 0, cipher => CIPHER}); # data to encrypt will be our IP address + port + time + checksum my @data = (split(/\./,$opt_client),$opt_port,$opt_time); # compute crc and push it onto the data array push(@data,sum(@data) % 255); print "encrypting data ",join(" ",@data),"\n"; # encrypt the packed data my $ciphertext = $cipher->encrypt(pack("C*",@data)); # unpack the ciphered data into unsigned char values 0-255 my @cipherpack = unpack("C*",$ciphertext); # create the knock sequence by mapping to ports PORTMIN-PORTMIN+255 print "ciphered data ",join(" ",@cipherpack),"\n"; my @knocks = map {PORTMIN+$_} @cipherpack; print "knock sequence [",int(@cipherpack)," knocks]: ",join(" ",@knocks),"\n"; # create TCP connection to the remote host to each of the ports in the knock sequence for my $port (@knocks) { my $sock = IO::Socket::INET->new(PeerAddr => $opt_remote, PeerPort => $port, Timeout => 0.5, Proto => 'tcp'); print " knocked on $opt_remote:$port\n"; } exit 0; ### end of Listing 1 ### Listing 2 use strict; use File::Tail; use Crypt::CBC; use Schedule::At; use Math::VecStat qw(sum); use POSIX qw(strftime); use Pod::Usage; use Getopt::Long; use constant PORTMIN => 745; use constant KNOCKLENGTH => 8; use constant KEY => "knock"; use constant CIPHER => "Blowfish"; use vars qw($opt_file); die "cannot open firewall log file (-file FILENAME)" if ! -e $opt_file || ! -r _; my $file = File::Tail->new(name=>"$opt_file",maxinterval=>2); my %QUEUE; while(defined(my $line=$file->read)) { # pay attention only to DENY packets next unless $line =~ /DENY/; if($line =~ /PROTO=6 ([\d.]+):\d+ [\d.]+:(\d+)/) { my ($ip,$port) = ($1,$2); # pay attention only to ports allocated for knocking next if ($port < PORTMIN || $port > PORTMIN+255); print "knock from $ip to port $port\n"; # push this port to each queue item $QUEUE{$ip} = [] if ! $QUEUE{$ip}; push(@{$QUEUE{$ip}},$port); print "current queue ",join(" ",@{$QUEUE{$ip}}),"\n"; # if the queue is of the expected length, process it if(@{$QUEUE{$ip}} == KNOCKLENGTH) { ProcessQueue($QUEUE{$ip}); print "current queue @{$QUEUE{$ip}}\n"; } } } sub ProcessQueue { my $queue = shift; # try to decrypt the queue contents my $cipher = Crypt::CBC->new({key => KEY, iv =>"01234567", prepend_iv => 0, cipher => CIPHER}); my @data = unpack("C*", $cipher->decrypt(pack("C*", map {$_ - PORTMIN } @$queue))); # decrypted list must have 7 elements, otherwise remove oldest item and keep listening if(@data != 7) { print "ERROR could not decrypt properly\n"; shift(@$queue); return; } print "Got data ",join(" ",@data),"\n"; # check crc of knock sequence if ((sum(@data[0..@data-2]) % 255) == $data[-1]) { # extract information from the decrypted list my ($remoteip,$localport,$time) = (join(".",@data[0..3]),$data[4],$data[5]); print "$remoteip $localport $time\n"; # open port to remote ip system("/sbin/ipchains -I input -p tcp -s $remoteip/32 -d 0/0 $localport -j ACCEPT") if $time != 255; # close the port if time = 255 if($time == 255) { system("/sbin/ipchains -D input -p tcp -s $remoteip/32 -d 0/0 $localport -j ACCEPT "); } elsif ($time) { # schedule the port to be closed in $time minutes my $time = strftime "%Y%m%d%H%M", localtime(time+60*$time); my $command = "/sbin/ipchains -D input -p tcp -s $remoteip/32 -d 0/0 $localport -j ACCEPT"; print "scheduled $time $command\n"; Schedule::At::add(TIME=>$time,COMMAND=>$command); } # clear the queue @$queue = (); } else { print "ERROR bad crc\n"; shift(@$queue); } } ### end of Listing 2