Port Knocking using TOTP on Linux and AWS

It all started with this paper from SANS, and it struck us with wonder. Can we actually request to open a closed port externally on demand?! It seemed too good to be true. We studied this with a little more depth and a month later, we had a slightly modernised version of Port Knocking. Let us understand from the basics, what we have implemented here.

Also, you can find our PoC for this at https://github.com/livingstonetech/port-knocking-totp.

We have covered a few topics in, what might seem to you, as an unnecessary amount of depth or you might just not want to waste your time on it because you are already aware of these concepts. No worries, we have you covered with this Index. CTRL\CMD + F away!

  • What is a Port? How do we connect to it?
  • Why port knocking?
  • Traditional Port Knocking
    — What is Port Knocking?
    — Infusing TOTP with Port Knocking
    — TOTP Internals
    — How we use the OTP with Port Knocking
  • Implementation of Port Knocking with TOTP on Linux
  • Port Knocking on the cloud?!
    — Summary of Traditional Port Knocking
    — Security Groups in AWS
    —Implementation of Port Knocking with TOTP on AWS
  • Footnote

What is a Port? How do we connect to it?

Let us start from the very beginning; from the port. A port is an endpoint in communication over a network, which is designated by a 16-bit number. Ports, as we know them, are application specific. For example, Port 80 is used for HTTP, Port 22 is used for SSH, so on and so forth. Now, these ports can be in (generally) two states: Open and Closed.

An Open port is a port that is ready to accept connections. A closed port, on the contrary, is a port that the kernel will drop or reject any attempts that are made to connect to it. Also, a connection is a TCP Connect. A TCP Connect consists of a three-way handshake as described here.

Why Port Knocking?

Let us discuss the need for this first. Let’s take the example of an FTP service, with basic authentication. If you keep this port, 21 in most cases, exposed to the internet, you will probably get hundreds of attempted network connections to this port every week! This increases the risk of the service being compromised, as the attack surface is still exposed to the internet (example attack vectors can be brute force guessing the credentials). The only hope we can hang on here is that the password to the FTP service isn’t weak.

Using Port Knocking, you can open the FTP port only when it is required, and will remain closed when not in use. Most importantly, you can open this port from the outside, without having to log into the server. This will greatly reduce the risk of the FTP service being compromised and most attack vectors will become inapplicable as the port is closed after use.

Traditional Port Knocking

What is Port Knocking?

“Port Knocking is a mechanism to externally open ports on the firewall by sending a set of packets in a certain pre-defined order.”

We prefer this definition because it is a very open definition. A set of packets here can also be TCP packets with certain flags, TCP Connect requests, anything. In our case, however, we used TCP connect requests.

A real-life metaphor for this can be the scene from the movie Leon the Professional, where Leon tells Mathilda to open the door only for a certain pattern of door knocks. In fact, the packets sent to open a certain port to the firewall are also called knocks.

Port Knocking in a meme

So consider a scenario where, for some reason, you would want to not expose the port 22 of your server all the time. You’d want to open it only when you need it and close it when you don’t need it. Port Knocking will allow you to do this. Conceptually, like this:

  • Select a few higher ports, say, 6543, 7658, 9013.
  • Write a Firewall rule that says, if there is a connect packet received on these ports in the given order, open port 22. These packets are your knocks.
  • Connect to port 22 over SSH and while disconnecting, enable the Firewall rule that closes port 22.

This, in essence, is what Port Knocking is. It has been around for years. How do we implement this? We use a program called knockd. knockd changes the iptables rules according to the specified ports in the knockd.conf file. This file looks something like this:

[options]
logfile = <LOG FILE FOR ALL KNOCKD EVENTS>
Interface = <INTERFACE>
[TOTP]
sequence = <KNOCKS> # Eg. 5432,9087
tcpflags = <PACKET TYPE> # Eg. syn
seq_timeout = <TIME BETWEEN UNITS OF THE SEQUENCE>
start_command = <IPTABLES COMMAND TO OPEN PORT>
cmd_timeout = <TIMEOUT BETWEEN START AND STOP COMMANDS>
stop_command = <IPTABLES COMMAND TO CLOSE PORT>

knockd and related files are well explained here. knock is a simple client to send these knocks to a specified host and its ports, which we have used during the demos. This can be replaced by a simple telnet or netcat too.

This is a good time to understand the start_command and stop_command that we have used in our implementation.

start_command: /sbin/iptables -F;/sbin/iptables -A INPUT -p tcp --dport {port} -j ACCEPTstop_command: /sbin/iptables -D INPUT -p tcp --dport {port} -j ACCEPT; /sbin/iptables -A INPUT -p tcp --dport {port} -j REJECT

These are both iptables commands, with specific instructions to the firewall. It should be noted that the {port} here is the port we are trying to protect.

