Itnull

Admin
Регистрация
22.05.13
Сообщения
29.228
Реакции
13.526
Веб-сайт
itnull.me

Защита веб-сервера на FreeBSD 14.5 с помощью pf: установка и настройка​


FreeBSD 14.5 вышла в сентябре 2026 года. Это поздний релиз ветки 14.x, в котором в основном исправлены ошибки и закрыты уязвимости, поэтому для продакшен-серверов, которые пока не переходят на 15.x, обновление обязательно. В этой статье разберём, как включить штатный файрвол pf (Packet Filter) и настроить его для типичного веб-сервера с SSH, HTTP и HTTPS, защитой от брутфорса и флуда.

1. Что нужно​


  • Сервер с FreeBSD 14.5-RELEASE и root-доступ.
  • Имя внешнего сетевого интерфейса.

Узнать имя интерфейса:
Код:
ifconfig

Обычно это vtnet0 на KVM/VPS, em0 / igb0 / ix0 на железе. Ниже в примерах используется vtnet0, подставьте своё.

⚠️ Важно: если вы работаете по SSH, неверное правило отрежет вам доступ. Прочитайте раздел 5 про безопасное применение правил до первого запуска pf.

2. Включаем pf​


pf входит в базовую систему и ставить ничего не нужно. Достаточно включить его в rc.conf:

Код:
sysrc pf_enable="YES"
sysrc pf_rules="/etc/pf.conf"
sysrc pflog_enable="YES"
sysrc pflog_logfile="/var/log/pflog"

Модуль ядра загрузится автоматически при старте службы. Проверить можно так:

Код:
kldload pf      # если модуль ещё не загружен
kldstat | grep pf

3. Конфигурация /etc/pf.conf​


Создайте файл /etc/pf.conf:

Код:
ee /etc/pf.conf

Готовая конфигурация для веб-сервера:

Код:
#==============================================
#  /etc/pf.conf — веб-сервер на FreeBSD 14.5
#==============================================

### Макросы
ext_if   = "vtnet0"
ssh_port = "22"
web_ports = "{ 80, 443 }"

# IP-адреса администраторов (можно добавить несколько)
admin_ips = "{ 203.0.113.10 }"

icmp_types = "{ echoreq, unreach }"

### Таблицы
table <bruteforce> persist file "/etc/pf.bruteforce"
table <abusers>    persist
table <blacklist>  persist file "/etc/pf.blacklist"

### Опции
set skip on lo0
set block-policy drop
set loginterface $ext_if
set optimization normal
set limit states 200000

### Нормализация трафика
scrub in on $ext_if all fragment reassemble

### Политика по умолчанию: всё запрещено
block log all

### Защита от подмены адресов
antispoof quick for $ext_if

### Блок-листы
block drop in quick on $ext_if from <blacklist>
block drop in quick on $ext_if from <bruteforce>
block drop in quick on $ext_if from <abusers>

### Исходящий трафик сервера разрешён
pass out quick on $ext_if keep state

### SSH с защитой от перебора
pass in on $ext_if inet proto tcp from any to ($ext_if) port $ssh_port \
    flags S/SA keep state \
    (max-src-conn 10, max-src-conn-rate 5/30, \
     overload <bruteforce> flush global)

### HTTP/HTTPS с ограничением флуда
pass in on $ext_if proto tcp from any to ($ext_if) port $web_ports \
    flags S/SA keep state \
    (max-src-conn 200, max-src-conn-rate 100/5, \
     overload <abusers> flush global)

### HTTP/3 (QUIC), если включён в nginx
pass in on $ext_if proto udp from any to ($ext_if) port 443 keep state

### ICMP (IPv4)
pass in on $ext_if inet proto icmp all icmp-type $icmp_types keep state

### ICMPv6 (без него IPv6 работать не будет)
pass in on $ext_if inet6 proto ipv6-icmp all keep state

Создайте пустые файлы для таблиц, иначе pf выдаст ошибку при загрузке:

Код:
touch /etc/pf.bruteforce /etc/pf.blacklist
chmod 600 /etc/pf.bruteforce /etc/pf.blacklist

Что здесь происходит​


БлокНазначение
set skip on lo0Не фильтровать loopback, иначе сломаются локальные сервисы (PHP-FPM, MySQL на 127.0.0.1)
block-policy dropЗапрещённые пакеты молча отбрасываются, сканер не получает ответа
scrubСобирает фрагменты и нормализует пакеты
antispoofОтбрасывает пакеты с поддельным адресом из вашей же сети
block log allВсё, что не разрешено явно, запрещено и пишется в лог
max-src-conn-rate 5/30Больше 5 новых SSH-подключений за 30 секунд — IP уходит в <bruteforce>
flush globalСбрасывает все уже открытые соединения нарушителя
keep stateОтветные пакеты пропускаются автоматически

