Credential Port Knocking, Part 1: Implementation in the Linux Kernel Firewall

Port knocking can be used to hide services that are particularly interesting for attackers. This first part of a multi-part tutorial shows which variants exist and how easy it is to implement credential port knocking using nftables or iptables directly in the Linux kernel firewall.

· 18 Minuten zu lesen
Credential Port Knocking, Part 1: Implementation in the Linux Kernel Firewall
🇩🇪
Dieser Artikel ist auch auf Deutsch verfügbar.
ℹ️
This article is part of a multi-part tutorial:
Part 1: Implementation in the Linux Kernel Firewall
Part 2: Integration in SSH Clients
Part 3: Not published yet.

There are many reasons to hide network services from third parties or to protect them from direct access, especially administrative or otherwise highly privileged services such as SSH.
Motives can be, for example, to put a stop to Internet mapping services such as SHODAN, or simply to prevent alarms caused by invalid login attempts, e.g. due to brute force attacks. But perhaps the security of the service alone is not (or no longer) trusted, in the case of OpenSSH, despite the generally high code quality, due to a serious security vulnerability such as the almost 20-year-old zombie vulnerability CVE-2024-6387 aka regreSSHion (re)discovered by the Qualys TRU team in June 2024, or the vulnerability discovered in March 2024 by Andres Freund during microbenchmarking on PostgreSQL, which was discovered quite by chance and just in time, sophisticated supply chain attack to implement a backdoor in OpenSSH via systemd, hidden in liblzma – events that have clearly pushed the boundaries of what should be considered a healthy level of paranoia for many people.

Port Knocking – it doesn't always have to be a VPN

To restrict direct access to administrative services on the hosts they are responsible for, many admins resort to VPNs, bastion hosts or proxy services – but even these are often comparatively complex, whether in terms of setup and operation or the underlying protocol, which in turn has to be exposed to the Internet with corresponding risks. Often it is only worthwhile operating them once a certain number of managed hosts has been reached.

If the services to be hidden are already strongly authenticated and encrypted, as is the case with SSH, port knocking may be a lightweight and reliable alternative, i.e. temporarily enabling access to administrative services for the IP address of an authorised client in the firewall.

Specialised services that manipulate the host's firewall at runtime are often used for this. However, depending on the method, this is also possible without any services at all by implementing complete port knocking directly in the system's kernel firewall.

A Variety of Port Knocking Methods

A distinction is generally made between the following port knocking methods:

  1. Classic port knocking: The system opens the service when certain ports are addressed in a specific sequence.
    Pro: Can be implemented both as a service and directly via a static kernel firewall; can often also be implemented on the client side using the operating system's on-board resources.
    Cons: Depending on the length of the authentication sequence, knocking is either slow or rather simple, might even be triggered by a port scan, knocking sequence is very likely to be completely recorded in connection data logs (e.g. by firewalls or routers), static secret.
  2. Credential port knocking: The system opens the service when a specific secret is received.
    Pros: Can be used virtually without delay, can be implemented both as a service and directly via a static kernel firewall, may also be implemented on the client side using the operating system's on-board resources, secret can often be selected in any complexity depending on the method, the credential may be hidden discreetly in other protocols.
    Con: Static secret.
  3. Challenge-response port knocking: The system opens the service when the client solves a task that can only be answered correctly by knowing a secret.
    Pros: Better security, as the secret does not leave the client.
    Cons: Higher complexity, requires bidirectional communication, can only be sensibly implemented as a service, requires special software on the client.
  4. OTP port knocking: The system opens the service when the client delivers a release request in the form of codes that can only be used once or a valid time- or event-based secret derivation.
    Pros: Better security as the secret can only be used once or does not leave the client.
    Cons: Higher complexity, can only be implemented as a service without a special kernel module, depends on correct system time on both server and client, may require special software on the client.

Method of Choice: Credential Port Knocking

In the first part of this tutorial, we will look at a simple variant of credential port knocking, as it compensates for all the cons of classic port knocking with the exception of the static secret. The credential is simply transmitted to the server as a UDP datagram.
The implementation of variants that hide the credential in network packets of other protocols will follow in a future part.

Since our aim is to hide services and not to add an additional layer of complex authentication to services with already sophisticated authentication (such as SSH), we are content with a static secret. If necessary, several static secrets can be set up for different users of the system to enable the service.