In the start command, we are doing two things. We are flushing all the existing rules. Then, we make an entry, or more accurately, append (-A) an entry to the INPUT chain. In this entry, we are telling the firewall to ACCEPT packets of protocol (-p) TCP, directed to port ( --dport) specified by us. Essentially, flushing existing rules configured on the firewall and making a new rule to accept traffic to our protected port.

In the stop command, we are again, doing two things. We will first delete the entry ( -D) we made in the INPUT chain. Then, we will explicitly put an entry to REJECT any TCP traffic to our protected port. Essentially, closing the protected port.

This is what worked for us and served our use case. If you wish to kaizen this configuration or have your own, please feel free and let us know about it too. For more information about iptables have a look here.

Infusing TOTP with Port Knocking

So in the attempt to modernise the traditional Port Knocking, we wanted to introduce a factor that was dynamic. The traditional implementation of Port Knocking in itself is very static. The Ports are pre-decided and if an adversary knows about these ports, then they will bypass the mechanism with no problems at all.

Thus, to counter this problem, we decided to make the ports dynamic using Time-based One Time Password or TOTP. If you think you don’t know what a TOTP is, you probably do and you just don’t know that it is called that. I am talking about the codes you get on the Google Authenticator App for two-factor-authentication. Yes! Now you know 😉.

TOTP Internals

Okay, so this section is purely optional. It delves into the TOTP algorithm as described in the RFC. The section will explain this algorithm using an example where we actually calculate an OTP. You can skip this without any consequences. Just remember the idea: TOTP gives us a 6-digit one-time password every 30 seconds.

Time-based One Time Password, as the name suggests, has two core components: the Time and the Password, that is, the Secret. The algorithm too is pretty straightforward. Let’s understand this using an example:

  • We need a secret that is in Base32 format.
    Let his secret be RPFYC5FREUVZ22I7
  • We decode this value to get actual bytes.
    Base32Decode('RPFYC5FREUVZ22I7') => (0x8b,0xcb,0x81,0x74,0xb1,0x25,0x2b,0x9d,0x69,0x1f)
  • Now we calculate the time. Since we are rotating our password every 30 seconds, we will divide the current Unix timestamp by 30 and floorit. This will make sure that every time the OTP is calculated within a 30-second window, we will always get the same OTP. Okay so calculating…
    current_time = floor(unix_ts / 30) = (822997400/30) = 2765580
  • Okay! Now we have our two components, time and secret. Now we create a HMAC-SHA1 digest using time as the message and our secret as the key.
    HMAC_SHA1(secret, time) => (0xe9,0x4e,0x53,0xee,0xc2,0x13,0x0b,0x7a,0xdd,0x2d,0x08,0xa4...)
  • Now we convert the HMAC-SHA1 sum to a number. We will mod (%) this number by 1000000. The reason is simple: our OTP is the last 6 digits of this number and the rest of the numbers are of no use to us. (Some RFC implementations have used 10000000 as the mod, and according to the RFC, that is completely compliant too.)
    a TOTP = int(hmac_sha1_sum) % 1000000 = 719686
  • A final step, though not required in this particular example, pad the number with 0’s till our power is reached. For example, if after the mod operation our result would’ve been 7293 then we would pad two 0’s to it and make it 729300 which will be our final OTP

Phew! So that was TOTP, in as much detail as there is. Please refer to our gist here for a reference implementation in Python3.

How we use the OTP with Port Knocking

Every 30 seconds, we will get a six-digit OTP. We take this OTP and take apart the first four digits and the last four digits into two groups. If any group’s value is less than 1000, we append 0’s at the end to make sure it is greater than or equal to 1000.

The goal is to dissect the OTP into two four digit numbers, which will become the ports that will be knocked to open the hidden port

Let us illustrate this:

Let our OTP be 970113. Now let’s take the first four and last four digits into two groups, say Group A and Group B.
Group A = 9701
Group B = 0113
As we can see, Group B is not a 4 digit number, so we append a zero at the end to make sure that it is a 4 digit number. So Group B becomes 1130.
So now, the knocking sequence will be a SYN packet on port 9701 and then a SYN packet on port 1130 and open sesame!

That’s the idea, and on Linux, we will implement this using iptables and knockd.

Implementation of Port Knocking on Linux with TOTP

Now let us get into the implementation specifics. To avoid confusion, we will remember this for the remainder of the article:

knocked port   : Protected port 
sequence ports : Ports that receive the knocks (closed ports)

If you have skipped to this section, I recommend you to scroll up and read how we have configured the knockd.config file, as it plays a very crucial role. Now functionally, the program would do the following things:

1. Calculate OTP at every instance in time.

The OTP will be used to derive the ports that will be used as the sequence ports. The derivation of ports from the OTP is as explained above.

2. Change "knockd.config” file to reflect the new sequence ports.

After deriving the sequence ports from the OTP, we will have to change the knockd.config file to reflect those ports. In this step, we will write a new knockd.config file in the knockd directory in /etc/. So now, when the knockd service starts, it will read the new config file with the new sequence ports.

3. Restart knockd service.

