Port-knocking средствами iptables

19 апреля 2024

Чтобы защитить информационную инфраструктуру от атак злоумышленников, доступ к ней из внешнего мира принято ограничивать, но не отключать полностью по двум причинам:

  • в момент аварии необходимые для её устранения специалисты могут оказаться вне офиса,
  • удалённая работа получила полноправное признание наравне с офисным отсиживанием присутственных жопочасов.

Но если инфраструктура недоступна напрямую, то мишенью для атак становятся точки подключения к ней. Например, основным инструментом удалённого доступа для Linux-администраторов является SSH (но большинство дальнейших приёмов применимо также к OpenVPN, RDP и т.д.).

Какие способы используются на SSH-сервере, чтобы администратор мог к нему подключиться, а посторонний не мог установить даже TCP-соединение, не говоря о попытке авторизоваться?

Перемещение со стандартного TCP-порта 22 на нестандартный (например, 2345):

  • спасает от быстрого автоматического сканирования (например, через "nmap -A ..."), но с течением времени всё хуже и хуже, т.к. производительность сканеров и пропускная способность используемых ими интернет-каналов непрерывно растёт,
  • какой бы редкий номер порта не был выбран, рано или поздно в /var/log/auth.log появится поток сообщений sshd про неудачные попытки входа пользователей root, admin, john, lp, nobody, ubuntu и далее по словарю.

Динамическая блокировка внешних IP-адресов через fail2ban:

  • пресекает продолжение неудачной атаки, но не предотвращает первых попыток, то есть бесполезна против таких уязвимостей, как недавняя закладка в xz,
  • хоть и незначительно, но чревата ложными срабатываниями,
  • дополнительный сервис потребляет ОЗУ и процессор,
  • cопровождение файрволла усложняется — например, при большом количестве адресов потребуется оптимизация (отдельная цепочка вместо INPUT, либо отдельное правило + ipset), при перезагрузке состояние теряется, и т.д.

Белые списки IP-адресов:

  • не могут быть панацеей, т.к. в современных сетях, особенно сотовых, IP-адрес клиента непрерывно меняется и зачастую неизвестен даже ему самому.

Блокировка по умолчанию + динамические краткосрочные разрешения для отдельных IP, выдаваемые через port knocking или Captive-портал:

  • добавляет в защищаемую систему ещё один сервис-поедатель ресурсов,
  • для прослушивания трафика и управления файрволлом требует для сервиса повышенных привилегий,
  • в случае knockd — требует специальную утилиту на клиентской стороне (привет владельцам айфонов),
  • в случае Captive portal — создаёт ещё одну мишень для атаки,
  • как и в случае с fail2ban — настройки для изменения правил файрволла могут потребовать допиливания.

Можно ли реализовать весь требуемый функционал, то есть приём последовательности knock-пакетов и создание краткосрочного разрешения на доступ, целиком внутри iptables?

Ответ: разумеется, можно.

Порядок настройки описан во многих источниках — например, в https://wiki.archlinux.org/title/Port_knocking. Однако всем встреченным описаниям, на наш взгляд, не хватает для понятности (а) вертикального выравнивания и (б) пояснения нескольких не вполне очевидных моментов. Поэтому мы решили составить такое описание, которое было бы данных минусов лишено.

Рассмотрим работающий пример:


iptables -A INPUT -p tcp                                                      -m tcp --dport 11111 -m recent --set --name knock1 -j DROP
iptables -A INPUT -p tcp -m recent --rcheck --seconds 10 --reap --name knock1 -m tcp --dport 22222 -m recent --set --name knock2 -j DROP
iptables -A INPUT -p tcp -m recent --rcheck --seconds 10 --reap --name knock2 -m tcp --dport 33333 -m recent --set --name knock3 -j DROP
iptables -A INPUT -p tcp -m recent --rcheck --seconds 10 --reap --name knock3 -m tcp --dport 44444 -m recent --set --name ACTIVE -j DROP
iptables -A INPUT -p tcp -m recent --rcheck --seconds 30 --reap --name ACTIVE -m tcp --dport 22                                  -j ACCEPT
iptables -A INPUT        -m state  --state RELATED,ESTABLISHED                                                                   -j ACCEPT

Что в нём необычного?

  • в части правил модуль "recent" используется дважды (да, так можно) с разными наборами параметров — сначала с rcheck/seconds/reap, затем с set,
  • в зависимости от параметров, вызов recent может быть привычной проверкой, а может быть действием,
  • то есть одним из действий правила становится не только "-j DROP", но и вызов "recent --set ..."