Unlike challenge-response and OTP port knocking, credential port knocking can be implemented completely in a static kernel firewall and can therefore be operated independently of the status of a locally running service, that would also have to fiddle around dynamically with our firewall rule set, which is not an option for many system administrators.

For the client-side implementation, there is usually no need for special software.

The Scenario

The following is an implementation of credential port knocking that can be realised under Linux in a static kernel firewall with the current nftables or its predecessor iptables.

As a credential, the following character string consisting of 32 ASCII characters from the character set [A-Za-z0-9] is used throughout:

$ pwgen -s 32 1
lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9

Credential for the port knocking examples

The length of the credential string is intentionally chosen to be comparatively long in order to demonstrate the size-related limitations of matching in the firewall later on, and how to deal with them.

In the example, we hide the SSH service on port 22/tcp of the server server.schoen-technisch.de, both for IPv4 and IPv6. After a successful knocking, the calling IP address shall be authorised for connections to the service for 10 seconds.

OpenSSH on Linux is used as the SSH client in the examples in this part of the tutorial.

In part two, we will work out how port knocking is transparently integrated into the login process of OpenSSH and PuTTY on Linux (and other Unix and Unix-like OS) as well as on Windows.

The Implementation

The sending of a credential as payload of a datagram to a specific, self-selected UDP port is the simplest and at the same time most flexible variant.

The key advantages of this variant are that one is free both in the length and thus complexity of the credential, and in the selection of the destination port, and UDP can be forwarded without any problems via port forwarding. This variant is therefore particularly suitable for situations in which NAT is used to make various target hosts accessible behind a shared public IP address. Thus, it is either possible to implement port knocking centrally on the NAT router/firewall or on the target hosts themselves by forwarding a separate UDP port for port knocking to each of these hosts.

We choose port 34567/udp for port knocking.

Server-side Implementation

Starting from a restrictive stateful firewall that does not allow access to the service to hide, firewall-based credential port knocking consists of three components per address family (IPv4 and IPv6):

  1. A list for entries of IP addresses that remain valid for a certain time.
  2. A firewall rule that matches the credential and then adds the IP address of the source of the datagram to the list mentioned above.
  3. A firewall rule that allows access to the service hidden by port knocking for the source IP addresses contained in the above list.

Lists for storing authenticated addresses

nftables

Since nftables does not specify the structure and names of the firewall tables and chains and can even process both IPv4 and IPv6 in a common filter chain when using a table of type inet, we create a list with its specific name in the context of the corresponding table for each of the address families. In the case of a common table for both address families (type inet), both of these lists are therefore placed in the same table, otherwise (type ip + type ip6) in the context of the respective address family table:

  set knocked_v4 {
    type ipv4_addr
    flags timeout
  }

  set knocked_v6 {
    type ipv6_addr
    flags timeout
  }

nftables: Definition of lists for successfully knocking IPv4 and IPv6 addresses.

iptables

In iptables, on the other hand, there are specifications for the names of both the tables and the standard chains, and the whole structure is strictly separated according to address family; iptables for processing IPv4 and ip6tables for processing IPv6 packets.

The IP address lists for authenticated clients are managed in iptables by the recent extension. This is done implicitly, i.e. unlike with nftables, the lists do not have to be defined explicitly.

Analysing knocking packets and filling lists with authenticated IP addresses

nftables

In nftables, we use raw payload matching with the syntax @<reference>,<offset in bit>,<length in bit> to analyse the packet content. Unfortunately, raw payload matching is only able to match 128 bits at once – a character string with 32 ASCII characters, however, is 32 Byte × 8 bits/Byte = 256 bit long. In addition, the character string must be in decimal or hexadecimal notation prefixed with 0x. We therefore generate two 128-bit hexadecimal character strings prefixed with 0x from the 256-bit ASCII character string:

$ echo -n "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" | hexdump -ve '/1 "%02x"' \
  | sed -e 's/\(.\{32\}\)/0x\1 /g'; echo
0x6c6475695379765377595a7a7770394f 0x6650694d745652596a396e3652376539 

Conversion of the 256 bit ASCII credential into two 128 bit hexadecimal strings.

As reference for the match, the beginning of the payload of the UDP datagram is suitable, which is addressed by @ih (inner header), so that the credential would be located at position 0, thus summarised @ih,0,128 for the match over the first 128 bits of the credential, and @ih,128,128 for the second 128 bits.