4. Только для администраторов: закрываем SSH от всех​


Если у вас статический IP, надёжнее вообще не показывать SSH миру. Замените блок SSH на:

Код:
pass in on $ext_if inet proto tcp from $admin_ips to ($ext_if) port $ssh_port \
    flags S/SA keep state

Также можно перенести SSH на нестандартный порт (в /etc/ssh/sshd_config и в макросе ssh_port). От целевой атаки это не спасёт, но мусора в логах станет на порядок меньше.

5. Безопасное применение правил​


Сначала проверьте синтаксис без загрузки:

Код:
pfctl -nf /etc/pf.conf

Если ошибок нет, подстрахуйтесь «таймером отката». Эта команда выключит pf через 5 минут, если вы потеряете доступ:

Код:
nohup sh -c 'sleep 300 && pfctl -d' >/dev/null 2>&1 &

Теперь запускаем:

Код:
service pf start
service pflog start

Откройте новую SSH-сессию, не закрывая старую. Если она подключилась и сайт открывается, отмените таймер:

Код:
pkill -f "sleep 300"

Если вы потеряли доступ, через 5 минут pf отключится сам.

6. Проверка и мониторинг​


Код:
pfctl -s info        # статистика и состояние
pfctl -s rules       # загруженные правила
pfctl -s states      # активные соединения
pfctl -vs rules      # правила со счётчиками пакетов

Смотреть заблокированные пакеты в реальном времени:

Код:
tcpdump -n -e -ttt -i pflog0

Читать сохранённый лог:

Код:
tcpdump -n -e -ttt -r /var/log/pflog

7. Управление таблицами​


Код:
# Посмотреть, кто попал в бан
pfctl -t bruteforce -T show
pfctl -t abusers -T show

# Добавить IP или подсеть в чёрный список
pfctl -t blacklist -T add 198.51.100.0/24

# Разбанить адрес
pfctl -t bruteforce -T delete 192.0.2.55

# Очистить таблицу целиком
pfctl -t abusers -T flush

Автоматическая разблокировка и сохранение банов​


Добавьте в /etc/crontab:

Код:
# Снимать баны старше суток
*/30  *  *  *  *  root  /sbin/pfctl -t bruteforce -T expire 86400
*/30  *  *  *  *  root  /sbin/pfctl -t abusers -T expire 3600
# Сохранять список брутфорсеров на диск, чтобы он пережил перезагрузку
0     *  *  *  *  root  /sbin/pfctl -t bruteforce -T show > /etc/pf.bruteforce

8. Повседневные команды​


Код:
pfctl -f /etc/pf.conf    # перечитать правила без перезапуска
pfctl -d                 # выключить pf
pfctl -e                 # включить pf
service pf reload        # то же самое через rc
pfctl -F states          # сбросить все соединения (осторожно по SSH!)

9. Частые ошибки​


  1. Сайт работает через Cloudflare или другой прокси. Все посетители приходят с IP прокси, и правило max-src-conn-rate может забанить сам Cloudflare. В этом случае разрешите 80/443 только из подсетей Cloudflare (табличкой) и уберите лимиты с веб-портов, а защиту от флуда настройте на стороне CDN и в nginx (limit_req).
  2. Не открыт ICMPv6. Без него IPv6 работает нестабильно или не работает совсем.
  3. Забыли set skip on lo0. В результате PHP-FPM, Redis и MySQL на localhost начинают «таинственно» отваливаться.
  4. Файлы таблиц не созданы. pf не загрузит конфигурацию с persist file, если файла нет.
  5. Правила применены без проверки pfctl -nf. Как следствие, сервер приходится поднимать через KVM-консоль хостера.

Итог​


После этих шагов у вас:
  • закрыто всё, кроме SSH, HTTP и HTTPS;
  • брутфорс SSH автоматически блокируется;
  • флуд на веб-порты ограничен;
  • запрещённый трафик логируется;
  • баны снимаются сами и переживают перезагрузку.

Документация для углублённого изучения: man pf.conf, man pfctl и раздел о файрволах в FreeBSD Handbook.
 

FreeBSD 14.5 + pf: продолжение. blacklistd, Cloudflare, GeoIP и тюнинг сетевого стека​


В первой части мы включили pf, написали базовый набор правил для веб-сервера и настроили автоматический бан за брутфорс и флуд. Теперь добавим четыре вещи, которые превращают «просто файрвол» в нормальную защиту продакшен-сервера:

  1. blacklistd — штатный аналог fail2ban, банит за неудачные попытки входа, а не просто за частоту подключений;
  2. Cloudflare — правильная фильтрация, если сайт стоит за CDN;
  3. GeoIP — доступ к SSH только из нужных стран;
  4. sysctl — усиление сетевого стека ядра.

1. blacklistd — бан за неверный пароль​


