Port knocking with OTP to secure SSH port

Image by LittleVisuals from Pixabay

There are various ways to block public access to SSH. The most common method is to hide them behind VPNs, followed by changing default SSH port and then using fail2ban or CSF to block IP addresses who are doing a port.

Of course, If you have a static IP address, you can whitelist your IP address to access SSH port. But that does not work if your IP address constantly changes or if you connect from a different location each time. 🧐

An interesting concept that I’ve come across is port knocking. Note that this is just an additional layer of security. Port knocking is sending packets to a pre-defined sequence of ports, so that recipient knows it is coming from a trusted client and open the port for you. It is the same concept of making a secret doorbell ring pattern with your close friends so that you know who is ringing the door!

For example, our SSH server is available at port 22 and this port is blocked via a firewall. By using port knocking, we can tell the firewall to momentary open port 22 for us to connect, only if within 5 seconds, we knock TCP port 11000, then UDP port 13500 and then TCP port 9000 in the correct sequence.

This is easily achievable by using a combination of knockd-server and iptable. This simple implementation of port knocking will keep away those automated bots who constantly scan ports to detect a connectable SSH port.

Basic port knocking implementation has some drawbacks 😞 as it is still possible for a bot to detect our sequence combination. Or for a man-in-the-middle to sniff our network and detect which ports we are knocking every time we are connecting to SSH. These drawbacks can defeat the whole purpose of using it in the first place. That being said, Let’s not forget that even if they gain access to our SSH port, they still need to authenticate to connect to the SSH server. SPA (via fwknop) is an alternative option, by sending single authenticated packet which then opens a specific port. It is an interesting approach too.

I wanted to give port knocking a try, but the concept of having a fixed set of port sequences to use every was not giving me ease of mind. Luckily, knockd-server allow a simple list of port sequences to be used as one-time-sequence, it will mark a sequence as soon as they are used. But again, there is a drawback. The list has to be shared with the client and if multiple clients access a port, the client list can go out of sync once another client uses a sequence that is already expired by previous client usage. 😳

I was looking for a method to allow random knock sequences for each connection while maintaining convenience for clients to connect. Here’s where enhanced port knocking with OTP comes to rescue.

I combined knockd-server’s one-time sequence file with a PHP script to generate a new sequence whenever an HTTPS request is made to the script with a correct time-based integrity code. You can use other languages if you are more comfortable with them. 👍

Given the correct key for the current timestamp:

  1. The PHP script will generate a new random sequence
  2. Load the generated sequence as one-time sequence into knockd-server
  3. Give generated one-time sequence to the client via HTTPS.
  4. Port knocking client (knock) use retrieved sequence to send packets to the server.
  5. knockd-server will match the received knocks with PHP generated sequence and mark the sequence as expired.
  6. knockd-server open SSH port to accept new connections for 5 seconds for client IP address.
  7. SSH client access SSH port and establish the connection.
  8. Firewall close the SSH port for new connections but allows established connection to be kept alive.

Best of all, all these steps occur automatically whenever a new SSH connection to a specific server is requested. This provides a secure yet convenient way of accessing SSH port. The source code and config files to achieve this are available in this git repository.

If you want to further secure your SSH, you can enable 2FA authentication after a successful key file(/password👀) authentication, but that is out of the scope of this article. Due to nature of SEO, Google might show this article to you if you search for SSH OTP, so if you come here for that, here is the link for that guide: You can read more about it here.

The experiment that I have done here, might have or do not have flaws and if you use it, you accept the risks yourself. This is just an additional layer of security and cannot be used as the only security approach. I am not responsible if there are any issues arise by following this experiment.

How to automate OTP SSH Port-knocking in a secure yet convenient way?

Here we get technical 🤓 to demonstrate how exactly this is achieved.

You will need knockd-server in your server, and knock in your client.

knockd-server scan received packets and if they match a specific sequence, it will run a command. This command is usually an iptables or firewalld command to open the port but It can be anything else, such as a custom bash script.

knock client is a companion app for knockd-server which makes it convenient to send packets into specified ports. So you need to have knock installed in your client. Packets can be sent via netcat too, but we are using knock to shorten the overall configuration.