Unfortunately, the implementation of @ih is still relatively new and is only included in Linux kernels starting with version 5.16 and nftables starting with version 1.0.1. Some distributions such as RedHat Enterprise Linux 9 and compatible EL9 distros have backported this feature into their older kernels and provide a suitable version of nftables. However, with many older LTS distributions you are left in the lurch.

In order to maintain compatibility with older Linux distributions, we therefore use the reference @th (transport header), which refers to the beginning of the transport protocol used, in this case UDP, which has a fixed header length of 8 Byte × 8 bit/Byte = 64 bit. The match thus changes to @th,64,128 for the first 128 bits of the credential, and @th,192,128 for the remaining 128 bits of the credential.

Before we look at the packet content using raw payload matching, we first match the protocol and the destination port as well as the size of the UDP datagram; we only want to examine the payload of the packet if it is a UDP datagram that is directed to the correct port 34567/udp (udp dport 34567) and is exactly 8 Byte of UDP header + 32 Byte of payload (credential) = 40 Byte long (udp length 40).

If all of this applies and the credential has been matched, the source IP address of the packet plus the desired opening time for port knocking is added to the list matching the address family (add @knocked_v4 { ip saddr timeout 10s } for IPv4 or add @knocked_v6 { ip6 saddr timeout 10s } for IPv6), and we also want a packet and byte counter for successful matches (counter) and want to note the successful knocking with a suitable prefix in the system logs (log prefix "Knocked: ").

# IPv4: Match UDP packets to destination port 34567 with a length of 40 bytes and
# the payload "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" and add the source address as
# well as a timeout of 10 seconds to the set knocked_v4, count the packet and log
# it with prefix "Knocked: ":
udp dport 34567 udp length 40 @th,64,128 0x6c6475695379765377595a7a7770394f \
  @th,192,128 0x6650694d745652596a396e3652376539 \
  add @knocked_v4 { ip  saddr timeout 10s } counter log prefix "Knocked: "

# IPv6: Match UDP packets to destination port 34567 with a length of 40 bytes and
# the payload "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" and add the source address as
# well as a timeout of 10 seconds to the set knocked_v6, count the packet and log
# it with prefix "Knocked: ":
udp dport 34567 udp length 40 @th,64,128 0x6c6475695379765377595a7a7770394f \
  @th,192,128 0x6650694d745652596a396e3652376539 \
  add @knocked_v6 { ip6 saddr timeout 10s } counter log prefix "Knocked: "

nftables: IPv4 and IPv6 rules for credential matching with filling of the IP lists.

It is important that these rules are positioned in the chain for incoming packets before any other rules with a final verdict (accept, reject or drop) that may apply to the knocking packets. This also applies to jumps to other chains either without return or with rules with final verdict that may apply to them.

iptables

The credential matching rules for iptables can be constructed following the same scheme, but there are a few differences:

  • The length extension of iptables cannot match the length of the UDP part in the IP payload, but instead always refers to the entire size of the IP packet. Therefore, with iptables 20 bytes of IPv4 or 40 bytes of IPv6 headers must be added to the 40 bytes calculated in the nftables example above.
  • With the string extension, iptables has the option of searching packets for character strings using various algorithms.
  • As the rule sets for IPv4 and IPv6 are strictly separated in iptables, the IP address list can have the same name in both rule chains. We use knocked.
  • For the recent extension, the period of validity is not defined during credential matching, but instead when the connection is authorised in the next section.
  • iptables rules to be loaded into the kernel using the iptables-restore command can not strech over multiple lines by separating them with \ for a better overview.

So the rule adapted for iptables for the filter table of the IPv4 rule set looks like this:

Append a rule to the INPUT chain (-A INPUT) that applies to UDP datagrams (-p udp), which are directed to the destination port 34567 (--dport 34567) and are exactly 20 Byte IPv4 header + 8 Byte UDP header + 32 Byte payload (credential) = 60 Byte long (-m length --length 60) and have our credential as content (-m string --algo bm --string "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9"). If this is the case, store the source IP with the name knocked (-m recent --set --name knocked --rsource) and log the successful knocking with the prefix Knocked: in the system logs (-j LOG --log-prefix "Knocked: ").

