Мой идеальный пакетный фильтр для FreeBSD (pf.conf)
Здравствуйте, я Munou.
В этой статье я хотел бы представить пакетный фильтр, с которым я недавно возился.
Текущие настройки
Они выглядят следующим образом.
В случае с Packet Filter:
правила преобразования, такие как NAT и rdr, применяются по принципу первого совпадения;
правила фильтрации, такие как pass и block, применяются по принципу последнего совпадения.
Такова спецификация.
В данном случае для удобства чтения я настроил все правила фильтрации с параметром quick, чтобы применялось первое совпавшее правило.
Однако правила block для fail2ban, которые я хочу логировать повсеместно, я помещаю в конец, чтобы обеспечить полное логирование.
Я оставлю комментарии там, где это уместно.
set skip on lo
set block-policy drop
set optimization conservative
set state-policy if-bound
set ruleset-optimization basic
wanint="vtnet0"
# Конфигурация WireGuard
wg_ifs = "{ wg0, wg1 }"
wg_net = "10.1.0.0/24"
wg_ports="{51820}"
table <wg_clients> const { 10.1.0.2, 10.1.0.4, 10.1.0.22 }
#### -- Правила Scrub -- ####
scrub in on $wanint all random-id max-mss 1360
scrub out on $wanint all random-id max-mss 1360
scrub in all
scrub out all
#### -- Правила NAT -- ####
# NAT для клиентов WireGuard
nat on $wanint inet from <wg_clients> to any -> ($wanint)
block all
#### -- Правила Fail2Ban -- ####
anchor "f2b/*"
pass out quick keep state
#### -- Правила UDP -- ####
pass in quick on $wg_ifs from $wg_net to any keep state
pass in quick on $wanint proto udp from any to ($wanint) port $wg_ports keep state
# Протокол HTTP/3
pass in quick on $wanint proto udp to ($wanint) port 443 keep state
#### -- Правила TCP -- ####
# Почтовые протоколы
pass in quick proto tcp from any to ($wanint) port {25, 465, 993, 995} synproxy state
# HTTP
pass in quick on $wanint proto tcp to ($wanint) port {80, 443} modulate state
set block-policy drop
Все пакеты, не соответствующие ни одному правилу, будут отброшены (drop).
В случае return для TCP возвращается RST, уведомляя отправителя и закрывая соединение.
Для UDP, поскольку TCP-соединения не существует, просто возвращается ICMP port unreachable.
set optimization conservative
Это правило оптимизации, которое в документации описывается как консервативная настройка.
В случае aggressive соединения закрываются по истечении времени в приоритетном порядке, но в таких средах, как WireGuard, где требуется поддерживать постоянное соединение, оно может быть разорвано просто из-за отсутствия новых пакетов.
Также существует вариант satellite, что звучит довольно романтично.
high-latency A high-latency environment (such as a satellite connec- tion). satellite Alias for high-latency.
set state-policy if-bound
Состояние (state) сохраняется и запоминается для пропуска пакетов, что предотвращает несоответствия.
Результат pfctl -ss — это и есть state. Рассмотрим следующую конфигурацию:
Клиент ←→ wg ←→ Сервер
Поскольку информация о том, «на каком интерфейсе было создано состояние», привязывается к state, разрешается только трафик через тот же интерфейс, что предотвращает несоответствия.
В обычном режиме floating:
Вход: wg0
Выход: vtnet0
Возврат: OK как через vtnet0, так и через wg0
Однако в случае state-policy if-bound:
Вход: wg0
Выход: vtnet0
Возврат: обязательно через wg0, иначе ошибка (NG)
set ruleset-optimization basic
Оптимизирует набор правил должным образом.
Из официальной документации:
remove duplicate rules
remove rules that are a subset of another rule
combine multiple rules into a table when advanta-geous
re-order the rules to improve evaluation perfor-mance
В переводе:
удаление дублирующихся правил
удаление правил, являющихся подмножеством других правил
объединение нескольких правил в таблицу, когда это выгодно
изменение порядка правил для повышения производительности оценки
Другими словами, даже небрежно написанные правила pf будут аккуратно сгруппированы. Это отличная функция, которая позволяет писать легкочитаемые правила, а система сама организует их оптимальным образом.
Определение переменных
Указание имени WAN-интерфейса
wanint="vtnet0"
Определение назначенного IPv4-адреса как явного исходящего IP
exsrv1 = "163.44.113.145"
Определение переменных и таблиц для WireGuard
wg_ifs = "{ wg0, wg1 }"
wg_net = "10.1.0.0/24"
wg_ports="{51820}"
table <wg_clients> const { 10.1.0.2, 10.1.0.4, 10.1.0.22 }
Scrub Rules
В данном случае значение MSS указывается как для входящих, так и для исходящих пакетов из WAN.
Что касается all ramdom-id, это, по-видимому, предназначено для того, чтобы сама ОС не могла быть идентифицирована по пакетам, но, насколько я проверил с помощью sudo tcpdump -n -v -i $interface, в случае с GNU/Linux это, похоже, включено по умолчанию.
04:15:18.538048 IP (tos 0x0, ttl 52, id 61686, offset 0, flags [DF], proto TCP (6), length 52)
04:15:18.538049 IP (tos 0x0, ttl 52, id 61687, offset 0, flags [DF], proto TCP (6), length 131)
04:15:18.538179 IP (tos 0x0, ttl 64, id 65430, offset 0, flags [DF], proto TCP (6), length 52)
04:15:18.538223 IP (tos 0x0, ttl 52, id 61688, offset 0, flags [DF], proto TCP (6), length 131)
Кроме того, я настроил in all и out all, чтобы выполнять scrub не только для WAN-интерфейса.
scrub in on $wanint all random-id max-mss 1360
scrub out on $wanint all random-id max-mss 1360
scrub in all
scrub out all
NAT Rules
Определенные клиенты WireGuard выходят через NAT WAN-интерфейса на стороне сервера WireGuard.
Однако в этом случае выход осуществляется только по IPv4. Поскольку на стороне сервера WireGuard не раздаются IPv6-адреса, я указал только inet from.
nat on $wanint inet from <wg_clients> to any -> ($wanint)
Fail2Ban Rules
Правило для того, чтобы Fail2Ban добавлял IP-адреса атакующих в таблицу pf и блокировал их. Напоследок, с помощью anchor "f2b/*" пакеты с IP-адресов, добавленных в таблицу pf, направляются в правила Fail2Ban.
Я разместил это вверху, так как оно должно срабатывать раньше правил фильтрации с параметром quick.
Сразу перед этим идет правило по умолчанию block all, поэтому все пакеты, которые здесь не совпадут, будут заблокированы.
/usr/local/etc/fail2ban/action.d/pf.conf:
# Option: block
#
# The action you want pf to take.
# Probably, you want "block quick", but adjust as needed.
# If you want to log all blocked use "blog log quick"
block = block quick
Поскольку эта настройка добавляет правила Fail2Ban с block quick, я размещаю её выше.
То есть сначала пакеты с IP-адресов, добавленных в таблицу pf, направляются в правила Fail2Ban через anchor "f2b/*", а затем:
anchor "f2b/*"
В качестве самого первого правила фильтрации я установил правило, разрешающее исходящие пакеты.
pass out quick keep state
UDP Rules
Разрешающие правила для сервера WireGuard.
Разрешаем 51820/udp и все виртуальные сетевые интерфейсы WireGuard в $wg_ifs.
Возможно, здесь стоит сделать правила строже на всякий случай?
pass in quick on $wg_ifs from $wg_net to any keep state
pass in quick on $wanint proto udp from any to ($wanint) port $wg_ports keep state
В этом случае, поскольку состояние (state) уже поддерживается, все пакеты, прошедшие через туннель WireGuard, будут разрешены. Если бы порядок был обратным, правило pass in quick on $wg_ifs from $wg_net to any keep state не сработало бы, так как оно подразумевает наличие туннеля WireGuard.
Разрешаем 443/udp для включения HTTP/3.
# HTTP/3 Protocol
pass in quick on $wanint proto udp to ($wanint) port 443 keep state
TCP Rules
Те, кто пользовался nmap, знают, что сканирование TCP-портов происходит очень быстро, поэтому злоумышленники легко находят их, даже если вы измените порт. Сканирование UDP-портов с помощью nmap занимает очень много времени, поэтому для большей безопасности я настроил доступ к SSH только через туннель WireGuard. По сути, конфигурация, в которой TCP инкапсулируется в UDP, является наиболее безопасной.
Поскольку установлен Fail2ban, при атаке на SSH IP-адрес источника добавляется в таблицу pf и блокируется. Это работает почти как приманка (honeypot).
Для почтовых протоколов включен synproxy для предотвращения атак типа SYN Flood.
В случае с HTTP указание целевого IP как ($wanint) нужно для динамического сопоставления с IP-адресом, назначенным WAN-интерфейсу. Я не указываю inet явно, чтобы разрешить как IPv4, так и IPv6.
Пакеты, приходящие по IPv6, будут динамически сопоставляться с IPv6-адресом, назначенным WAN-интерфейсу, так как целевой IP указан как ($wanint).
# Mail Protocols
pass in quick proto tcp from any to ($wanint) port {25, 465, 993, 995} synproxy state
# HTTP
pass in quick on $wanint proto tcp to ($wanint) port {80, 443} modulate state
На этом всё, получилось длинновато...