To do this experiment, I wrote a quick PHP script and assembled an example CentOS 7 LAMP server with knockd-server on it.

In knockd-server configuration file (usually in /etc/knockd.conf) we specify a block for SSH:

[KnockForSSH]
one_time_sequences = /var/www/public_html/onetime_sequence_file.txt
seq_timeout = 5
tcpflags = syn
start_command = /sbin/iptables -I INPUT -s %IP% -p tcp — dport 22 -j ACCEPT
cmd_timeout = 5
stop_command = /sbin/iptables -D INPUT -s %IP% -p tcp — dport 22 -j ACCEPT

This block tells knockd-server to look for packets within 5 seconds duration if are a match with any of sequence file rows, then open port 22 for the client IP address, and after 5 seconds, close the port. We are using iptables example here. It can be done with firewalld or any other firewalls.

And I’ve used a PHP script to generate that onetime_sequence_file.txt. PHP script would generate a new sequence file every time it is triggered with the correct key, then reload the knockd-server to use this new sequence. Note that you need to add restart command as one of the sudoer commands.

This PHP file will be available over HTTPS protocol to give the current sequence to a client with a valid key. In this example we serve it over https://1.2.3.4/generate.php available at /var/www/public_html/generate.php.

The secure_keyword specified here, must match with our SSH client config secure key that we will specify in next step.

<?phperror_reporting(0);#This is your secure key, which must match with client.
$secure_keyword='type_a_secure_key_here';
#This is the file that knockd will read sequences from
$sequence_file='onetime_sequence_file.txt';
#This is the number of knocks needed to unblock ports
$sequence_length=4;
#Knock port range
$port_min=15000;
$port_max=25000;
#Get client request
$time=$_GET['time'];
$code=$_GET['code'];
#Validate client request
if ($time=='') $teapot=true;
if (round($time)==0) $teapot=true;
if ($time<(time()-3)) $teapot=true;
if (md5($_SERVER['REMOTE_ADDR'].$secure_keyword.$time)!=$code) $teapot=true;
#If client request cannot be verified, convert to a teapot
if (isset($teapot)&&$teapot){
header("HTTP/1.1 418 I'm a teapot");
?>
<title>418 I'm a teapot</title>
<p>This server refuses to brew coffee because it is, permanently, a teapot.</p>
<?php
die();
}
#Generate new random sequence
$codes=[];
for ($i=0; $i<=$sequence_length; $i++){
$codes[]=mt_rand($port_min,$port_max).":".(mt_rand(0,1)==1?"tcp":"udp");
}
$new_otp=" ".implode(",",$codes);
#Save new knockd sequence file
file_put_contents($sequence_file,$new_otp);
#Display new sequence for client
echo str_replace(","," ",$new_otp);
#reload knockd server, so it load new sequence file
exec("/bin/sudo /sbin/service knockd restart");
?>

Now, we configure our SSH client config file (~/.ssh/config), so that it allows for convenient retrieval of knocking correct sequence and accessing the SSH port:

Host myserver
ProxyCommand bash -c 'MYIP=$(dig +short myip.opendns.com @resolver1.opendns.com.);knock %h -d 100 $(curl -ksS "https://1.2.3.4/generate.php?time=$(php -r "echo time();")&code=$(md5 -qs "${MYIP}type_a_secure_key_here$(php -r "echo time();")")");sleep 0.5; nc %h %p'
Hostname 1.2.3.4

Notice that our example server IP (1.2.3.4) is being used 2 times. Once for getting OTP sequence from PHP, and second, to connect to the server via SSH. This ProxyCommand generate the correct key using a matching secure_keyword with PHP script and knock those ports.

How to connect to my server now?

ssh myserver

Yes, as simple as that! At this point, If everything is working correctly, you can connect to your server’s port 22 just by typing ‘ssh myserver’ into your terminal. SSH client will take care of everything for you in the background.

You can see the source codes and config files I’ve used in the following git repository.

Warning!
Before closing port 22 for the public, make sure you monitor knockd-server log file to ensure it is indeed receiving the correct knocks and opening the port. otherwise, you might lock yourself out and not be able to gain SSH access to your server anymore!

Coders changed the world. To be continued…

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store