У правила max-src-conn-rate из первой части есть недостаток: оно считает только количество подключений. Медленный перебор (одна попытка раз в минуту) оно не поймает, а админа, который быстро открыл пять вкладок терминала, — забанит.

В базовой системе FreeBSD есть демон blacklistd. sshd сам сообщает ему о каждой неудачной авторизации, а blacklistd добавляет нарушителя в pf. Ничего ставить не нужно.

В FreeBSD 15 демон переименован в blocklistd, параметры аналогичные. Для 14.5 используем старое имя.

Шаг 1. Включаем поддержку в sshd​


В файле /etc/ssh/sshd_config раскомментируйте или добавьте:

Код:
UseBlacklist yes

Шаг 2. Настраиваем правила бана​


Откройте /etc/blacklistd.conf. Строка для SSH по умолчанию уже есть:

Код:
# adr/mask:port  type    proto   owner   name    nfail   disable
[local]
ssh             stream  *       *       *       3       24h

Это означает: 3 неудачные попытки — бан на 24 часа. Если сменили порт SSH, укажите его вместо ssh, например 2222.

Чтобы никогда не забанить себя, добавьте свой IP в секцию [remote]:

Код:
[remote]
203.0.113.10:ssh  *  *  *  =  *  *

Шаг 3. Подключаем к pf​


В /etc/pf.conf сразу после строки antispoof добавьте якорь:

Код:
anchor "blacklistd/*" in on $ext_if

Якорь должен стоять выше правил pass, иначе забаненный адрес успеет пройти.

Шаг 4. Запускаем​


Код:
sysrc blacklistd_enable="YES"
sysrc blacklistd_flags="-r"
pfctl -nf /etc/pf.conf && pfctl -f /etc/pf.conf
service blacklistd start
service sshd restart

Флаг -r заново загружает баны в pf после перезагрузки сервера.

Просмотр и разбан​


Код:
# Кто сейчас забанен
blacklistctl dump -b

# Что лежит в pf
pfctl -a blacklistd/22 -t port22 -T show

# Разбанить адрес
pfctl -a blacklistd/22 -t port22 -T delete 192.0.2.55

Номер в имени якоря и таблицы совпадает с портом SSH.

2. Сайт за Cloudflare​


Если домен проксируется через Cloudflare, все посетители приходят на сервер с IP-адресов Cloudflare. Из этого следуют две проблемы:
  • правило с overload <abusers> рано или поздно забанит сам Cloudflare, и сайт ляжет;
  • злоумышленник, узнавший реальный IP сервера, может обойти CDN и атаковать напрямую.

Решение: принимать 80/443 только от Cloudflare.

Скрипт обновления списка адресов​


Создайте /usr/local/sbin/pf-cloudflare.sh:

Код:
#!/bin/sh
TMP=$(mktemp)
fetch -qo - https://www.cloudflare.com/ips-v4 >> "$TMP" || exit 1
echo "" >> "$TMP"
fetch -qo - https://www.cloudflare.com/ips-v6 >> "$TMP" || exit 1

# Защита от пустого ответа
if [ $(grep -c '/' "$TMP") -lt 10 ]; then
    rm -f "$TMP"; exit 1
fi

mv "$TMP" /etc/pf.cloudflare
chmod 644 /etc/pf.cloudflare
/sbin/pfctl -t cloudflare -T replace -f /etc/pf.cloudflare

Код:
chmod 700 /usr/local/sbin/pf-cloudflare.sh
/usr/local/sbin/pf-cloudflare.sh

Правила в pf.conf​


Добавьте таблицу к остальным:

Код:
table <cloudflare> persist file "/etc/pf.cloudflare"

И замените блок HTTP/HTTPS из первой части на:

Код:
### Веб только через Cloudflare
pass in on $ext_if proto tcp from <cloudflare> to ($ext_if) port $web_ports \
    flags S/SA keep state
pass in on $ext_if proto udp from <cloudflare> to ($ext_if) port 443 keep state

Лимиты здесь не нужны: от флуда защищает сам Cloudflare.

Автообновление раз в сутки​


В /etc/crontab:

Код:
15  4  *  *  *  root  /usr/local/sbin/pf-cloudflare.sh

Не забудьте про nginx! Чтобы в логах и в форуме были реальные IP посетителей, а не Cloudflare, подключите модуль real_ip: директивы set_real_ip_from для подсетей Cloudflare и real_ip_header CF-Connecting-IP;. Иначе XenForo будет видеть всех пользователей с одних и тех же адресов.

3. GeoIP: SSH только из своих стран​


Если вы администрируете сервер только из определённых стран, нет смысла пускать на SSH весь мир. Списки подсетей по странам бесплатно публикует ipdeny.com.

Скрипт загрузки​


/usr/local/sbin/pf-geoip.sh:

