Простая блокировка портов несомненно эффективна, но сервисам всё-таки нужно смотреть в открытый интернет. Настоящая защита требует комплексного подхода: динамических списков блокировок и механизмов скрытия сервисов. В этой статье я хочу поделиться своей «боевой» конфигурацией nftables.

Стратегия защиты

  1. SSH закрыт: SSH-порт закрыт для всех. Чтобы он открылся, используется port knocking: с nftables это намного проще реализуется по сравнению с iptables.

  2. Доверие, но проверка: Веб-трафик (80/443) и WireGuard (51820) открыты, но каждый пакет проверяется на соответствие состоянию соединения.

  3. Перманентный бан уже замеченных в атаках IP: Используется этот список abuseIPDB и обновляется ежедневно.

Конфигурация nftables.conf

Основной файл конфигурации /etc/nftables.conf оптимизирован для высокой производительности: проверка установленных соединений (established) стоит первой, что экономит ресурсы сервера. Для постоянных интерфейсов (lo, ens3) используется бинарный идентификатор. Для wg0 используется строковый идентификатор, что предотвращает ошибку запуска nftables, если интерфейс wireguard не успеет подняться раньше сервиса nftables.

/etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    # Списки для Port Knocking
    set ssh_knock { type ipv4_addr . inet_service; timeout 5s; }
    set ssh_ok { type ipv4_addr; timeout 1m; }

    # Список плохих парней (наполняется скриптом)
    set bad-ip {
        type ipv4_addr
        flags interval
        comment "Blocked IP addresses"
    }

    chain input {
        type filter hook input priority 0; policy drop;

        # 1. Принимаем всё, что уже проверено (established)
        ct state established,related accept
        ct state invalid drop

        # 2. Разрешаем доверенные интерфейсы
        iif lo accept
        iifname wg0 accept

        # 3. Сразу отсекаем ботов из черного списка
        # Установлен счётчик. Иногда интересно посмотреть статистику.
        ip saddr @bad-ip counter drop

        # 4. ICMP (чтобы сервер был "живым" для диагностики)
        ip protocol icmp accept

        # 5. Магия Port Knocking (40000 -> 5000 -> 10001)
        # Порты могут быть любые, кроме тех, на которые слушают сервисы
        tcp dport 40000 update @ssh_knock { ip saddr . 5000 } drop
        tcp dport 5000 ip saddr . tcp dport @ssh_knock update @ssh_knock { ip saddr . 10001 } drop
        tcp dport 10001 ip saddr . tcp dport @ssh_knock update @ssh_ok { ip saddr } drop
        
        # Разрешаем SSH только для "своих"
        tcp dport 22 ip saddr @ssh_ok counter accept

        # 6. ВЕБ-СЕРВЕР: открываем двери для HTTP и HTTPS
        tcp dport { 80, 443 } ct state new accept

        # 7. VPN: открываем порт WireGuard
        udp dport 51820 ct state new accept
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
        ct state established,related accept
        # Разрешаем выход в мир из подсети VPN
        # Не забыть про sysctl: net.ipv4.ip_forward=1
        ip saddr 10.8.0.0/24 accept
    }

# По-умолчанию accept. Можно не указывать.
#    chain output {
#        type filter hook output priority 0; policy accept;
#    }

}

# Правила для роутинга пакетов
table ip nat {
    chain postrouting {
        type nat hook postrouting priority 100;
        # Использование masquerade часто удобнее, если IP динамический,
        # но snat более "чистый" для серверов со статикой
        iifname wg0 oif ens3 snat to IP_сервера
    }

#    chain prerouting {
#        # Прозрачный прокси (например, Tor)
#        # Можно подменить IP для сайтов TOR'ом (опция TransPort)
#        iifname wg0 tcp dport { 80, 443 } redirect to :9040
#    }
}


# Подгружаем список заблокированных IP
# О нём информация будет дальше. Можно заранее создать пустой файл
# touch /etc/bad-ip.nft - иначе nftables заругается.
include "/etc/bad-ip.nft"

Автоматизация "чёрного" списка IP

Скрипт скачивает базу AbuseIPDB с github и обновляет только содержимое сета bad-ip, не затрагивая остальные правила. Здесь видны преимущества nftables: если для использования сетов в iptables использовалась утилита ipset, в которой для "бесшовной" смены сета использовалась команда ipset swap set1 set2, в nftables это происходит атомарно, одной транзакцей, без использования других утилит. Для загрузки в nftables используется файл /etc/bad-ip.nft, который был включён в основную конфигурацию.

Содержимое скрипта:

/usr/local/bin/bad-ip-updater.sh
#!/bin/bash

