Брандмауэры и netfilter

TL;DR: Брандмауэр в Linux — это netfilter, подсистема ядра, которая перехватывает пакеты в определённых точках (hooks) и применяет к ним правила: пропустить, отбросить, изменить. Управляется через iptables (legacy) или nftables (современный). Ключевая идея — цепочки правил на пути пакета через ядро: PREROUTING → FORWARD/INPUT → OUTPUT → POSTROUTING.

Зачем это знать

Firewall — первая линия защиты сервера. Но он не «ставится и забывается»:

  • Неправильный порядок правил — трафик проходит мимо фильтра
  • Непонимание пути пакета — NAT-правила не срабатывают, port forwarding не работает
  • Docker, Kubernetes, libvirt добавляют собственные цепочки в iptables — конфликты ломают сеть
  • Миграция с iptables на nftables требует понимания обеих моделей

Ключевые концепции

Netfilter — подсистема ядра

Netfilter — фреймворк в ядре Linux для перехвата и обработки сетевых пакетов. Он определяет 5 точек перехвата (hooks) на пути пакета через сетевой стек:

                              Пакет прибыл
                                  │
                                  ▼
                          ┌──────────────┐
                          │  PREROUTING  │ ← DNAT, connection tracking
                          └──────┬───────┘
                                 │
                          Решение маршрутизации:
                          пакет для нас или транзитный?
                                 │
                    ┌────────────┴────────────┐
                    │                         │
                    ▼                         ▼
            ┌──────────┐              ┌──────────┐
            │  INPUT   │              │ FORWARD  │ ← фильтрация транзитного трафика
            └────┬─────┘              └────┬─────┘
                 │                         │
                 ▼                         │
          Локальный процесс                │
          (nginx, sshd, etc.)              │
                 │                         │
                 ▼                         │
            ┌──────────┐                   │
            │  OUTPUT  │                   │
            └────┬─────┘                   │
                 │                         │
                 └────────────┬────────────┘
                              │
                              ▼
                      ┌───────────────┐
                      │ POSTROUTING   │ ← SNAT, masquerade
                      └───────┬───────┘
                              │
                              ▼
                        Пакет уходит
  • PREROUTING — сразу после получения пакета, до решения о маршрутизации. Здесь DNAT (port forwarding) — меняем dst до того, как ядро решит куда направить пакет
  • INPUT — пакет предназначен локальному процессу на этом хосте
  • FORWARD — транзитный пакет, проходящий сквозь хост (маршрутизатор)
  • OUTPUT — пакет сгенерирован локальным процессом
  • POSTROUTING — перед отправкой в сеть. Здесь SNAT/masquerade — подменяем src после маршрутизации

Таблицы iptables

iptables группирует правила в таблицы по назначению. Каждая таблица содержит цепочки (chains), а каждая цепочка — упорядоченный список правил.

ТаблицаНазначениеЦепочкиКогда нужна
filterФильтрация (пропустить/отбросить)INPUT, FORWARD, OUTPUTВсегда — основная таблица
natПодмена адресов (NAT)PREROUTING, OUTPUT, POSTROUTINGМаршрутизатор, port forwarding
mangleМодификация заголовков (TTL, TOS, mark)Все 5Редко — QoS, policy routing
rawОбход connection trackingPREROUTING, OUTPUTРедко — высоконагруженные серверы

По умолчанию работает таблица filter (если не указать -t):

# Эквивалентные команды:
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo iptables -t filter -A INPUT -p tcp --dport 22 -j ACCEPT

Правила и действия (targets)

Каждое правило состоит из условий совпадения (match) и действия (target):

# Структура правила:
# iptables -t ТАБЛИЦА -A ЦЕПОЧКА [условия] -j ДЕЙСТВИЕ
 
# Пример: разрешить HTTPS только из внутренней сети
sudo iptables -A INPUT \
  -p tcp \                    # протокол TCP
  --dport 443 \               # destination port 443
  -s 10.0.0.0/8 \             # source IP из подсети 10.0.0.0/8
  -i eth0 \                   # входящий интерфейс eth0
  -m state --state NEW \      # новое соединение (connection tracking)
  -j ACCEPT                   # действие: пропустить