# IPv4: Match UDP packets to destination port 34567 with a length of 60 bytes and
# the payload "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" and store the source address
# with the name knocked, then log it with prefix "Knocked: ":
-A INPUT -p udp --dport 34567 -m length --length 60 -m string --algo bm --string "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" -m recent --set --name knocked --rsource -j LOG --log-prefix "Knocked: "

iptables: IPv4 rule for credential matching with storing of the source IP address.

For the IPv6 rule set, the rule is identical, except that the packet length should be exactly 40 Byte IPv6 header + 8 Byte UDP header + 32 Byte payload (credential) = 80 Byte (-m length --length 80).

# IPv6: Match UDP packets to destination port 34567 with a length of 80 bytes and
# the payload "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" and store the source address
# with the name knocked, then log it with prefix "Knocked: ":
-A INPUT -p udp --dport 34567 -m length --length 80 -m string --algo bm --string "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" -m recent --set --name knocked --rsource -j LOG --log-prefix "Knocked: "

iptables: IPv6 rule for credential matching with storing of the source IP address.

The rules should be positioned in the same way as described for nftables above.

Allowing SSH connections from authenticated IP addresses

nftables

Since we now have set up the lists for successfully knocking IP addresses and the matches for credential port knocking for both address families, all that still remains are rules for the hidden SSH service (tcp dport 22) incoming new connections (ct state new) for IPv4 (ip saddr @knocked_v4) or IPv6 source addresses in the lists of successful port knocks (ip6 saddr @knocked_v6) to be counted (counter) and allowed (accept).

# IPv4: TCP packets to port 22 (SSH) for new connections with source IP address
# present in set knocked_v4 are counted and allowed:
tcp dport 22 ct state new ip  saddr @knocked_v4 counter accept

# IPv6: TCP packets to port 22 (SSH) for new connections with source IP address
# present in set knocked_v6 are counted and allowed:
tcp dport 22 ct state new ip6 saddr @knocked_v6 counter accept

nftables: Allowing new SSH connections from source IPs present in the IPv4 and IPv6 lists.

Again, these rules must be positioned in the chain for incoming packets in such a way that incoming new connections are not already rejected or authorised by a preceding rule.

iptables

The rule for allowing connections by successfully knocking clients is identical in iptables for the IPv4 and IPv6 rule sets and is constructed as follows:

Append a rule to the INPUT chain of the filter table (-A INPUT) that matches packets directed to TCP destination port 22 (-p tcp --dport 22) for new connections (-m state --state NEW), if the source IP address has been captured within the past 10 seconds under the name knocked (-m recent --rcheck --seconds 10 --name knocked --rsource), and then accept this packet (-j ACCEPT).

# TCP packets to port 22 (SSH) for new connections allowed, if the source IP
# address has been recorded within the past 10 seconds under the name knocked:
-A INPUT -p tcp --dport 22 -m state --state NEW -m recent --rcheck --seconds 10 --name knocked --rsource -j ACCEPT

iptables: Allowing new SSH connections from source IPs recorded under the name knocked.

As with the nftables rule, this rule must also be positioned in the rule set in such a way that incoming new connections have not already been rejected or authorised by a previous rule.

First-time operation with test of the call waiting function

To verify the function during commissioning, it is advisable to position a rule that generally allows SSH connections (e.g. tcp dport 22 ct state new counter accept for nftables, -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT for iptables) immediately after the port knocking allow rule constructed above, respectively to position the port knocking allow rules immediately before an existing rule that generally allows the SSH service.

After an SSH connection has been established via port knocking, nft list ruleset or iptables -L -vn and ip6tables -L -vn can be used to check whether the connection was accepted by the port knocking rule or the general rule. To achieve this, the packet counters are examined to determine which of the rules' packet counters increased when the connection was established. If it is certain that port knocking is working, the general rule can be removed from the rule set or commented out.

Complete example rule set

The following examples combine all the elements described above into functional restrictive rule sets with stateful firewalling and port knocking for SSH – a combined nftables rule set for IPv4 and IPv6, an iptables rule set for IPv4, and an iptables rule set for IPv6 (ip6tables).

The example rule sets are not intended to simply replace an existing firewall, but to illustrate which elements should be present and how they should be positioned in a rule set. Alternatively, the example can also be used as a basis for creating a new firewall rule set customised to the respective target system.

nftables
flush ruleset

