Ви не помічаєте фаєрвол, поки не стане боляче. Зазвичай о 02:13, коли «маленька зміна» перетворює SSH на привида,
ваш дежурний телефон — на сигнал тривоги, а CEO — на напівпрофесійного мережевого інженера.
nftables може бути елегантним і швидким — але тільки якщо припинити ставитись до нього як до збільшеного iptables.
Це продакшн-структура, яка лишається читабельною, коли набір правил виростає понад 500, витримує аудит
і дозволяє налагоджувати як дорослий.
Чому структура важливіша за синтаксис
nftables не складний через синтаксис. Він складний через те, що ваш набір правил — це жива система:
він росте шляхом нашарування, його змінюють під тиском, і його читають люди, які його не писали
(включно з Майбутнім Вами, який є найменш поблажливим рецензентом).
Коли ви переходите позначку ~200 правил, відбуваються дві речі:
- Ризик змін зростає. Невелика зміна може перевести шлях пакета з «accept» у «drop» без очевидного сигналу diff.
- Час налагодження вибухає. Ви припиняєте мислити в термінах політики й починаєте шукати рядки через grep і молитися.
Ліки — це структура. Не «коментарі скрізь». Справжня структура:
передбачувані межі ланцюгів, суворе іменування, набори/мапи для ідентичності та набір правил, який відображає реальний потік пакетів.
Моя думка: якщо ваш nftables-файл — це один 1 200-рядковий моноліт, у вас немає фаєрвола. У вас є майбутній інцидент.
Кілька фактів та історичний контекст (щоб ви не повторювали помилок 2012 року)
Короткі, конкретні контекстні пункти, що важливі в продакшні:
- nftables потрапив у mainline ядро Linux в 3.13 (2014). Він уже не «новий»; ваші погані звички — ось що нове.
- iptables насправді — фронтенд до netfilter. nftables — новіший фронтенд, спроєктований замінити сімейство iptables і знизити дублювання.
- nftables використовує підхід на кшталт VM-байткоду. Через це правила можна компактно виразити, і набори/мапи радикально зменшують кількість правил.
- iptables історично мав окремі інструменти для IPv4/IPv6/arptables/ebtables. nftables їх уніфікував, що добре — доки ви випадково не «уніфікуєте» дві політики, які мали бути окремими.
- Conntrack (станова відстежуваність) значно старший за nftables. nftables не винайшов стан; він зробив його застосування простішим і послідовним.
- nftables підтримує атомарну заміну ruleset. Це операційне золото: ви можете завантажити новий набір правил як транзакцію, а не як ризиковану послідовність правок.
- Набори були проривом для продуктивності й читабельності. Замість 200 рядків «ip saddr X accept» ви маєте одне правило плюс визначення набору.
- nftables має кращі засоби інспекції. Лічильники, ідентифікатори (handles) і структурований перелік створені для налагодження та інструментів — не лише для людей, що дивляться на текст.
Одна цитата, яку операційні люди повинні «татуювати» на процесі змін:
Надія — це не стратегія.
— General Gordon R. Sullivan
Принципи проєктування, що масштабуються понад 500 правил
1) Один потік пакета — одна історія
Читабельний набір правил розповідає історію пакета. Наприклад: вхідний пакет потрапляє в input → перевірки санітарності →
established/related → «дозволені сервіси» → логування з обмеженням → drop.
Якщо ваш input-ланцюг чергує «дозволити nginx» та «заблокувати bogons» та «дозволити моніторинг» і «скинути фрагменти» без порядку,
ви змушуєте читача симулювати весь ланцюг у голові. Це не масштабується.
2) За замовчуванням deny на межах ланцюгів, а не розкидано
«За замовчуванням deny» — це не «скидати пакети в 90 місцях». Це одне свідоме політичне рішення наприкінці шляху
(або як політика ланцюга), з винятками, винесеними вперед.
Розкидання випадкових drop правил унеможливлює аудит і створює «приховані політики», про які ніхто не пам’ятає.
3) Використовуйте набори/мапи для ідентичності; використовуйте ланцюги для поведінки
Набори та мапи — ваш бюджет читабельності. Ви «витрачаєте» його, щоб правила були короткими.
Ланцюги виражають поведінку та порядок. Якщо робити навпаки (ланцюги для ідентичності, правила для поведінки),
отримаєте комбінаторний кошмар.
4) Розділіть «edge», «service host» і «transit»
Найшвидший спосіб створити зламаний набір правил — змішати логіку маршрутизатора (форвардинг/NAT), логіку хоста (локальні сервіси)
і логіку «ця машина також VPN-ендпойнт» в одному ланцюзі.
Використовуйте окремі таблиці або принаймні окремі include-файли. Зробіть неможливим випадкове змінення NAT під час «просто відкриваю порт».
5) Будьте явними щодо того, хто може говорити з самим хостом
Більшість болю в продакшні пов’язана з трафіком control-plane: SSH, конфігурування, моніторинг, синхронізація часу, service discovery.
Ставте його як політику першого класу, а не як післямірку.
6) Логування має бути корисним або його не має бути
Лог-шторм не «допомагає налагодженню». Він допомагає рахунку SIEM.
Логуйте лише у точках прийняття рішень, обмежуйте швидкість і додавайте префікс, який можна шукати через grep.
Жарт №1: Якщо ваш фаєрвол логує все, вітаю — ви винайшли дуже дорогий генератор випадкових чисел.
7) Віддавайте перевагу атомарним перезавантаженням і тестуйте всерйоз
Зміни в продакшні мають бути: перевірити синтаксис → dry-run (або хоча б парсинг) → застосувати атомарно → перевірити лічильники/поведінку.
Не «редагувати на живому терміналі і сподіватися, що TCP сесія виживе».
8) Не женіться за мікрооптимізаціями, поки не зможете пояснити набір правил
nftables швидкий. Ваш реальний вузький горлечко зазвичай не «два додаткові порівняння», а «ніхто не знає, яке правило реально працює».
Оптимізуйте для керованості спочатку. Майбутні інциденти скажуть вам «дякую».
Еталонна структура: файли, таблиці, ланцюги та іменування
Розміщення файлів (те, що аудитори люблять)
Тримайте /etc/nftables.conf крихітним. Він має підключати ваші реальні правила з директорії.
Це полегшує рев’ю, дозволяє часткову власність (мережна команда відповідає за файл NAT, платформа — за сервіси),
і запобігає мерджконфліктам, що виглядають як спагетті.
/etc/nftables.conf— точка входу/etc/nftables.d/00-defs.nft— константи, набори, мапи, імена інтерфейсів/etc/nftables.d/10-filter-base.nft— базові ланцюги: каркас input/output/forward/etc/nftables.d/20-filter-services.nft— дозволи для сервісів (вхід до локальних сервісів)/etc/nftables.d/30-filter-management.nft— SSH/моніторинг/конфіг управління/etc/nftables.d/40-nat.nft— NAT (тільки якщо потрібно)/etc/nftables.d/90-debug.nft— опціональні правила для дебагу (вимкнені за замовчуванням)
Угоди іменування, що витримують команди
Імена — не марнославство. Це єдиний спосіб швидко налагоджувати без пейджити автора правил трирічної давності.
Використовуйте передбачувані префікси:
- Таблиці:
inet filter,ip nat(іip6 natлише якщо потрібно) - Базові ланцюги:
input,output,forward - Користувацькі ланцюги:
in_sanity,in_established,in_allow_mgmt,in_allow_services,in_log_drop - Набори: префікс
set_і опис вмісту:set_mgmt_v4,set_bogon_v4,set_allowed_tcp_services - Мапи: префікс
map_і опис відображення:map_if_trust,map_service_ports
Чому «inet filter» — за замовчуванням
Використовуйте table inet filter для хостового фаєрволу, коли це можливо. Ви отримуєте одну політику для IPv4 і IPv6,
менше дубльованих правил і менше випадків «ми забули, що існує IPv6».
Але не будьте догматичними: NAT зазвичай лишається в сімействі ip/ip6.
Тримайте NAT окремо від filter. Змішувати їх — як зберігати бензопили в ящику з ножами.
Структура ланцюгів: маленькі, цілеспрямовані та нудні
Базові ланцюги повинні бути тонкими і в основному лише переходити в підланцюги. Підланцюги мають виконувати по одній задачі.
Коли ви так робите, лічильники та трасування набувають сенсу: ви бачите, на якому етапі трафік падає.
Приклад чистого набору правил (з include-файлами)
Це така структура, що лишається читабельною при 500 правилах, тому що кількість правил живе в наборах, а не в повторних рядках.
Вона також робить налагодження передбачуваним, бо потік пакетів розбитий на етапи.
cr0x@server:~$ sudo sed -n '1,160p' /etc/nftables.conf
#!/usr/sbin/nft -f
flush ruleset
include "/etc/nftables.d/*.nft"
cr0x@server:~$ sudo sed -n '1,260p' /etc/nftables.d/00-defs.nft
define IF_WAN = "eth0"
define IF_LAN = "eth1"
define IF_LOOP = "lo"
table inet filter {
set set_mgmt_v4 {
type ipv4_addr
flags interval
elements = { 198.51.100.10, 198.51.100.0/24 }
}
set set_mgmt_v6 {
type ipv6_addr
flags interval
elements = { 2001:db8:100::/64 }
}
set set_bogon_v4 {
type ipv4_addr
flags interval
elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16,
172.16.0.0/12, 192.0.2.0/24, 192.168.0.0/16, 198.18.0.0/15, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4 }
}
set set_allowed_tcp_services {
type inet_service
elements = { 22, 80, 443, 9100 }
}
set set_allowed_udp_services {
type inet_service
elements = { 123 }
}
}
cr0x@server:~$ sudo sed -n '1,260p' /etc/nftables.d/10-filter-base.nft
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iifname $IF_LOOP accept
jump in_sanity
jump in_established
jump in_allow_mgmt
jump in_allow_services
jump in_log_drop
}
chain forward {
type filter hook forward priority 0; policy drop;
jump fwd_sanity
jump fwd_established
jump fwd_allow
jump fwd_log_drop
}
chain output {
type filter hook output priority 0; policy accept;
jump out_sanity
}
chain in_sanity {
ct state invalid drop
ip saddr @set_bogon_v4 drop
ip6 saddr ::/128 drop
meta l4proto { tcp, udp, icmp, icmpv6 } accept
drop
}
chain in_established {
ct state { established, related } accept
}
chain in_log_drop {
limit rate 10/second burst 20 packets log prefix "nft in drop " flags all counter
drop
}
chain out_sanity {
ct state invalid drop
}
chain fwd_sanity {
ct state invalid drop
}
chain fwd_established {
ct state { established, related } accept
}
chain fwd_log_drop {
limit rate 10/second burst 20 packets log prefix "nft fwd drop " flags all counter
drop
}
}
cr0x@server:~$ sudo sed -n '1,260p' /etc/nftables.d/30-filter-management.nft
table inet filter {
chain in_allow_mgmt {
tcp dport 22 ip saddr @set_mgmt_v4 accept
tcp dport 22 ip6 saddr @set_mgmt_v6 accept
}
}
cr0x@server:~$ sudo sed -n '1,300p' /etc/nftables.d/20-filter-services.nft
table inet filter {
chain in_allow_services {
tcp dport { 80, 443 } accept
tcp dport 9100 accept
udp dport 123 accept
}
}
Зауваження щодо прикладу:
- Базові ланцюги короткі. Вони переходять у поетапні підланцюги, що робить потік пакетів легким для оповідання.
- Політика drop на input/forward. Output за замовчуванням — accept для більшості серверів; звужуйте його лише за потреби.
- Санітарний ланцюг строгий. Він рано скидає invalid і обробляє bogons. Також має «allow-list» протоколів L4, аби нісенітниця відкидалася швидко.
- Трафік керування ізольовано. Правила дозволу SSH в окремому ланцюгу з обмеженням джерел.
- Логування з обмеженням і пізнє. Логуєте лише drop наприкінці input/forward шляхів.
Жарт №2: «Тимчасові» правила фаєрвола мають той самий життєвий цикл, що й «тимчасові» таблиці в базі даних: вони переживуть вашу команду.
Практичні завдання: команди, виводи та що з цього вирішувати
Це реальні задачі, які ви виконуватимете на реальних машинах. Кожна включає команду, реалістично виглядаючий вивід,
що означає вивід і яке рішення прийняти.
Завдання 1: Підтвердити, що nftables активний і який завантажувач використовує ваша дистрибуція
cr0x@server:~$ systemctl status nftables
● nftables.service - nftables
Loaded: loaded (/lib/systemd/system/nftables.service; enabled; preset: enabled)
Active: active (exited) since Tue 2026-02-04 09:11:22 UTC; 2h 13min ago
Docs: man:nft(8)
Process: 612 ExecStart=/usr/sbin/nft -f /etc/nftables.conf (code=exited, status=0/SUCCESS)
Main PID: 612 (code=exited, status=0/SUCCESS)
Значення: Сервіс застосував набор правил і чисто вийшов (звично для nftables).
Рішення: Якщо сервіс неактивний/failed — спочатку виправте управління сервісом; налагоджувати правила без консистентного завантажувача — хаос.
Завдання 2: Здампіть активний набор правил так, як його бачить ядро
cr0x@server:~$ sudo nft list ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
iifname "lo" accept
jump in_sanity
jump in_established
jump in_allow_mgmt
jump in_allow_services
jump in_log_drop
}
chain in_allow_mgmt {
tcp dport 22 ip saddr @set_mgmt_v4 accept
tcp dport 22 ip6 saddr @set_mgmt_v6 accept
}
chain in_log_drop {
limit rate 10/second burst 20 packets log prefix "nft in drop " flags all counter
drop
}
}
Значення: Це правда. Не ваш файл, не ваш git — активний набор правил у ядрі.
Рішення: Якщо вивід не відповідає очікуваним include-файлам, у вас несумісність завантажувача або застаріле розгортання конфігурації.
Завдання 3: Перевірити синтаксис перед застосуванням (щоб не відрізати себе)
cr0x@server:~$ sudo nft -c -f /etc/nftables.conf
Значення: Відсутність виводу і код виходу 0 означає успішну перевірку/парсинг.
Рішення: Якщо валідація падає — спочатку виправте це; не «пробуйте на живому». Якщо застосовуєте віддалено, валідація — обов’язкова.
Завдання 4: Застосувати зміни атомарно і перевірити успіх
cr0x@server:~$ sudo nft -f /etc/nftables.conf
Значення: Знову ж таки, відсутність виводу зазвичай означає успіх.
Рішення: Негайно перевірте лічильники (Завдання 7) і досяжність сервісів. «Завантажено» ≠ «коректно».
Завдання 5: Показати handles ланцюгів (для точних видалень, аудитів і tooling)
cr0x@server:~$ sudo nft -a list chain inet filter input
table inet filter {
chain input { # handle 1
type filter hook input priority filter; policy drop;
iifname "lo" accept # handle 5
jump in_sanity # handle 6
jump in_established # handle 7
jump in_allow_mgmt # handle 8
jump in_allow_services # handle 9
jump in_log_drop # handle 10
}
}
Значення: Handles — стабільні ідентифікатори для правил в активному ruleset.
Рішення: Якщо треба хірургічно видалити одне правило під час інциденту — використовуйте handles, а не вгадування номерів рядків.
Завдання 6: Перелічити набори і підтвердити, що вони завантажені як треба
cr0x@server:~$ sudo nft list set inet filter set_mgmt_v4
table inet filter {
set set_mgmt_v4 {
type ipv4_addr
flags interval
elements = { 198.51.100.0/24, 198.51.100.10 }
}
}
Значення: Ваші дані ідентичності присутні. Інтервальні набори можуть коалесцирувати елементи.
Рішення: Якщо дозволи управління залежать від цього набору — підтвердьте його наявність перед тим, як звинувачувати «SSH-проблеми» в мережі.
Завдання 7: Перевірити лічильники, щоб побачити, що реально влучається
cr0x@server:~$ sudo nft list chain inet filter in_log_drop
table inet filter {
chain in_log_drop {
limit rate 10/second burst 20 packets log prefix "nft in drop " flags all counter packets 41 bytes 2870
drop
}
}
Значення: 41 пакет влучили в drop-логер. Це не теоретично — щось реально відкидають.
Рішення: Якщо лічильники зростають після деплою — бісектуйте зміни політики. Якщо лічильники нульові, але користувачі скаржаться — проблема в іншому (маршрутизація, застосунок, upstream ACL).
Завдання 8: Перевірити поведінку conntrack (часте джерело багів «працює раз і все»)
cr0x@server:~$ sudo conntrack -S
cpu=0 found=18231 invalid=12 ignore=0 insert=40121 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=0
Значення: У вас є invalid пакети (12). Деякі invalid нормальні в інтернеті; великі числа вказують на асиметричну маршрутизацію або зламаний offload.
Рішення: Якщо invalid швидко зростає — дослідіть симетрію маршрутизації і налаштування offload; не просто «дозволяйте invalid», щоб графіки заспокоїлися.
Завдання 9: Використайте nft monitor під час вікна змін
cr0x@server:~$ sudo nft monitor
add rule inet filter in_allow_services tcp dport 8443 counter accept
Значення: Живий вигляд правил, що додаються/видаляються. Чудово для підтвердження, що автоматика зробила те, що заявляє.
Рішення: Якщо бачите неочікувану скакання — зупиніть і перевірте інструментацію деплоя; CM-петлі можуть трясти ваш ruleset.
Завдання 10: Прослідкуйте шлях пакета (найшвидший спосіб знайти «яке правило його з’їло»)
cr0x@server:~$ sudo nft add rule inet filter input meta nftrace set 1
cr0x@server:~$ sudo nft monitor trace
trace id 3c2a inet filter input packet: iif "eth0" ip saddr 203.0.113.55 ip daddr 198.51.100.20 tcp sport 51234 tcp dport 22
trace id 3c2a inet filter input rule jump in_sanity
trace id 3c2a inet filter in_sanity verdict accept
trace id 3c2a inet filter input rule jump in_established
trace id 3c2a inet filter input rule jump in_allow_mgmt
trace id 3c2a inet filter in_allow_mgmt verdict drop
Значення: Ця трасування показує шлях оцінювання. Пакет не в наборі джерел управління, тому не матче accept-правила і падає пізніше.
Рішення: Або додати джерело до set_mgmt_v4 (якщо легітимно), або сказати користувачу «ні, це навмисно».
Завдання 11: Дивитися логи ядра на предмет rate-limited drop-ів фаєрвола
cr0x@server:~$ sudo journalctl -k -n 5
Feb 04 11:22:19 server kernel: nft in drop IN=eth0 OUT= MAC=52:54:00:aa:bb:cc SRC=203.0.113.55 DST=198.51.100.20 LEN=60 TOS=0x00 PREC=0x00 TTL=49 ID=53113 DF PROTO=TCP SPT=51234 DPT=22 WINDOW=64240 RES=0x00 SYN URGP=0
Feb 04 11:22:20 server kernel: nft in drop IN=eth0 OUT= MAC=52:54:00:aa:bb:cc SRC=203.0.113.8 DST=198.51.100.20 LEN=52 TOS=0x00 PREC=0x00 TTL=51 ID=0 DF PROTO=TCP SPT=60211 DPT=443 WINDOW=1024 RES=0x00 SYN URGP=0
Значення: Ваш префікс логування працює, і ви можете зіставити знімки з скаргами на сервіси.
Рішення: Якщо легітимний трафік відкидають — виправте allow-правила. Якщо бачите сміття — тримайте логування мінімальним і подумайте про фільтрацію вгорі (upstream).
Завдання 12: Підтвердити, що ви не забули IPv6
cr0x@server:~$ ip -6 addr show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
inet6 2001:db8:200::20/64 scope global
valid_lft forever preferred_lft forever
Значення: Хост має глобальну IPv6-адресу. Якщо ви писали лише IPv4-правила, ви могли випадково відкрити сервіси по v6 — або навпаки заблокувати їх.
Рішення: Використовуйте inet таблицю для filter, підтвердіть v6 джерела в наборах управління і явно дозволяйте/забороняйте сервіси по IPv6 як потрібно.
Завдання 13: Переконатися, що порт справді слухає, перш ніж звинувачувати фаєрвол
cr0x@server:~$ sudo ss -lntp | head
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=811,fd=3))
LISTEN 0 4096 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1002,fd=6))
LISTEN 0 4096 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=1002,fd=7))
Значення: Сервіси слухають на IPv4. Якщо користувачі не можуть підключитись, може винен фаєрвол — або маршрутизація, security groups, upstream ACL.
Рішення: Якщо не слухає — виправте сервіс. Якщо слухає — продовжуйте з лічильниками/трасуванням nft і перевірками upstream.
Завдання 14: Виявити роздування правил підрахунком рядків і пошуком дублікатів
cr0x@server:~$ sudo nft list ruleset | wc -l
892
Значення: Кількість рядків — грубий метрик, але це індикатор. Якщо ви очікували 250 рядків, але отримали 892 — ймовірно дублювання правил або розгортання згенерованого контенту.
Рішення: Перетворіть повторювані літерали в набори/мапи, розділіть include-файли за доменом і припиніть генерувати майже-двійники в автоматиці.
Завдання 15: Підтвердити, що offload/fast path не позбавляє вас видимості
cr0x@server:~$ ethtool -k eth0 | egrep 'gro|gso|tso|rx-checksumming|tx-checksumming'
rx-checksumming: on
tx-checksumming: on
tcp-segmentation-offload: on
generic-segmentation-offload: on
generic-receive-offload: on
Значення: Offload-и увімкнені. Це нормально, але в деяких середовищах conntrack і певні інкапсуляції поводяться дивно.
Рішення: Якщо бачите високий рівень invalid conntrack або дивні трасування — під час вікна техобслуговування протестуйте відключення конкретних offload-ів, а не робіть це як випадкову профілактику.
Плейбук швидкої діагностики
Коли ви під тиском, ви не «переглядаєте набір правил». Ви запускаєте короткий цикл, який швидко знаходить вузьке місце.
Ось швидка послідовність, що працює для більшості випадків «трафік блокується» або «трафік повільний».
Перше: доведіть сервіс і сокет
- Перевірте, чи процес слухає очікувану IP/порт (
ss -lntp). - Підтвердьте локальну досяжність (curl до localhost або
nc -vz 127.0.0.1 PORT, якщо застосовно). - Підтвердьте, що слухає на IPv6, якщо клієнти використовують v6 (
ss -lntp | grep '\[::\]:').
Якщо сервіс не слухає — фаєрвол невинний. Вважайте невинність цінним активом.
Друге: доведіть, що пакети доходять до хоста
- Зробіть захоплення на інтерфейсі на короткий проміжок (
tcpdump -ni eth0 port 443). - Якщо нічого не приходить — досліджуйте маршрутизацію, upstream фаєрвол, здоров’я load balancer-ів або security groups.
Третє: доведіть, що nftables — точка прийняття рішення
- Перевірте лічильники на ваших drop/log ланцюгах (
nft list chainз лічильниками). - Увімкніть короткострокове трасування через
nftraceіnft monitor trace, щоб знайти точний етап. - Підтвердіть, що набори/мапи містять очікувані елементи (IP-адреси управління, порти сервісів).
Четверте: ізолюйте конкретний розрив політики
- Це новий трафік чи існуючі з’єднання? Якщо існуючі потоки працюють, а нові падають — ймовірно проблема з conntrack або порядком нових правил.
- Чи це тільки IPv4, тільки IPv6, чи обидва? «Обидва» часто означає політику ланцюга; «один» — відсутні правила сімейства або неправильне використання inet.
- Чи є NAT? Якщо так, перевіряйте таблиці NAT окремо; не женіться за фільтром, якщо проблема в NAT.
П’яте: виправляйте з мінімальним радіусом ураження
- Краще додати до існуючого набору/мапи, ніж створювати одноразове правило.
- Краще додати allow-правило в правильний виділений ланцюг (mgmt vs services), а не в базовий ланцюг.
- Перезавантажуйте атомарно і перевіряйте лічильники та синтетичний тест.
Поширені помилки: симптом → корінь → виправлення
1) Симптом: SSH працює з офісу, але не через VPN
Корінь: Дозвіл управління прив’язаний до набору джерел, який не включає egress-діапазони VPN, або VPN використовує IPv6, а ви дозволяли лише v4.
Виправлення: Додайте egress-підмережу VPN до set_mgmt_v4/set_mgmt_v6. Перевірте трасування, потім перезавантажте атомарно.
2) Симптом: «Працювало після перезавантаження, потім зламалось»
Корінь: Conntrack дозволяв established потоки, але нові потоки потрапили під нове deny-правило, або тимчасові зміни (DHCP, динамічні IP) вивели клієнтів з дозволених наборів.
Виправлення: Перевірте розміщення ct state; тримайте established,related рано. Для динамічних IP використовуйте стабільну ідентичність (pool VPN, bastion), а не випадкові діапазони офісних ISP.
3) Симптом: IPv6-експозиція або раптовий IPv6-аутаж
Корінь: Ви написали лише v4-правила, тоді як хост має глобальний v6, або використали ip таблицю для фільтрування, думаючи, що вона покриває обидві сімейства.
Виправлення: Використовуйте table inet filter для хостового фаєрвола. Явно дозволяйте/забороняйте сервіси по v6. Перевірте через ip -6 addr і nft list ruleset.
4) Симптом: Високе CPU на зайнятому edge-пристрої після «покращення логування»
Корінь: Логування занадто рано в ланцюгу, логування accept-ів або відсутність rate-limit призводять до накладних витрат на логування в ядрі та користувацькому просторі.
Виправлення: Логуйте лише фінальні drop-и, обмежуйте швидкість і використовуйте лаконічні префікси. Якщо потрібна телеметрія, використовуйте лічильники і трасування селективно.
5) Симптом: Пакети відкидаються випадково під час високого трафіку
Корінь: Тиск на таблицю conntrack, стрибки invalid стану або асиметрична маршрутизація, що робить established потоки виглядати invalid.
Виправлення: Перевірте статистику conntrack (conntrack -S), забезпечте симетрію маршрутизації і не робіть «accept invalid» як пластир. Дослідіть offload та взаємодію інкапсуляцій.
6) Симптом: Автоматизація постійно «фіксує» вашу ручну екстрену зміну
Корінь: Config management повторно застосовує бажаний стан, не визнаючи інцидентні правки.
Виправлення: Під час інциденту: швидко оновіть репозиторій як джерело істини (навіть hotfix-галузь), або тимчасово призупиніть роль nftables на цьому хості. Після інциденту: закодуйте екстрену зміну належним чином (ідеально — як елемент набору, а не ad-hoc правило).
7) Симптом: NAT працює для деяких хостів, але не для інших
Корінь: NAT та фільтр переплутані в заплутаний спосіб; forward-ланцюг дозволяє трафік, але postrouting masquerade відсутній або занадто вузький.
Виправлення: Розділіть NAT у ip nat (і ip6 nat, якщо треба). Перевіряйте через захоплення пакетів і лічильники. Узгодьте forward-allow та NAT за ідентичністю інтерфейсу/підмережі.
Три корпоративні міні-історії (бо ви впізнаєте запах)
Міні-історія 1: Інцидент через неправильне припущення
Середня SaaS-компанія перейшла від старих iptables-скриптів до nftables через «прості транслейти».
Команда припустила, що порядок старих правил не важливий, бо «це просто allow-листи».
Також вирішили, що IPv6 неактуальний, бо їхній load balancer термінує з’єднання по IPv4.
Новий ruleset пішов у продакшн у тихий полудень. Він пройшов базовий smoke-test:
веб-сервіс доступний, і SSH з bastion працював. Усім здалося, що тепер вони зрілі.
Через дві години моніторинг показав спорадичні відмови на підмножині нод в одному регіоні.
Справжня проблема: ті ноди віддавали перевагу IPv6 для внутрішнього service discovery. Фаєрвол мав ip family filter table,
і IPv6-трафік йшов по майже пустому шляху. Деякі сервіси випадково відкрилися; інші мовчки блокувалися,
залежно від того, який процес прив’язався до [::].
Налагодження тривало довше, ніж треба, бо ruleset не був поетапним.
Не було «ланцюга управління», «ланцюга сервісів», лише довгий список змішаних правил. Трасування показало б розрив за хвилини,
але ніхто не мав робочого процесу трасування або безпечного debug include.
Виправлення було нудним: консолідувати хостовий фаєрвол у table inet filter, поетапити потік ланцюгів
і зробити IPv6 явним рішенням, а не випадковим побічним ефектом.
Найкорисніший висновок постмортему: «IPv6 — це не фіча. Це те, що вже існує».
Міні-історія 2: Оптимізація, що відгукнулась невдачею
Фінансова команда з високим трафіком API хотіла витиснути латентність.
Хтось помітив «занадто багато правил» і вирішив стиснути фаєрвол, перемістивши купу логіки в хитрі мапи,
зробив агресивні ранні drop-и і логував кожен reject для «кращої безпеки».
На папері все виглядало чудово: менше рядків і багато структурованих матчів.
На практиці вони отримали ruleset, який ніхто не міг прочитати під час інциденту.
«Хитра мапа» кодувала кілька поведінок (allow, drop, log) так, що потрібно було уявний компілятор, щоб зрозуміти.
Відгук проявився під навантаженням: обсяг логів зріс, ядро витрачало більше часу, а SIEM-пайплайн почав відставати.
Під тиском хтось відключив логування повністю — видаливши спільний ланцюг, який використовувався в трьох місцях — бо структура не відокремлювала обов’язки.
Це позбавило видимості саме тоді, коли вона була потрібна.
Відновлення означало редизайн: мапи використовувалися лише для ідентичності (групи портів і джерел),
поведінка лишилася в явних ланцюгах, а логування перемістилось у фінальні drop-ланцюги з жорстким rate-limit.
Латентність трохи покращилась, але справжній виграш — операційний: наступний інцидент зайняв 15 хвилин замість півдня.
Міні-історія 3: Нудна, але правильна практика, що врятувала день
Велике підприємство мало тисячі Linux VM з nftables, керованими CM.
Їхня політика безпеки змінювалася щоквартально, і кожен квартал була побоювання: «ця зміна нас відрізає».
Одна команда тихо впровадила практику, яку ніхто не святкував: кожна зміна nftables мала бути валідувана через nft -c,
розгорнута атомарно і перевірена лічильниками плюс один синтетичний тест з відомого probe-хоста.
Місяцями потому прийшла термінова зміна в пʼятницю ввечері (звісно).
Нова політика випадково прибрала потрібний UDP-порт для синхронізації часу на підмножині хостів.
Зміна розгорнулась, і за хвилини їхній pipeline верифікації зафіксував сплеск лічильників у drop-ланцюгу і невдачі синтетичних тестів.
On-call не треба було гадати. Вони мали поетапний ruleset, тож лічильники прямо вказали на in_allow_services, де бракувало UDP 123.
Вони оновили набір портів, перевірили, і атомарно перезавантажили. Простій був обмежений і локалізований.
Ніхто не писав героїчних Slack-повідомлень. Ось як виглядає хороша інженерія.
Урок: найцінніша властивість фаєрвола — не вираз матчів. Це дисциплінований процес змін, що припускає втомлених людей.
Контрольні списки / покроковий план
Покроково: побудувати читабельний набір правил, що масштабується
- Виберіть сімейства таблиць: Використовуйте
inetдля filter, окремоip/ip6для NAT, якщо потрібно. - Визначте інтерфейси одного разу: Використовуйте
define IF_WAN,IF_LANтощо. Уникайте магічних рядків у правилах. - Створіть набори ідентичності: джерела управління, джерела моніторингу, внутрішні підмережі, дозволені порти сервісів.
- Створіть поетапні ланцюги: sanity → established → management → services → log/drop.
- Тримайте базові ланцюги тонкими: Базові ланцюги мають переважно робити jump; не ховайте політику в них.
- Логуйте лише в кінці шляху: Один drop-логер на шлях (input/forward), з rate-limit і сталими префіксами.
- Примусьте порядок: Переконайтесь, що
ct state established,relatedрозташовано рано. invalid — теж рано. - Використовуйте атомарні перезавантаження: Завжди деплойте через
nft -fпісляnft -c. - Перевіряйте лічильниками: Підтвердіть, що очікувані ланцюги інкрементуються під трафіком; стежте, щоб лічильники drop були розумними.
- Тримайте інструменти для дебагу під рукою: Майте include для дебагу, вимкнений за замовчуванням, який можна увімкнути в екстрених випадках (trace, тимчасове логування).
Контрольний список: безпека перед зміною для віддалених хостів
- Є позасітковий доступ (console/ILO/IPMI/serial) або відомий перевірений bastion-шлях.
nft -c -f /etc/nftables.confпроходить успішно.- У вас є метод відкату: остання відома добра конфігурація доступна локально.
- Ви не змішуєте «рефактор NAT» з «відкрити один порт».
- У вас готова команда для живої перевірки (curl, nc або моніторинг-проба).
Контрольний список: постійна гігієна (те, що тримає 500 правил читабельними)
- Кожне нове allow-правило потрапляє в правильний ланцюг (mgmt vs services vs transit).
- Кожен повторюваний літерал (одна й та сама підмережа/група портів) стає набором після двох ітерацій.
- Кожне лог-правило має rate-limit і узгоджений префікс.
- Зміни ruleset переглядаються з думкою «історія пакета»: чи може читач переказати шлях за 60 секунд?
- Аудит щоквартально: очищайте мертві порти, мертві джерела, зводьте дублі в набори.
FAQ
1) Чи використовувати policy drop для ланцюга чи явне фінальне drop-правило?
Для базових ланцюгів, як input і forward, політика ланцюга drop — чисто й очевидно.
Все ж тримайте явний in_log_drop ланцюг, який логуватиме і скидати — бо політика drop сама по собі не логуватиме.
2) Чому не логувати кожен drop у кількох місцях?
Бо ви потонете. Централізуйте логування в кінці шляху, обмежуйте швидкість і використовуйте лічильники в інших місцях.
Якщо потрібна деталізація — увімкніть тимчасово трасування на короткий проміжок.
3) Чи завжди набори швидші?
Зазвичай так — особливо для довгих списків IP/портів. Але більший виграш — у підтримуваності.
Набори також дозволяють оновлювати членство без переписування логіки правил.
4) Куди помістити фільтрацію bogon-ів?
Помістіть її в санітарний ланцюг рано в input (і в forward, якщо ви маршрутизуєте).
Тримайте список як набір і періодично переглядайте. Не блоковуйте RFC1918 на внутрішніх інтерфейсах, якщо вам не подобаються самостійнозавдані відмови.
5) Як уникнути випадкового блокування DNS/NTP/monitoring egress?
Почніть з output policy accept для більшості серверів.
Якщо треба обмежувати egress — робіть це окремим проєктом з повною картою залежностей і гарною видимістю.
Блокування egress без інвентаризації — хороший спосіб дізнатися, скільки «опціональних» залежностей у вас є.
6) Який найбезпечніший спосіб міграції з iptables на nftables?
Не робіть сліпий переклад. Виражайте намір заново, використовуючи набори і поетапні ланцюги.
Запустіть бічний режим у контрольованому середовищі, де можливо, перевірте тестами трафіку, а потім переключіться атомарним завантаженням.
7) Чи ставити доступ управління і доступ до сервісів в один ланцюг?
Ні. Розділяйте їх. Управління — це control-plane, сервіси — data-plane.
Вони мають різні обмеження джерел, різні вимоги до аудиту і різні реакції на інциденти.
8) Як налагоджувати «деякі клієнти можуть підключитись, інші — ні»?
Перевірте, чи не мають невдалих клієнтів спільного діапазону джерел, що не включений у ваші набори.
Використайте nft monitor trace, щоб побачити точну невідповідність. Підтвердіть, чи клієнти використовують IPv6.
9) Чи nftables за замовчуванням stateful?
Ні. Ви вирішуєте, як використовувати conntrack з матчами ct state.
Більшість хостових фаєрволів у продакшні дозволяють established,related рано і трактують invalid як drop.
10) Як зберегти читабельність ruleset, коли продуктові команди вимагають «ще одного винятку»?
Змушуйте винятки ставати даними (елементи набору), а не логікою (нові ad-hoc правила).
Якщо виняток змінює поведінку, йому потрібен власний ланцюг з іменем, що каже, що це таке.
Сором — недооцінений інструмент управління.
Наступні кроки, що не зіпсують ваш вікенд
Читабельний nftables ruleset — це не стильова примха. Це функція надійності.
При 500 правилах ви боретеся не з пакетами — ви боретеся з ентропією.
Зробіть наступне:
- Розбийте моноліт на include-файли: defs, base, mgmt, services, nat, debug.
- Перетворіть повторювані списки IP/портів у набори. Тримайте ідентичність у наборах; поведінку — у ланцюгах.
- Поетапте шлях пакета: sanity → established → allow mgmt → allow services → log/drop.
- Впровадьте цикл змін:
nft -c→ атомарне завантаження → перевірка лічильників → трасування лише за потреби. - Запишіть свої конвенції іменування і дотримуйтеся їх у рев’ю. Ви будуєте інструмент для on-call, а не поему.
Якщо нічого іншого не зробите: зробіть історію пакета очевидною. Коли дзвонить пейджер, ясність — єдиний показник продуктивності, що має значення.