ДействиеОписание
ACCEPTПропустить пакет
DROPОтбросить молча (отправитель не узнает)
REJECTОтбросить с ICMP-ответом (отправитель получит ошибку)
LOGЗалогировать и продолжить обработку (не терминальное)
DNATИзменить destination (таблица nat)
SNATИзменить source (таблица nat)
MASQUERADESNAT с автоопределением IP (таблица nat)

DROP vs REJECT: DROP безопаснее — сканер портов не получает подтверждения. REJECT вежливее — клиент сразу получает ошибку, не ждёт таймаут. Для внешнего трафика обычно DROP, для внутреннего — REJECT.

Порядок имеет значение

Правила проверяются сверху вниз, первое совпавшее — срабатывает:

# ❌ Неправильно: ACCEPT до DROP — запрет не сработает
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT    # правило 1
sudo iptables -A INPUT -s 10.0.0.5 -p tcp --dport 22 -j DROP  # никогда не сработает!
 
# ✅ Правильно: специфичные правила первыми
sudo iptables -A INPUT -s 10.0.0.5 -p tcp --dport 22 -j DROP  # конкретный запрет
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT    # общий доступ

Политика по умолчанию

Если ни одно правило не совпало, применяется политика цепочки (policy):

# Стратегия «default deny» — запретить всё, разрешить явно
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT    # исходящий обычно разрешают
 
# Обязательно перед этим:
sudo iptables -A INPUT -i lo -j ACCEPT                          # loopback
sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT  # ответы
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT              # SSH

Важно: iptables -P INPUT DROP без правила для SSH = потеря удалённого доступа. Всегда добавляй allow-правила до смены политики.

Connection Tracking (stateful firewall)

Netfilter отслеживает состояние каждого соединения (модуль conntrack). Это позволяет не прописывать отдельные правила для ответного трафика:

СостояниеОписание
NEWПервый пакет нового соединения
ESTABLISHEDПакет принадлежит уже установленному соединению
RELATEDСвязан с существующим (ICMP error, FTP data)
INVALIDНе относится ни к одному известному соединению
# Одно правило вместо десятков: разрешить все ответы на наши запросы
sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
 
# Теперь достаточно разрешать только NEW-соединения для входящего трафика
sudo iptables -A INPUT -p tcp --dport 80 -m state --state NEW -j ACCEPT

Без connection tracking (stateless firewall) пришлось бы явно разрешать входящие пакеты на ephemeral-порты (49152–65535) для ответного трафика — дыра в безопасности.

# Текущая таблица conntrack
sudo conntrack -L
# tcp  6 300 ESTABLISHED src=192.168.1.100 dst=93.184.216.34 sport=49832 dport=443 ...
 
# Статистика
sudo conntrack -S
cat /proc/sys/net/netfilter/nf_conntrack_count   # текущих записей
cat /proc/sys/net/netfilter/nf_conntrack_max     # максимум

nftables — замена iptables

nftables — новый фреймворк (с ядра 3.13, 2014), заменяющий iptables/ip6tables/arptables/ebtables единым инструментом nft. Debian 10+, RHEL 8+ используют nftables по умолчанию.

Отличия от iptables

iptablesnftables
Утилитыiptables, ip6tables, arptables, ebtablesОдна: nft
ТаблицыПредопределённые (filter, nat, mangle)Создаёшь сам
ПроизводительностьЛинейный обход правилОптимизации: sets, maps, concatenations
АтомарностьПравила добавляются по одномуВесь ruleset — атомарно (nft -f)
СинтаксисФлаги CLI (-A INPUT -p tcp --dport 22 -j ACCEPT)Ближе к языку (tcp dport 22 accept)

Базовый firewall на nftables

#!/usr/sbin/nft -f
# /etc/nftables.conf — базовый серверный firewall
 
flush ruleset                          # очистить всё
 
