Чтобы защитить информационную инфраструктуру от атак злоумышленников, доступ к ней из внешнего мира принято ограничивать, но не отключать полностью по двум причинам:
- в момент аварии необходимые для её устранения специалисты могут оказаться вне офиса,
- удалённая работа получила полноправное признание наравне с офисным отсиживанием присутственных жопочасов.
Но если инфраструктура недоступна напрямую, то мишенью для атак становятся точки подключения к ней. Например, основным инструментом удалённого доступа для 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"
Предлагайте свои варианты в комментариях.