table inet filter {

  set knocked_v4 {
    type ipv4_addr
    flags timeout
  }

  set knocked_v6 {
    type ipv6_addr
    flags timeout
  }

  chain input {
    type filter hook input priority filter; policy drop;
  
    # Stateful firewalling:
    ct state related,established counter accept
    ct state invalid counter drop
  
    # Allow IPv6 NDP:
    icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } \
      counter accept

    # Allow loopback traffic:
    iif lo counter accept

    # IPv4: Match UDP packets to destination port 34567 with a length of 40 bytes and
    # the payload "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" and add the source address as
    # well as a timeout of 10 seconds to the set knocked_v4, count the packet and log
    # it with prefix "Knocked: ":
    udp dport 34567 udp length 40 @th,64,128 0x6c6475695379765377595a7a7770394f \
      @th,192,128 0x6650694d745652596a396e3652376539 \
      add @knocked_v4 { ip  saddr timeout 10s } counter log prefix "Knocked: "

    # IPv6: Match UDP packets to destination port 34567 with a length of 40 bytes and
    # the payload "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" and add the source address as
    # well as a timeout of 10 seconds to the set knocked_v6, count the packet and log
    # it with prefix "Knocked: ":
    udp dport 34567 udp length 40 @th,64,128 0x6c6475695379765377595a7a7770394f \
      @th,192,128 0x6650694d745652596a396e3652376539 \
      add @knocked_v6 { ip6 saddr timeout 10s } counter log prefix "Knocked: "

    # IPv4: TCP packets to port 22 (SSH) for new connections with source IP address
    # present in set knocked_v4 are counted and allowed:
    tcp dport 22 ct state new ip  saddr @knocked_v4 counter accept

    # IPv6: TCP packets to port 22 (SSH) for new connections with source IP address
    # present in set knocked_v6 are counted and allowed:
    tcp dport 22 ct state new ip6 saddr @knocked_v6 counter accept

    # Test rule for first-time operation for validation and to prevent lockout:
    #tcp dport 22 ct state new counter accept

    # Place rules to allow other (e.g. generally accessible) services here.

    # Allow pings, but at limited rate:
    icmp   type echo-request limit rate 5/second counter accept
    icmpv6 type echo-request limit rate 5/second counter accept

    # Reject everything else (supplementing the drop policy of this chain):
    counter reject with icmpx type admin-prohibited
  }

}

nftables: Complete example rule set for filtering of both IPv4 and IPv6.

iptables for IPv4
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]

# Stateful Firewalling:
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -m state --state INVALID -j DROP

# Loopback-Traffic zulassen:
-A INPUT -i lo -j ACCEPT

# IPv4: Matche UDP-Pakete an Zielport 34567 mit der Paketgröße 60 Bytes und dem
# Payload "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" und merke die Quell-Adresse unter
# dem Namen "knocked", dann logge es mit dem Präfix "Knocked: ":
-A INPUT -p udp --dport 34567 -m length --length 60 -m string --algo bm --string "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" -m recent --set --name knocked --rsource -j LOG --log-prefix "Knocked: "

# TCP-Pakete an Port 22 (SSH) mit neuen Verbindungen erlauben, deren Quell-IP-
# Adresse vor weniger als 10 Sekunden unter dem Namen knocked gemerkt wurden:
-A INPUT -p tcp --dport 22 -m state --state NEW -m recent --rcheck --seconds 10 --name knocked --rsource -j ACCEPT

# Test-Regel zum Test bei Inbetriebnahme und gegen versehentliches Aussperren:
#-A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT

# Regeln zur Freigabe anderer (bspw. allgemeinzugänglicher) Dienste hier
# platzieren.

# Pings zulassen, aber bei eingeschränkter Rate:
-A INPUT -p icmp --icmp-type echo-request -m limit --limit 5/second -j ACCEPT 

# Alles andere ist untersagt.
-A INPUT -j REJECT --reject-with icmp-admin-prohibited

COMMIT

iptables: Complete example rule set for filtering of IPv4.

iptables for IPv6
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]

# Stateful Firewalling:
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -m state --state INVALID -j DROP

# IPv6 NDP zulassen:
-A INPUT -p icmpv6 --icmpv6-type neighbour-solicitation -j ACCEPT
-A INPUT -p icmpv6 --icmpv6-type router-advertisement -j ACCEPT
-A INPUT -p icmpv6 --icmpv6-type neighbor-advertisement -j ACCEPT

# Loopback-Traffic zulassen:
-A INPUT -i lo -j ACCEPT