Great! Now we have our new config ready, so we need to set it as the current config. We simply do this by restarting the knockd service.

This is all that the main program will do, just wrap around the knockd program (why to change something that works flawlessly, right? 🤷‍♂). So with every new config, there will be different sequence ports according to the TOTP at the instant. Responding to knocks by changing firewall rules and closing the knocked port after, is fully managed by knockd.

That is Port Knocking on Linux with TOTP. Now let’s head over to the Cloud, where everything means the same but looks very different.

Port Knocking on the Cloud?!

Summary of Traditional Port Knocking:

  • Based on a TOTP secret, we opened certain high number ports (sequence ports) changing every 30 seconds.
  • If we get a sequence of TCP SYN packets in order, then we modify the firewall (iptables) to actually open the port, on which we want to communicate (knocked port).

The need for port knocking on the cloud is furthermore because customarily the cloud instances are exposed on the internet which makes them vulnerable to random scanners. So we decided to implement port knocking in an EC2 instance on AWS. Did it work? NO. As explained in the earlier section, using iptables the firewall rules on the EC2 instance were getting enforced but the packets (knocks) never reached the EC2 instance. Why?

Security Groups in AWS

In addition to the firewall and other security mechanisms local to the instance, AWS has an additional layer of granular network security mechanism called Security Groups. A Security Group in AWS acts as a virtual firewall for the instance to control inbound and outbound traffic. AWS enforces a DENY ALL policy for inbound traffic and ALLOW ALL policy for outbound traffic, in the absence of explicit rules in security groups. So, the TCP SYN packets (knocks) for the sequence ports never reached the EC2 instance. Thus, in addition to the Linux instance’s firewall which we controlled using iptables, we also need to find a way to control the security groups.

Basic overview of Security Groups in the cloud

Because of this virtual firewall by AWS, the instance’s local firewall (iptables in this case) becomes insignificant. While, yes, there are reasons to use both security groups and internal firewall to achieve certain security goals, for simplicity we will completely ignore the internal firewall and leave at its default configuration. Now, instead of controlling the internal firewall using iptables to achieve port knocking we can control the AWS virtual firewall (Security Groups) to open and close ports.

Implementation of Port Knocking with TOTP on AWS

The steps remain the same as traditional port knocking (as explained in detail in the earlier section):

  • Calculate OTP at every instance in time.
  • Change knockd.config file to reflect the new sequence ports.
  • Restart knockd service.

But, there are some differences and one additional step required for implementing port knocking on the cloud:

For Sequence Ports:

Normally, all packets whether they are sent to an open port or closed port reach the Linux kernel’s TCP stack and it is the responsibility of the kernel to accept, reject or drop the packets depending on open and closed ports, firewall configurations, etc. Therefore, knockd has visibility into packets arriving, which are sent to even the closed ports but rejected/dropped by the kernel. This allows knocking to work.

The major difference in implementation, between traditional port knocking and port knocking on the cloud, is that by default the virtual AWS firewall does not forward the packets to the EC2 instance, which are sent to the closed ports. Thus, the packets to closed ports do not reach the Linux kernel and knockd loses visibility into the packets sent to the closed ports, which is required for it to implement port knocking. We solve this problem by adding one more step to traditional port knocking:

Using the aws cli, every 30 seconds when new knocks are generated:

  • Modify the security groups to close the previous sequence ports.
  • Modify the security groups to open the new sequence ports.

Effectively, the sequence ports are open on the AWS virtual firewall but closed on Linux. Thus, they still show as closed ports but packets sent to these ports reach the Linux kernel and knockd can read it.

For Knocked Port:

As explained in the earlier section the knockd.conf has two parameters:

start_command : Open the knocked port
stop_command : Close the knocked port

We use the aws cli to modify the security group instead of the local firewall using iptables.

start_command : aws ec2 authorize-security-group-ingress --group-id {sg_id} --protocol tcp --port {port} --cidr "0.0.0.0/0"stop_command  : aws ec2 revoke-security-group-ingress --group-id {sg_id} --protocol tcp --port {port} --cidr "0.0.0.0/0"

After receiving a successful knock, the knockd (daemon) will run the start_command which modifies the security group to open the knocked port and after waiting for the timeout specified in the knocker.conf, it will run the stop_command to again modify the security group to close the knocked port. This concept can be further extended to any of the cloud providers like Google Cloud Platform using firewall rules, Microsoft Azure using security rules, etc.

Footnote

Foremost, we would like to thank you for reading, hope you enjoyed the article and found it useful. This is our very first story, which we had also presented as a talk at a meetup by Null Mumbai. Your claps and comments will highly encourage us to generate more content like this. Find our other projects here.

We are just two friends (Swapnil Kumbhar and Akshay Shah) writing under the pseudonym Livingstone. Please follow us to not miss out on our content! Thank you and have a great day! 😄

Two friends, writing about technology. (Swapnil Kumbhar and Akshay Shah)