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 явно указать интерфейс - хуже не будет.

Чтобы такие фокусы заработали, нужно проделать еще несколько телодвижений:

Вот теперь красиво - и 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.ов:

Ну а что там можно навертеть со squid я пока не знаю (точнее не умею) - с пользовательским трафиком через IPS так просто не получается (icap, content adaptation... нужно писать adaptation сервис - простой стыковкой компонентов тут не обойтись).