table inet filter {                    # inet = IPv4 + IPv6
    chain input {
        type filter hook input priority 0; policy drop;  # default deny
 
        iifname "lo" accept            # loopback
        ct state established,related accept  # ответы на наши запросы
        ct state invalid drop          # битые пакеты
 
        # ICMP (ping, MTU discovery, etc.)
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept
 
        # Разрешённые сервисы
        tcp dport 22 accept            # SSH
        tcp dport { 80, 443 } accept   # HTTP(S)
 
        # Логировать отброшенное (rate limit чтобы не забить лог)
        limit rate 5/minute log prefix "nft-drop: " drop
    }
 
    chain forward {
        type filter hook forward priority 0; policy drop;  # не маршрутизатор
    }
 
    chain output {
        type filter hook output priority 0; policy accept;  # исходящее разрешено
    }
}
# Применить конфигурацию
sudo nft -f /etc/nftables.conf
 
# Просмотреть текущие правила
sudo nft list ruleset
 
# Добавить правило на лету
sudo nft add rule inet filter input tcp dport 8080 accept
 
# Удалить правило (по handle)
sudo nft -a list chain inet filter input   # показать handles
sudo nft delete rule inet filter input handle 15
 
# Включить при загрузке
sudo systemctl enable nftables

Стратегии фильтрации

Default deny (whitelist)

Запретить всё по умолчанию, разрешить только нужное. Рекомендуемый подход — любой пропущенный порт закрыт:

policy DROP → явно ACCEPT нужные порты

Default allow (blacklist)

Разрешить всё по умолчанию, запрещать конкретное. Опасно — забытый open-порт = уязвимость:

policy ACCEPT → явно DROP опасные порты

Зонирование

Разные правила для разных интерфейсов:

# nftables: внешний vs внутренний интерфейс
chain input {
    type filter hook input priority 0; policy drop;
 
    ct state established,related accept
 
    # Внутренняя сеть — доверенная
    iifname "eth1" accept
 
    # Внешний интерфейс — только SSH и HTTPS
    iifname "eth0" tcp dport { 22, 443 } accept
}

Взаимодействие с Docker и Kubernetes

Docker и Kubernetes создают собственные цепочки в iptables. Это частый источник проблем.

Docker

Docker вставляет правила в цепочку DOCKER (nat + filter), обходя цепочку INPUT. Результат: ufw deny 8080 не блокирует Docker-порт, потому что трафик идёт через FORWARD → DOCKER, минуя INPUT.

# Docker добавляет при -p 8080:80:
sudo iptables -t nat -L DOCKER
# DNAT  tcp  --  anywhere  anywhere  tcp dpt:8080 to:172.17.0.2:80
 
sudo iptables -L DOCKER
# ACCEPT  tcp  --  anywhere  172.17.0.2  tcp dpt:80

Решения:

  • Привязывать порты к localhost: -p 127.0.0.1:8080:80
  • Использовать DOCKER-USER цепочку для своих правил (Docker её не перезаписывает)
  • "iptables": false в /etc/docker/daemon.json — ручное управление

Kubernetes

kube-proxy создаёт цепочки KUBE-SERVICES, KUBE-SVC-*, KUBE-SEP-* для маршрутизации к Pod’ам через DNAT. NetworkPolicy реализуется CNI-плагином (Calico, Cilium) через дополнительные правила в iptables или eBPF.

Подводные камни

Важно: Никогда не применяй iptables -P INPUT DROP через SSH без правила allow для порта 22. Второго шанса не будет (только console/IPMI/KVM).

ПроблемаСимптомРешение
Правило не в том порядкеТрафик проходит/блокируется вопреки ожиданиямiptables -L -n --line-numbers, проверить порядок
Docker обходит ufwПорт доступен снаружи несмотря на ufw denyDOCKER-USER цепочка или bind на 127.0.0.1
conntrack table fulldropping packet в dmesg, потеря соединенийУвеличить nf_conntrack_max, уменьшить таймауты
iptables vs nftables конфликтПравила «исчезают» или дублируютсяВыбрать один инструмент. update-alternatives --config iptables
REJECT на внешнем интерфейсеСканеру видны закрытые порты (получает RST)Использовать DROP для внешнего трафика
Правила теряются после rebootFirewall «сбрасывается»netfilter-persistent save или systemctl enable nftables

Связанные материалы

  • configure-firewall — практика: ufw, iptables, nftables, firewalld
  • linux-router — ip_forward, masquerade, FORWARD-правила
  • networking — стек TCP/IP, IP, подсети, маршрутизация
  • nat — NAT/SNAT/DNAT, masquerade, conntrack
  • harden-server — SSH, fail2ban, комплексная защита