# IPv4: Matche UDP-Pakete an Zielport 34567 mit der Paketgröße 60 Bytes und dem
# Payload "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" und merke die Quell-Adresse unter
# dem Namen "knocked", dann logge es mit dem Präfix "Knocked: ":
-A INPUT -p udp --dport 34567 -m length --length 60 -m string --algo bm --string "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" -m recent --set --name knocked --rsource -j LOG --log-prefix "Knocked: "

# TCP-Pakete an Port 22 (SSH) mit neuen Verbindungen erlauben, deren Quell-IP-
# Adresse vor weniger als 10 Sekunden unter dem Namen knocked gemerkt wurden:
-A INPUT -p tcp --dport 22 -m state --state NEW -m recent --rcheck --seconds 10 --name knocked --rsource -j ACCEPT

# Test-Regel zum Test bei Inbetriebnahme und gegen versehentliches Aussperren:
#-A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT

# Regeln zur Freigabe anderer (bspw. allgemeinzugänglicher) Dienste hier
# platzieren.

# Pings zulassen, aber bei eingeschränkter Rate:
-A INPUT -p icmpv6 --icmpv6-type echo-request -m limit --limit 5/second -j ACCEPT 

# Alles andere zurückweisen (ergänzend zur drop-Policy der Kette):
-A INPUT -j REJECT --reject-with icmp6-adm-prohibited

COMMIT

iptables: Complete example rule set for filtering of IPv6.

Client-side Implementation

A tool is required to send a UDP datagram with the credential as a payload to the server's call waiting port. In the simplest case, this is the GNU Bash, which should already be available on pretty much every Linux system.

A TCP proxy is also required at the latest when it comes to integration with PuTTY (more on this in the next part). OpenBSD netcat, Nmap ncat or socat, for example, can be used for this, all of which can also be used to send our credential as a UDP datagram so that the same tool can be used both for knocking and as a TCP proxy. Most Linux distributions offer ready-made packages for all three tools.
However, socat can only be recommended to a limited extent for three reasons; the command line syntax is significantly more complex than with the other tools, after sending a UDP datagram, despite SIGPIPE it first pauses for a while, which must be explicitly deactivated by setting the timeout to 0 seconds, and the specification of IPv6 addresses requires a syntax that differs from the syntax for specifying IPv4 addresses or hostnames.

The following four commands can be used equivalently for knocking on our server (the first command requires Bash as the shell):

$ echo -n "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" \
  > /dev/udp/server.schoen-technisch.de/34567

$ echo -n "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" \
  | netcat -u "server.schoen-technisch.de" "34567"

$ echo -n "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" \
  | ncat --send-only -u "server.schoen-technisch.de" "34567"

$ echo -n "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" \
  | socat -t 0 STDIO "UDP-SENDTO:server.schoen-technisch.de:34567"

Anklopfen am Server per UDP-Credential mittels Bash, netcat, ncat und socat.

Establishing SSH connections with port knocking

After knocking using any of the above methods, the server's kernel firewall should now have logged a packet prefixed with the string Knocked: in the system logs, and a connection via SSH should be possible for 10 seconds.

$ ssh user@server.schoen-technich.de
ssh: connect to host server.schoen-technich.de port 22: Network is unreachable

$ echo -n "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" > /dev/udp/server.schoen-technisch.de/34567
  
$ ssh user@server.schoen-technich.de
Last login: Thu May 30 11:52:51 2024 from 10.0.0.2
[user@server ~]$ 

Failed SSH connection before, and successfil SSH connection after manual knocking.

Prospect: Integration into SSH clients and protocols

In this first part, we have now worked out how little effort is required to implement credential port knocking to protect the SSH service directly and reliably via the Linux kernel firewall.

The second part deals with various types of integration into the SSH clients OpenSSH and PuTTY under both Linux (and other Unix(-like) OS) and Windows, so that port knocking is carried out automatically when the connection is established and the use of credential port knocking thus becomes transparent to the user.

The third part of the series will show how the transmission of the credential can be integrated into datagrams of protocols such as ICMP echo requests (ping) or SNMP queries in order to further conceal the port knocking or to allow it to pass through upstream firewalls more easily.


Next part: Credential Port Knocking, Part 2: Integration in SSH Clients

Port Knocking Integration in SSH Clients
Fully automatic port knocking for OpenSSH und PuTTY on Linux/Unix and Windows.