Код:
#!/bin/sh
# Коды стран, из которых разрешён SSH
COUNTRIES="ua pl"
URL="https://www.ipdeny.com/ipblocks/data/aggregated"
TMP=$(mktemp)

for c in $COUNTRIES; do
    fetch -qo - "$URL/${c}-aggregated.zone" >> "$TMP" || { rm -f "$TMP"; exit 1; }
done

if [ $(wc -l < "$TMP") -lt 50 ]; then
    rm -f "$TMP"; exit 1
fi

mv "$TMP" /etc/pf.geo_allow
/sbin/pfctl -t geo_allow -T replace -f /etc/pf.geo_allow

Код:
chmod 700 /usr/local/sbin/pf-geoip.sh
/usr/local/sbin/pf-geoip.sh

Правила в pf.conf​


Код:
table <geo_allow> persist file "/etc/pf.geo_allow"

### SSH: только из разрешённых стран или с IP админа
pass in on $ext_if inet proto tcp from { <geo_allow>, $admin_ips } \
    to ($ext_if) port $ssh_port flags S/SA keep state \
    (max-src-conn 10, max-src-conn-rate 5/30, \
     overload <bruteforce> flush global)

Обновление раз в неделю:

Код:
30  4  *  *  1  root  /usr/local/sbin/pf-geoip.sh

⚠️ Внимание: если вы подключаетесь через VPN или из поездки, страна может не совпасть, и вы останетесь без доступа. Всегда держите $admin_ips в правиле и знайте, как попасть в KVM-консоль хостера.

4. Тюнинг сетевого стека (sysctl)​


Эти параметры делают сервер менее заметным для сканеров и устойчивее к SYN-флуду. Добавьте в /etc/sysctl.conf:

Код:
# Не отвечать RST на закрытые TCP-порты и ICMP на закрытые UDP
net.inet.tcp.blackhole=2
net.inet.udp.blackhole=1

# Защита от SYN-флуда
net.inet.tcp.syncookies=1

# Не принимать ICMP redirect и source routing
net.inet.icmp.drop_redirect=1
net.inet.ip.redirect=0
net.inet.ip.sourceroute=0
net.inet.ip.accept_sourceroute=0
net.inet6.ip6.redirect=0

# Ограничение ответов ICMP
net.inet.icmp.icmplim=50

# Случайные ID в IP-заголовках
net.inet.ip.random_id=1

# Очередь входящих соединений для нагруженного nginx
kern.ipc.soacceptqueue=4096

Применить без перезагрузки:

Код:
service sysctl restart

ПараметрЭффект
tcp.blackhole=2Сканер не может отличить закрытый порт от фильтруемого, сканирование сильно замедляется
syncookies=1Сервер не расходует память на полуоткрытые соединения при SYN-флуде
drop_redirect=1Злоумышленник не может подменить маршрутизацию через ICMP
soacceptqueueМеньше отказов в соединении при пиковой нагрузке

5. Логи pf не забьют диск​


Ротация /var/log/pflog в FreeBSD уже настроена по умолчанию. Проверьте, что в /etc/newsyslog.conf есть строка:

Код:
/var/log/pflog    600  3  100  *  JB  /var/run/pflogd.pid

Это означает: ротация при достижении 100 КБ, хранить 3 архива, сжатие bzip2. Если на сервере много атак, увеличьте размер и количество архивов, например 600 7 1024.

Быстрая статистика «кто больше всех стучится»:

Код:
tcpdump -n -r /var/log/pflog 2>/dev/null | awk '{print $3}' | \
    cut -d. -f1-4 | sort | uniq -c | sort -rn | head -20

Итоговый порядок блоков в pf.conf​


Чтобы не запутаться, вот правильная последовательность после всех изменений:

  1. Макросы
  2. Таблицы (bruteforce, abusers, blacklist, cloudflare, geo_allow)
  3. Опции (set ...)
  4. scrub
  5. block log all
  6. antispoof
  7. anchor "blacklistd/*"
  8. Блок-листы (block drop in quick ...)
  9. pass out
  10. SSH
  11. HTTP/HTTPS
  12. ICMP / ICMPv6

После каждой правки помните про правило из первой части: сначала pfctl -nf, потом таймер отката, потом применение.

Итог​


Теперь сервер:
  • банит за неверные пароли, а не только за частые подключения;
  • принимает веб-трафик только через Cloudflare и не светит реальный IP;
  • пускает на SSH только из нужных стран;
  • не отвечает сканерам и устойчив к SYN-флуду;
  • не забивает диск логами.
 

Создайте аккаунт или войдите в систему, чтобы комментировать

Вы должны быть зарегистрированным, чтобы оставить комментарий

Создать аккаунт

Создайте аккаунт в нашем сообществе.

Войти

У вас уже есть аккаунт? Войдите здесь.

Назад
Сверху Снизу