URL="https://github.com/borestad/blocklist-abuseipdb/raw/main/abuseipdb-s100-7d.ipv4"
FAMILY="inet"
TABLE="filter"
SET="bad-ip"
OUT_FILE="/etc/bad-ip.nft"



# Проверка зависимостей
for cmd in nft curl jq; do
    if ! command -v "$cmd" >/dev/null; then
        echo "Error: $cmd is not installed." >&2
        exit 1
    fi
done

(( EUID == 0 )) || { echo "Run as root" >&2; exit 1; }

TMP_IPS=$(mktemp)
TMP_NFT=$(mktemp)

clear_tmp() {
    rm -f "$TMP_IPS" "$TMP_NFT"
}

# Скачивание списка
# Используем -sS чтобы видеть ошибки, но не прогресс-бар
curl -fLSs --connect-timeout 10 "$URL" |
          grep -Eo '^([0-9]{1,3}\.){3}[0-9]{1,3}' > "$TMP_IPS" || {
    echo "Download failed" >&2
    clear_tmp
    exit 1; }

[ -s "$TMP_IPS" ] || {
    echo "Received empty list" >&2
    clear_tmp
    exit 1; }

# Генерируем пакет правил
# Сначала очищаем сет (flush), затем добавляем элементы
{
    echo "flush set $FAMILY $TABLE $SET"
    echo "add element $FAMILY $TABLE $SET {"
    # Быстрая вставка запятых через paste
    paste -sd ',' "$TMP_IPS"
    echo "}"
} > "$TMP_NFT"

# Проверка синтаксиса перед применением
if nft -c -f "$TMP_NFT"; then
    nft -f "$TMP_NFT"
    cp "$TMP_NFT" "$OUT_FILE"

    # Итоговый отчет
    DLCNT=$(wc -l < $TMP_IPS)
    COUNT=$(nft -j list set $FAMILY $TABLE $SET | jq '.nftables[] | select(has("set")) | .set.elem | length')
    echo "Success: $SET updated. ($COUNT/$DLCNT)"
else
    echo "Error: nftables syntax check failed!" >&2
    exit 1
fi

clear_tmp

Настройка ежедневных автообновлений чёрного списка

Используется systemd сервис и таймер, запускающий обновление сета .

Скрытый текст

/etc/systemd/system/bad-ip.service

[Unit]
Description=Bad IP updater

[Service]
Type=oneshot
ExecStart=/usr/local/bin/bad-ip-updater.sh

/etc/systemd/system/bad-ip.timer

[Unit]
Description=Bad IP daily updater

[Timer]
OnCalendar=6:00
RandomizedDelaySec=1h
Persistent=true

[Install]
WantedBy=timers.target

Для включения достаточно выполнитьsystemctl enable bad-ip.timer --now

Проверка обороны

Тут всё просто. С другой машины запускаем

nmap -p 22,53,80,443,51820 IP_сервера

Видим что 80 и 443 порты open, остальные - filtered. Значит, всё отлично работает. Пробуем зайти по ssh:

ssh IP_СЕРВЕРА

Если подключения не произошло - то тут тоже всё отлично работает! Сначала постучаться надо:

for p in 40000 5000 10001; do nc -w 1 -z IP_СЕРВЕРА $p; done; ssh IP_СЕРВЕРА

Вот только теперь подключение должно сработать.

Почему именно чёрный список IP, а не fail2ban?

Конечно, у такого подхода c abuseIPDB есть свои преимущества и недостатки.

Преимущества:

  • Пакеты "дропаются на подходе". Бот-сети даже не увидят ваш сервер

  • Предотвращение лишней нагрузки на сервисы. Никто постоянно не долбится в открытые порты. И чистые логи. 

Недостатки:

  • Риск бана обычного пользователя, с динамическим IP, который ранее принадлежал спамеру, или использует общий NAT вместе со спамером.

  • Отчёты abuseIPDB могут быть неточными. Но ежедневные обновления несколько облегчают такую ситуацию.

  • Против направленной атаки на ваш север такие списки конечно не помогут.

Тем не менее, для домашнего использования считаю подход со списками плохих IP вполне обоснованным. Если с fail2ban логи apache достаточно быстро наполняются мусором от бот-сетей, со списками плохих IP всё вполне чисто. Максимум около десятка запросов в день главной странички, никто не перебирает пароли от админки с разных IP.

Итоги

Достаточно простой настройкой nftables и одним скриптом сервер защищен от атак. Трафик идет своим чередом, WireGuard держит туннель, а случайные боты из abuseIPDB списков натыкаются на глухую стену.

Послесловие

Это моя первая статья для Хабра. Конечно, всё это уже так или иначе уже есть на сайте. Возможно, упустил некоторые моменты и не раскрыл подробности. Но тем не менее, это личный опыт знакомства с nftables.