Suricata и SSL Offloading/Termination на HAProxy
Иногда раньше, иногда позже, но все-таки наступает момент, когда штатная работа такой обыденной, и безусловно полезной и необходимой в домашней сети вещи как IPS, перестает устраивать даже самого непритязательного админа... :-)
Шутка конечно, порожденная профессиональной деформацией (по мнению моих коллег). И да - я в курсе отношения окружающих к подобным извращениям...
Но шутки в сторону. Эта полезная вещь, в условиях нынешнего интернета становится совершенно бесполезной. Успешно лопатит только HTTP, коего нынче в инете днем с огнем не найдешь - сплошь и рядом HTTPS. Да и прочий трафик норовит в SSL завернуться. А хочется по-взрослому - проверять все, что пропускается внутрь периметра домашней сети.
По-взрослому в enterprice решениях - это ssl-offloading в любом направлении. Для входящего трафика - decrypting с известным сертификатом, для исходящего - с подменой сертификата (например Cisco FTD так умеет).
Для домашней сети первая задача решается любым реверс-прокси (nginx, haproxy), вторая - squid (из известных мне). OpenSource решения, реализующие обе задачи сразу мне не известны.
Нет, ну можно конечно и дома развернуть Cisco vFTD, но это уже с моей точки зрения является извращением. Так что далее рассматриваем haproxy с suricata, а не FTD с отнюдь не любительским ценником. И да - я таки в курсе за Диффи-Хеллмана (DHE/ECDHE) и TLSv1.3. И да - корпорация добра и создатели TLSv1.3 (не особо вникал в вопрос - возможно это одни и те-же лица) ведут непримиримую борьбу с MitM, отмахнувшись от интересов производителей и пользователей корпоративных фаерволов. Но пока не победили окончательно - заметка остается актуальной...
Сходу (в лоб) задача решается легко - достаточно декриптовать трафик на прокси, и в таком виде отправить на сервер. И, соответственно, отловить IPS декриптованный трафик на выходе - после прокси. Но мы же не ищем простых решений, а извращаемся (по версии коллег). Значит нужно трафик декриптовать, проверить IPS и закриптовать взад. И желательно не покидая извращаемого хоста. Вроде просто:
#1 /etc/haproxy/haproxy.cfg
listen inbound
mode tcp
# принимаем входящие соединения с публичным (от letsencrypt например)
# сертификатом/ключем и декриптуем трафик
bind <external ip>:443 ssl crt /etc/haproxy/public.pem
# коннектимся к серверу по ssl
server ServerName <server ip>:443 ssl verify none crt
Все хорошо? Да если бы... HAProxy все эти процедуры проделывает в userspace (за пределы процесса декриптованный трафик не выходит), так что и на входе, и на выходе мы получим зашифрованный трафик. И iptables его не увидит, и в IPS его ни как не завернуть. Нужно декриптованный трафик прогнать через последовательность frontend - backend. Т.е. добавить еще один лиссенер для декриптованного трафика:
#2 /etc/haproxy/haproxy.cfg
listen inbound
mode tcp
bind <external ip>:443 ssl crt /etc/haproxy/public.pem
# отправляем декриптованный трафик на промежуточный фронтенд;
# номер порта 443, но ssl-ем там и не пахнет (можно любой порт выбрать, но лучше оставить исходный,
# иначе трудно будет понять - что же мы там (на IPS) на самом деле заблокировали):
server intermediary 127.0.0.1:443
# собственно, вот на этом этапе можно (и нужно) отловить трафик в iptables и завернуть в IPS
listen intermediary
mode tcp
# принимаем декриптованный трафик
bind 127.0.0.1:443
server ServerName <server ip>:443 ssl verify none
И все бы хорошо, только адреса источника и назначения с т.з. IPS теперь 127.0.0.1, а адрес клиента для сервера - адрес исходящего (в сторону сервера) интерфейса хоста. Криминальный трафик IPS может и заблокирует, но вот от кого именно мы уже не узнаем. Не по-взрослому, хоть и не смертельно... :-(
Можно оставить так, а можно подшаманить - haproxy умеет в transparent proxy & binding (через TPROXY фичу ядра). Конечно и haproxy должен быть скомпилирован с соответствующей опцией, и ядро, но в штатной поставке (искаропки) это все по умолчанию включено (в Centos 7 по крайней мере):
#3 /etc/haproxy/haproxy.cfg
listen inbound
mode tcp
bind <external ip>:443 ssl crt /etc/haproxy/public.pem
# и адрес у него внешний:
server intermediary <external ip>:443
# но коннектимся мы к нему через интерфейс lo (обязательно, иначе haproxy неявно выберет внешний интерфейс),
# да еще и с исходного адреса клиента:
source 0.0.0.0 usesrc clientip interface lo
listen intermediary
mode tcp
# принимаем декриптованный трафик... на внешний адрес хоста, но на интерфейсе lo:
bind <external ip>:443 transparent interface lo
# коннектимся к серверу по ssl
server ServerName <server ip>:443 ssl verify none
# но теперь - с исходного адреса клиента (с интерфеса в сторону сервера (это неявное поведение haproxy)):
source 0.0.0.0 usesrc clientip
Собственно, основная фишка здесь - явно указать интерфейсы приема/отправки трафика на лиссенерах. Без этого haproxy выбирает их неявно (по своему разумению) и не всегда корректно. Например в лиссенере "inbound" (в примере 2), при включении диррективы "usesrc clientip", неявно выбранным интерфейсом для отправки пакетов будет lo, а для возвращаемого трафика будет выбран внешний интерфейс (тот, на котором bind с внешним адресом висит). И фиг знает почему так - и там и там открыт сокет с одинаковым (изначальным, внешним) адресом источника (на внешнем - изначальное соединение от клиента, а на lo мы его распорядились создать соответствующей директивой), и таблицы rule/route настроены (см. ниже). Поэтому, в принципе, можно на всех директивах bind/source явно указать интерфейс - хуже не будет.
Чтобы такие фокусы заработали, нужно проделать еще несколько телодвижений:
-
установить следующие sysctl-параметры ядра:
sysctl -w net.ipv4.ip_forward=1 sysctl -w net.ipv4.ip_nonlocal_bind=1
с первым понятно, а второй как раз и позволяет слать пакеты с исходных адресов клиентов (хоть их на данном хосте отродясь не бывало), и слушать внешний адрес на lo интерфейсе (так же оному не принадлежащий).
-
маркируем пакеты, адреса назначения которых соотвествует одному из локальных сокетов. Мы отправили на сервер/backend пакет с адресом источника, соответствующим адресу клиента, т.е. открыли сокет. Вот и обратный пакет (в котором уже адрес назначения соответствует адресу клиента) принять надо-бы на этот же сокет:
iptables -t mangle -N DIVERT iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT iptables -t mangle -A DIVERT -j MARK --set-mark 1 iptables -t mangle -A DIVERT -j ACCEPT
-
и указывем, что пакеты с этим маркером доставляются локально (в пределах хоста на вышеупомянутый сокет), а не отправляются на исходный ip адрес клиента - наружу, где им и положено быть исходя из глобальной таблицы маршрутизации:
ip rule add fwmark 1 lookup 100 ip route add local 0.0.0.0/0 dev lo table 100
Вот теперь красиво - и IPS увидит настоящий адрес клиента как источник, а наш внешний адрес как адрес назначения, и сервер увидит настоящий адрес клиента как источник. Да и в suricata заворачивать проще - не надо грести весь трафик из lo интерфейса:
iptables -t filter -A INPUT -i lo -d <external ip> -j NFQUEUE --queue-num 0 --queue-bypass
iptables -t filter -A OUTPUT -o lo -s <external ip> -j NFQUEUE --queue-num 0 --queue-bypass
Парочка P.S.ов:
- в случае цепочки доверия, в используемом на внешнем лиссенере haproxy сертификате (например как в Let's Encrypt), нужно указывать не "голую" пару сертификат/ключ, а полную цепочку с ключем (в формате цепочки от конечного сертификата к корневому с ключем в конце). В случае Let's Encrypt - fullchain.pem + private.key. Иначе схлопочем сообщения о недоверенном сертификате на клиентской стороне. В общем - требования те же, что и при настройке HTTPS на web-сервере.
- в примере выше, для "приземления" декриптованного трафика используется тот-же порт, что и для криптованного (443). Для наглядности - более чем удобно (сразу видно, какой исходный трафик попал в IPS после декриптования), но самой IPS далеко не пофигу - можно запросто схлопотать сообщения типа "HTTP on unusual port" или "non-TLS on TLS port". Смысл понятен, последствия (при соответствующих настройках IPS) - тоже. Так что пример все-таки больше академический - https для декриптования таки лучше приземлять на http лиссенер с соответствующим номером порта. Это тем более актуально учитывая наличие информации об исходном протоколе в заголовках http (tcp лиссенер их закономерно проигнорирует) - IPS может (и должна) счеть такое поведение нелигитимным.
- ну и не стоит забывать о защите бэкенда как-раз против MitM - в HTTP протокол включены различные расширения, способные изрядно осложнить жизнь ssl-прокси (и его хозяину) - как то: HSTS, HPKP. Поэтому нужно быть особенно внимательным с конфигурацией, в которой за прокси стоит именно HTTP сервер (особенно администрируемый не Вами). SSL-прокси (в виде, описанном выше) терминирует соединение на транспортном уровне, и если на HTTP-сервер установлен сертификат, отличный от установленного на прокси, упомянутые расширения доставят немало впечатлений. Так что процедура установки/обновления сертификатов на WEB-сервере и ssl-прокси должна быть синхронизирована (например, в случае Let's Encrypt - через renewal-hooks). Или, как альтернатива, настраивать haproxy лиссенеры в режиме http и рулить заголовками на нем (но тогда проще целиком перенести функционал SSL на haproxy и гнать на web-сервер голый http).
- учитывая предыдущие p.s. - почему тогда я рассматривал именно mode tcp? А потому, что это более общий случай. Помимо упомянутого https существуют еще и smtps и imaps и <требуемое впишите сами>. И весь этот трафик тоже стоит прогонять через IPS.
Ну а что там можно навертеть со squid я пока не знаю (точнее не умею) - с пользовательским трафиком через IPS так просто не получается (icap, content adaptation... нужно писать adaptation сервис - простой стыковкой компонентов тут не обойтись).