Что делает модуль recent?

  • с параметрами "--set --name ИмяСписка" — работает как действие, а именно добавляет IP-адрес отправителя пакета в список с указанным именем (если списка нет, он будет автоматически создан),
  • с параметрами "--rcheck --seconds .. --reap --name ИмяСписка" — работает как проверка, а именно проверяет в указанном списке наличие и возраст IP-адреса отправителя, при отсутствующем или устаревшем адресе прекращает выполнение правила.

Таким образом:

  • после прихода TCP-пакета на порт 11111 модуль recent помещает IP-адрес отправителя в список knock1,
  • если пришёл TCP-пакет на порт 22222, IP-адрес отправителя есть в списке knock1, и оказался в нём менее 10 секунд назад — IP-адрес отправителя добавляется в список knock2,
  • аналогично работают правила для портов 33333 и 44444,
  • в течение 30 секунд после прихода пакета на порт 44444 (т.е. попадания в список ACTIVE) разрешён приём пакетов с того же IP-адреса на порт 22,
  • для установившегося TCP-соединения разрешение создаётся модулем state.

То есть от клиента требуется:

  • с интервалами не более 10 секунд последовательно отправить на сервер TCP-пакеты на порты 11111, 22222, 33333, 44444,
  • после чего в течение 30 секунд подключиться к SSH.

Чем отправлять knock-пакеты?

Годится любой консольный клиент для любого основанного на TCP протокола, поддерживающий (а) завершение по таймауту и (б) подключение к нестандартному порту.

Например, netcat:

for p in 11111 22222 33333 44444 ; do netcat -w1 "$HOST" "$p" ; done
ssh "$HOST"

Или curl (в отличие от netcat, поддерживает таймауты меньше секунды):

for p in 11111 22222 33333 44444 ; do curl -s -m0.2 "$HOST:$p" ; done
ssh "$HOST"

Слишком короткий таймаут лучше не указывать, чтобы recent в iptables успевал срабатывать для предыдущего пакета до прихода следующего.

Какую последовательность портов имеет смысл выбрать для knock?

  • не слишком короткую — чем короче, тем хуже защищённость,
  • не слишком длинную — чем длиннее, тем выше нагрузка и сложнее подключение,
  • с большим разбросом значений, чтобы сканер не успевал «прозвонить» их все в течение 10 секунд, указанных в качестве таймаута для recent,
  • без строгого возрастания или убывания, чтобы полное сканирование не затронуло все knock-порты по очереди,
  • без «красивых» номеров — 11111..44444 мы использовали в примерах для удобства восприятия, а на практике лучше сгенерировать их датчиком случайных чисел, например:
    perl -e 'printf "%d\n", rand() * 55000 + 10000 foreach 1..5'

Не представляет труда при желании заменить протокол для knock-пакетов с TCP на UDP:

  • на клиентской стороне можно продолжать использовать netcat, но с ключом "-u" и обязательной отправкой каких-то данных:
    echo | netcat -u -w1 $HOST $PORT
  • curl не годится, т.к. никаких прикладных протоколов на базе UDP не поддерживает,
  • но на замену ему появляются самые причудливые альтернативы вроде клиентов DNS, например:
    dig -p11111 @ssh.example.org x +timeout=1 +tries=1
    nslookup -retry=1 -timeout=1 -port=22222 x $HOST

Играет ли роль порядок правил в файрволле?

  • в приведённом примере он не важен, т.к. для каждого пакета knock-серии и ssh-сессии срабатывает ровно одно правило из списка,
  • мы выбрали такой порядок, в котором позиция правила совпадает с позицией обрабатываемого им пакета внутри knock-серии,
  • такой порядок способствует наглядности, но незначительно ухудшает производительность,
  • на практике правило "state RELATED,ESTABLISHED" принято располагать первым.

Как можно усилить защиту, опираясь только на возможности iptables?

  • в iptables на сервере — в дополнение к recent использовать модуль string
    iptables ... -m string --string VerySecret123
  • на клиенте, при использовании UDP — отправлять строку через netcat или в теле DNS-запроса:
    echo VerySecretString123 | netcat -w1 -u $HOST 22222
    nslookup -retry=1 -timeout=1 -port=22222 VerySecretString123 $HOST
  • на клиенте, при использовании TCP — использовать для отправки пакетов утилиту nping из состава nmap:
    sudo nping -c1 --tcp -p "$PORT" --data-string VerySecret123 "$HOST"

Предлагайте свои варианты в комментариях.



← Назад в Блог

Подпишитесь на новые статьи: