Debian 13: nftables + Docker — припиніть несподіванки від автоматичних правил (і виправте це) (випадок №39)

Було корисно?

Ви налаштували чистий брандмауер nftables на Debian 13. Ви протестували його. Вас це тішить. Потім ви встановлюєте Docker і — загадковим чином — порти з’являються відкритими, пересилання поводиться інакше, а пакети починають йти шляхами, які ви не авторизували.

Це одна з тих операційних неприємностей, яка може перерости в інцидент безпеки, якщо продовжувати ставитися до неї як до «просто мережевих справ». Docker має власні припущення. nftables має власні припущення. Ваша команда відповідності має свої припущення. Лише одна із цих сторін платить вам зарплату.

Ментальна модель: хто володіє брандмауером?

На Debian 13 nftables є основним інтерфейсом брандмауера, але ядро все ще надає гачки netfilter, які можуть програмувати кілька інструментів. Docker за замовчуванням також програмує ці гачки. Залежно від пакування та конфігурації, він може робити це через iptables-nft (синтаксис iptables, бекенд nft), тоді як ви пишете нативні правила nft. Ті самі гачки, різні інструменти, спільний стан. Ось як у вас з’являється «працюючий» брандмауер, що поводиться як імпровізаційне шоу.

Найбільша помилка — думати в термінах «мій файл правил брандмауера». Системі байдуже до вашого файлу. Системі важливий активний набір правил у ядрі. Docker динамічно змінює цей активний набір. Якщо ви хочете передбачуваності, ви маєте вирішити:

  • Або дозволити Docker управляти NAT і базовим пересиланням, а ви контролюєте це через правильні точки застосування (особливо гачок DOCKER-USER),
  • Або відключити втручання Docker у брандмауер і взяти на себе повну відповідальність за NAT/forward/published порти самостійно.

Напівзаходи — це місце, де команди безпеки йдуть плакати.

Одна цитата, яку приклеювали до кількох ноутбуків в режимі on-call:
Сподівання — не стратегія. — General Gordon R. Sullivan

Дев’ять фактів, які припинять здогадки

  1. nftables давно замінило iptables як «наступника», проте iptables все ще широко використовується як сумісний інтерфейс, часто відображаючись у nft під капотом.
  2. Docker історично використовував iptables напряму для налаштування NAT і публікації портів. Ця спадщина досі проявляється, навіть коли ви обрали nftables як інтерфейс.
  3. Бекенд iptables-nft означає, що команди iptables можуть створювати правила nftables. Це зручно, але також означає, що два різні синтаксиси пишуть у один і той самий набір правил.
  4. «Опубліковані порти» Docker — це не просто сокети, що слухають; зазвичай це правила DNAT плюс зміни у фільтрі. Контейнер може бути доступним, навіть якщо нічого не прив’язано на хості так, як ви очікуєте.
  5. Ланцюг DOCKER-USER існує спеціально для того, щоб оператори могли застосовувати політику до того, як Docker застосує власні правила accept. Якщо ви ним не користуєтесь, ви втрачаєте контроль.
  6. NAT і фільтрація — це різні таблиці. Блокування в filter без розуміння nat може призвести до ситуації «блоковано, але якось працює» або «відкрито, але недосяжно».
  7. Пересилання залежить від net.ipv4.ip_forward та суміжних sysctl; Docker може ввімкнути поведінку, яку ваша базова жорстка конфігурація вимкнула, і навпаки.
  8. Брандмауери оцінюються в порядку викликів (hook order). Якщо Docker вставляє правила раніше за ваші (або з вищим пріоритетом), ваші ретельно підготовлені drop можуть ніколи не виконатися.
  9. За замовчуванням Debian може бути оманливо тихим: ви можете мати «встановлений nftables», але «сервіс nftables неактивний», і при цьому інші компоненти все одно вставляють активні правила в ядро.

Жарт №1: Брандмауери — як офісна політика: все добре, поки хтось тихо не змінить ланцюг командування.

Що фактично відбувається в Debian 13 при запуску Docker

1) Docker створює мости та простори імен

Docker зазвичай створює Linux-міст (часто docker0) та один або більше користувацьких мостів. Контейнери розміщуються в мережевих просторах імен з veth-парами, підключеними до цих мостів. Ця частина проста.

2) Docker програмує netfilter, щоб усе «просто працювало»

«Просто працювало» означає:

  • Контейнери можуть виходити в інтернет через маскарад (SNAT) при egress.
  • Опубліковані порти на хості перенаправляються (DNAT) на IP контейнера.
  • Пересилання між інтерфейсами хоста та мостом контейнера дозволяється.

Точна реалізація залежить від того, чи Docker використовує сумісність iptables і чи система працює зі старим iptables або з iptables на базі nft. На Debian 13 слід припускати бекенд nft, якщо ви явно не примусили legacy.

3) Ви бачите ланцюги nftables, які ви не писали

Типові артефакти включають ланцюги на кшталт DOCKER, DOCKER-USER і часом правила в nat для DNAT/MASQUERADE. Імена можуть відрізнятися; шаблон однаковий. Якщо це вас дивує — ви не одні. Але «здивування» не є прийнятним робочим станом у продакшені.

4) «Сюрприз» зазвичай не означає, що Docker злий

Це Docker, який оптимізує досвід розробника, тоді як ви оптимізуєте передбачувані межі безпеки. Цілі не є ворогами, але вони потребують явного дизайну. Ви не виграєте, сподіваючись, що Docker припинить так робити.

Швидкий план діагностики (перший / другий / третій)

Коли порт здається відкритим несподівано або трафік контейнера обходить вашу політику, не починайте переписувати правила. Почніть з видимості. Потім вирішіть, хто має володіти чим.

Спершу: підтвердіть, що активне (nft ruleset + погляд iptables)

  • Зробіть дамп активного nft ruleset і шукайте Docker-ланцюги та точки переходу.
  • Перевірте правила iptables через шар сумісності (це може показати, як Docker вставив правила).

По-друге: простежте шлях пакета для конкретного симптому

  • Чи це вхідний трафік до опублікованого порту? Це DNAT + filter/forward.
  • Чи це egress контейнера? Це masquerade + forward + sysctl.
  • Чи це контейнер-до-контейнера? Це фільтрація мосту і еквіваленти ланцюга FORWARD.

По-третє: вирішіть модель і застосуйте її

  • Якщо Docker керує iptables/nft: зафіксуйте політику в DOCKER-USER, тримайте свої nft-правила сумісними, уникайте конфліктних гачків.
  • Якщо ви керуєте всім: відключіть iptables у Docker, реалізуйте власні nat/filter правила для мостів і опублікованих портів, і прийміть операційні витрати.

Більшість продакшн-команд повинні почати з «Docker керує NAT, але ми контролюємо політику через DOCKER-USER», якщо тільки у них немає суворих вимог сегментації мережі та персоналу для її підтримки.

Практичні завдання (команди, виводи, рішення)

Це польові завдання. Виконайте їх на хості. Читайте вивід. Приймайте рішення. Повторюйте, поки система не стане нудною.

Завдання 1 — Підтвердити стан Docker та nftables

cr0x@server:~$ systemctl status docker --no-pager
● docker.service - Docker Application Container Engine
     Loaded: loaded (/lib/systemd/system/docker.service; enabled; preset: enabled)
     Active: active (running) since Mon 2025-12-30 09:14:22 UTC; 2h 1min ago
       Docs: https://docs.docker.com
   Main PID: 1423 (dockerd)
      Tasks: 22
     Memory: 154.2M
        CPU: 1min 12.553s
     CGroup: /system.slice/docker.service
             └─1423 /usr/bin/dockerd -H fd://

Що це означає: Docker активний і може зараз вставляти правила.

Рішення: Припустіть, що брандмауер не «статичний». Перейдіть до перевірки активного набору правил, а не конфігураційних файлів.

cr0x@server:~$ systemctl status nftables --no-pager
○ nftables.service - nftables
     Loaded: loaded (/lib/systemd/system/nftables.service; enabled; preset: enabled)
     Active: inactive (dead)

Що це означає: сервіс nftables може не завантажувати вашу заплановану базу під час завантаження, але ядро все одно може мати правила, вставлені іншими компонентами.

Рішення: Якщо ви очікуєте базовий фаєрвол, виправте стан сервісу пізніше. Спершу перевірте живий набір правил.

Завдання 2 — Зробити дамп активного nftables ruleset (істина)

cr0x@server:~$ sudo nft list ruleset | sed -n '1,120p'
table inet filter {
	chain input {
		type filter hook input priority filter; policy drop;
		ct state established,related accept
		iif "lo" accept
		tcp dport 22 accept
	}
	chain forward {
		type filter hook forward priority filter; policy drop;
		jump DOCKER-USER
		jump DOCKER-FORWARD
	}
	chain output {
		type filter hook output priority filter; policy accept;
	}
	chain DOCKER-USER {
		return
	}
	chain DOCKER-FORWARD {
		iif "docker0" accept
		oif "docker0" accept
	}
}

Що це означає: Всередині вашої inet filter таблиці існують ланцюги, пов’язані з Docker, і forward виконує перехід до них. Навіть якщо ви їх не створювали.

Рішення: Якщо ви хочете контролювати політику, лишивши Docker працездатним, DOCKER-USER — ваша точка примусового застосування.

Завдання 3 — Перевірте таблицю nat nft на DNAT/MASQUERADE

cr0x@server:~$ sudo nft list table ip nat
table ip nat {
	chain PREROUTING {
		type nat hook prerouting priority dstnat; policy accept;
		iifname != "docker0" tcp dport 8080 dnat to 172.18.0.5:80
	}
	chain POSTROUTING {
		type nat hook postrouting priority srcnat; policy accept;
		oifname != "docker0" ip saddr 172.18.0.0/16 masquerade
	}
}

Що це означає: Порт 8080 на хості DNAT’иться на контейнер, і egress контейнерів маскарадиться.

Рішення: Якщо ви бачите DNAT-правила, які не авторизували, знайдіть контейнери з опублікованими портами і вирішіть, чи дозволяти таку публікацію взагалі.

Завдання 4 — Визначити опубліковані порти з перспективи Docker

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
NAMES        IMAGE           PORTS
web-01       nginx:alpine    0.0.0.0:8080->80/tcp
redis-01     redis:7         6379/tcp

Що це означає: web-01 опублікований на всіх інтерфейсах хоста на порті 8080. Redis не опублікований (лише в межах контейнера).

Рішення: Якщо 0.0.0.0 неприпустимий, обмежте до 127.0.0.1:8080:80 або конкретної IP-адреси інтерфейсу, або блокуйте в DOCKER-USER.

Завдання 5 — Підтвердити, який бекенд iptables активний

cr0x@server:~$ sudo update-alternatives --display iptables | sed -n '1,40p'
iptables - auto mode
  link best version is /usr/sbin/iptables-nft
  link currently points to /usr/sbin/iptables-nft
  link iptables is /usr/sbin/iptables
/usr/sbin/iptables-nft - priority 20
/usr/sbin/iptables-legacy - priority 10

Що це означає: Команди iptables відображаються в правила nft (бекенд nft). Програмування iptables Docker’ом потраплятиме в nftables.

Рішення: Не змішуйте iptables-legacy з нативними nft-правилами, якщо вам не подобається дебаг у паралельних всесвітах.

Завдання 6 — Подивитися правила iptables так, як їх бачить Docker (nft-backed)

cr0x@server:~$ sudo iptables -S | sed -n '1,80p'
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-USER
-N DOCKER-ISOLATION-STAGE-1
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A DOCKER-USER -j RETURN

Що це означає: Docker вставив стандартну «партитуру» мережевого ландшафту. Наявність DOCKER-USER — ваш шанс застосувати політику.

Рішення: Якщо ви зберігаєте Docker у ролі того, хто керує правилами, розміщуйте drop/accept у DOCKER-USER (або в еквівалентному nft-ланцюгу, якщо ви повністю нативні).

Завдання 7 — Довести, що хост реально слухає, або що це DNAT

cr0x@server:~$ sudo ss -lntp | awk 'NR==1 || $4 ~ /:8080$/'
State  Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0      4096   0.0.0.0:8080      0.0.0.0:*     users:(("docker-proxy",pid=2331,fd=4))

Що це означає: Процес (часто docker-proxy, залежно від налаштувань) прив’язаний до порту 8080; в інших налаштуваннях ви могли б не побачити слухача, бо використовується чистий DNAT.

Рішення: Якщо ви очікували «немає слухача хоста — значить закрито», виправте це припущення. Ви маєте перевіряти nat/filter правила також.

Завдання 8 — Перевірте sysctl, що контролюють пересилання і фільтрацію мостів

cr0x@server:~$ sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1

Що це означає: Пересилання увімкнено, і трафік мосту потрапляє на netfilter hooks. Це типово для Docker, але не завжди бажано в суворо контрольованих середовищах.

Рішення: Якщо трафік контейнерів не повинен маршрутизуватися між мережами, розгляньте вимкнення пересилання глобально (але розумійте, що мережі Docker зміняться) або ізолюйте через правила.

Завдання 9 — Знайдіть, який інтерфейс насправді отримує «сюрпризний» трафік

cr0x@server:~$ ip -brief addr
lo               UNKNOWN        127.0.0.1/8 ::1/128
ens3             UP             203.0.113.10/24 2001:db8:10::10/64
docker0          DOWN           172.17.0.1/16
br-2a1d3c4e5f6a  UP             172.18.0.1/16

Що це означає: Ваш публічний інтерфейс — ens3; Docker-мережі існують на br-....

Рішення: Для вхідної політики завжди міркуйте від інтерфейсу ingress (часто ens3) через nat PREROUTING і потім forward/filter.

Завдання 10 — Підтвердити, який контейнер має DNAT-ціль

cr0x@server:~$ docker inspect -f '{{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' web-01
/web-01 172.18.0.5

Що це означає: IP-адреса DNAT-цілі відповідає web-01.

Рішення: Якщо цей контейнер не повинен бути доступним з інтернету, виправте конфігурацію публікації і/або заблокуйте в DOCKER-USER.

Завдання 11 — Додати явний шлюз політики в DOCKER-USER (блокувати вхід, крім схвалених)

cr0x@server:~$ sudo iptables -I DOCKER-USER 1 -i ens3 -p tcp --dport 8080 -s 198.51.100.0/24 -j ACCEPT
cr0x@server:~$ sudo iptables -I DOCKER-USER 2 -i ens3 -p tcp --dport 8080 -j DROP
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -i ens3 -p tcp -s 198.51.100.0/24 --dport 8080 -j ACCEPT
-A DOCKER-USER -i ens3 -p tcp --dport 8080 -j DROP
-A DOCKER-USER -j RETURN

Що це означає: Вхід до опублікованого порту 8080 із публічного інтерфейсу тепер обмежений дозволеним CIDR, інакше відкидається, до того як Docker застосує власні accept правила.

Рішення: Якщо це виправляє розбіжність політик без порушення мережі контейнерів, ви підтвердили модель «Docker керує plumbing, ми примушуємо політику».

Завдання 12 — Зробити правила стійкими (не покладатися на ручні правки)

cr0x@server:~$ sudo install -d -m 0755 /etc/iptables
cr0x@server:~$ sudo sh -c 'iptables-save > /etc/iptables/rules.v4'
cr0x@server:~$ sudo head -n 20 /etc/iptables/rules.v4
*filter
:INPUT ACCEPT [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:DOCKER - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -i ens3 -p tcp -s 198.51.100.0/24 --dport 8080 -j ACCEPT
-A DOCKER-USER -i ens3 -p tcp --dport 8080 -j DROP
-A DOCKER-USER -j RETURN
COMMIT

Що це означає: Тепер у вас є довготривалий запис застосованих правил політики. Відновлення через systemd unit або інший механізм — на ваш вибір; головне, що «працювало у вівторок» не є стратегією конфігурації.

Рішення: Якщо в організації стандарт — нативний nftables, перекладіть ці правила в синтаксис nft і завантажуйте через сервіс nftables. Якщо стандарт — iptables-nft, зберігайте послідовність і автоматизуйте відновлення при завантаженні.

Завдання 13 — Якщо ви хочете, щоб Docker перестав вводити правила зовсім (режим високої церемонії)

cr0x@server:~$ sudo install -d -m 0755 /etc/docker
cr0x@server:~$ sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
  "iptables": false,
  "ip-forward": false
}
EOF
cr0x@server:~$ sudo systemctl restart docker
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Ports}}'
NAMES        PORTS
web-01       0.0.0.0:8080->80/tcp

Що це означає: Docker все ще повідомляє про опубліковані порти, але більше не встановлюватиме netfilter-правила, щоб зробити їх досяжними. Поки ви не додасте власні NAT/forward/filter правила і не налаштуєте sysctl, щось перестане працювати.

Рішення: Обирайте цей режим лише якщо готові реалізувати й підтримувати NAT та пересилання для кожної Docker-мережі, яку створюєте.

Завдання 14 — Перевірити конкретний шлях пакета за лічильниками (нативна видимість nft)

cr0x@server:~$ sudo nft -a list chain inet filter forward
table inet filter {
	chain forward { # handle 8
		type filter hook forward priority filter; policy drop;
		jump DOCKER-USER # handle 21
		jump DOCKER-FORWARD # handle 22
	}
}

Що це означає: Ви можете посилатися на handle правил, додавати лічильники і спостерігати збіги. Це спосіб припинити суперечки про «повинно» і перейти до вимірювання «є».

Рішення: Якщо ви не можете пояснити, який ланцюг обробляє ваш трафік, ще не змінюйте політику — спершу інструментуйте її.

Жарт №2: Якщо ви коли-небудь почуваєтеся непотрібним, згадайте, що існує запит на зміну брандмауера з відміткою «терміново», без вихідної IP-адреси і без порту.

Два розумні підходи (і один, що лише виглядає розумним)

Проєкт A (рекомендовано для більшості команд): Docker керує plumbing, ви застосовуєте політику в DOCKER-USER

Ви дозволяєте Docker створювати DNAT/MASQUERADE і тримаєте контейнери доступними за задумом. Потім додаєте явні allow/deny правила в DOCKER-USER на основі:

  • Ingress інтерфейсу (публічний проти приватного)
  • Порту призначення (опубліковані сервіси)
  • Джерела CIDR (адмінські мережі, VPN-діапазони, мережі партнерів)

Чому це працює: Генерація правил Docker складна і динамічна (контейнери стартують/зупиняються, мережі з’являються/зникають). Ваша політика відносно стабільна. Помістіть стабільну частину туди, де її завжди будуть оцінювати раніше.

Чого уникати: розкидати drop-правила в випадкові ланцюги і сподіватися, що вони перетнуть правила accept Docker. Це не інженерія; це враження.

Проєкт B (для суворих середовищ): відключити програмування фаєрвола Docker і керувати всім у nftables

Якщо у вас регуляторні обмеження або ви будуєте платформу, де Docker — «ще одне робоче навантаження», ви можете відключити iptables-поведінку Docker. Тоді:

  • Ви ввімкнете пересилання вибірково.
  • Створите nft nat правила для кожної підмережі мосту Docker.
  • Явно дозволите пересилання для опублікованих портів і необхідного egress.

Це життєздатно, але це праця. Вам потрібні тести, автоматизація і людина на виклику, яка зможе розібратися в потоках пакетів о 03:00.

Проєкт C (виглядає розумним, але ні): поєднувати нативні правила nftables з імпровізованими виправленнями iptables

Режим відмови: ви «виправляєте» проблему командою iptables поспіхом, потім пізніше «виправляєте» іншу проблему в синтаксисі nft, потім Docker оновлюється і перезаписує частини, і тепер набір правил — це багатошаровий торт жалю.

Оберіть одну контрольну площину для рукописної політики: або нативний nft з контрольованою конфігурацією Docker, або iptables-nft з контрольованим використанням ланцюгів. Узгодженість перемагає кмітливість.

Три корпоративні міні-історії (анонімізовані, правдоподібні, технічно точні)

Інцидент: неправильне припущення («Ми заблокували в INPUT, отже недоступно»)

Середня SaaS-компанія перенесла набір внутрішніх інструментів на VM-флот на базі Debian. Безпекова база: nftables з політикою INPUT за замовчуванням drop, SSH тільки з корпоративного VPN, і все інше закрито. Розгортання виглядало чисто.

Потім розробник розгорнув контейнерний адміністративний UI і опублікував його з -p 8443:443, щоб «просто протестувати». Ніхто не помітив, бо на хості не було звичного сервісу, що слухає 8443, і ланцюг INPUT залишився недоторканим: drop за замовчуванням, без дозволу для 8443. Усім було спокійно.

Через тиждень зовнішнє сканування помітили 8443 як відкритий. Інженер on-call зробив звичайну дію: перевірив ss -lntp, побачив пов’язаний із Docker слухач, знизив плечима і додав drop в INPUT. Сканування все одно показувало відкриття. Момент перейшов з «нормально» в «нас переслідують».

Корінна причина: трафік DNAT’ився в PREROUTING і проходив через FORWARD, а не INPUT. Прийняття у FORWARD від Docker або ваша занадто дозволяюча політика форварду дозволяли його.

Виправлення: застосувати політику в DOCKER-USER (відкидати вхід до опублікованих портів, крім VPN-діапазонів) і змінити публікацію контейнера, щоб прив’язуватися лише до VPN-інтерфейсу. Дія в постмортемі: припинити ставитися до INPUT як до єдиної межі брандмауера на хостах з контейнерами.

Оптимізація, що відкинулася назад: «Вимкнути docker-proxy і покладатися на чистий nft»

Команда платформи хотіла вигоди в продуктивності та кращої видимості. Вони вимкнули docker-proxy (поширене налаштування) і стандартизувалися на nftables. Вони чекали менше процесів, менше рухомих частин і простішого відкриття портів.

У стенді все виглядало швидше і «більш нативно для ядра». Потім в продакшені виникла дивна помилка: певні опубліковані порти були доступні з деяких мереж, але не з інших, а health check’і флікали лише для IPv6-клієнтів. Інженери витратили дні, сперечаючись, чи це поведінка балансувальника навантаження, ліміти conntrack чи поломка оновлення ядра.

Справжня проблема була в дрейфі політики: правила nft команди припускали, що слухаючі сокети репрезентують експозицію, але при вимкненому proxy експозиція була в основному поведінкою DNAT. Їхній моніторинг перевіряв ss на наявність слухачів і не помічав правил, що відкривали шляхи. IPv6-політика була неповною: NAT і фільтрація поводилися інакше між ip і inet таблицями.

Виправлення: розглядати публікацію портів як функцію брандмауера, а не процесну особливість. Моніторити різниці наборів правил nft (або принаймні кількість ланцюгів і вибрані правила), застосовувати політику через DOCKER-USER і явно продумувати поведінку IPv6 (або підтримувати її наскрізь, або цілеспрямовано вимкнути).

Оптимізація не була «неправильною». Вона була непризначеною складністю. Так «покращення продуктивності» стають «інцидентами доступності».

Нудна, але правильна практика, що врятувала день: дампи правил у інцидентних тікетах

Велика корпорація з консервативним процесом змін мала просте правило: кожен інцидент, пов’язаний із фаєрволом, мусить містити вкладення nft list ruleset, iptables -S і список портів docker ps, зроблені в момент впливу. Інженери буркотіли, бо це виглядало бюрократично.

Потім інтеграція з пристроєм вендора почала періодично падати. Трафік з діапазону IP партнера іноді доходив до контейнерного ендпоінта, а іноді тонув. Команда додатку звинувачувала партнера. Партнер звинувачував корпорацію. Всі вже збиралися ставити зустріч (традиційний спосіб вирішити втрату пакетів).

On-call дотримався нудного правила і прикріпив дампи. Рецензент помітив, що активний набір правил змінився після redeploy контейнера: з’явився новий користувацький міст, і Docker встав відповідні правила маскараду. Користувацькі drop-правила корпорації були прив’язані до імен інтерфейсів, які мінялися разом із мостом. Політика була правильною за задумом, але крихкою в реалізації.

Оскільки вони мали «до/після» дампи, гадати не довелося. Вони переписали політику так, щоб вона відповідала на діапазони адрес і використовувала DOCKER-USER для обмеження ingress замість прив’язки до ефемерних імен мостів. Виправили того ж дня без залучення десяти людей у планерку.

Нудна практика. Правильний результат. Оце й є робота.

Поширені помилки: симптоми → корінна причина → виправлення

1) «Порт відкритий, хоча INPUT — drop»

Симптоми: Зовнішнє сканування показує опублікований порт; у вашому nft input немає дозволу для нього.

Корінна причина: Трафік DNAT’иться в PREROUTING і проходить через FORWARD, а не INPUT. Прийняття у FORWARD від Docker або ваш помилково дозволяючий forward це допускає.

Виправлення: Застосуйте обмеження ingress у DOCKER-USER (переважно) або в хук forward перед тим, як Docker приймає трафік. Перевірте nat PREROUTING правила для цього порту.

2) «Я заблокував контейнер, але він все одно виходить в інтернет»

Симптоми: Ви додаєте drop у хостовий input-ланцюг; egress контейнера все одно працює.

Корінна причина: Egress контейнера — це пересланий трафік; INPUT тут неактуальний. Також egress часто маскарадується, ховаючи IP контейнера, якщо не робити відповідні матчі по інтерфейсу/підмережі.

Виправлення: Блокуйте у шляху forward на основі джерельної підмережі (діапазони мостів Docker) або IP контейнера, або використовуйте матч по iifname для docker-мостів. Віддавайте перевагу контрольованим мережам за додатком.

3) «Після увімкнення сервісу nftables мережа Docker зламалась»

Симптоми: Контейнери не можуть виходити; опубліковані порти перестають відповідати після перезавантаження.

Корінна причина: Сервіс nftables завантажує базу, яка очищує або перевизначає ланцюги, яких чекає Docker, або встановлює політику forward drop без надання потрібних переходів Docker.

Виправлення: Визначте модель власності. Якщо Docker керує правилами, не очищуйте таблиці, на які Docker покладається; вбудуйте потрібні переходи або тримайте політику в DOCKER-USER. Якщо ви володієте всім, відключіть iptables Docker і реалізуйте еквівалентні nat/forward правила.

4) «Правила виглядають правильно в nft, але iptables показує інше»

Симптоми: Дамп nft не відповідає тому, що стверджує інструмент на базі iptables, і навпаки.

Корінна причина: Змішування legacy iptables з nft-бекендом, або одночасна активність обох у плутаних варіантах, або припущення, що один інструмент показує все.

Виправлення: Стандартизуйтесь на iptables-nft, якщо мусите користуватися синтаксисом iptables. Уникайте legacy. Завжди перевіряйте з nft list ruleset, бо це стан, видимий ядром.

5) «IPv6 поводиться по-іншому (або загадково обходить обмеження)»

Симптоми: IPv4-обмеження працюють; IPv6-клієнти все ще можуть під’єднатися, або навпаки.

Корінна причина: Правила застосовано лише до ip (v4) таблиць, а не inet; або Docker/хост має IPv6 ввімкнений з різними ланцюгами і політиками.

Виправлення: Використовуйте table inet для фільтр-правил, коли хочете паритету, і явно вирішуйте, чи має Docker IPv6 бути ввімкненим. Тестуйте обидва стеки, не робіть припущень.

6) «Перезавантаження змінило все»

Симптоми: Поведінка фаєрвола відрізняється після рестарту; порти раптово відкриваються/закриваються.

Корінна причина: Невизначений порядок завантаження: nftables завантажується після Docker або навпаки, і один очищає/перезаписує інший; або правила додавалися вручну і ніколи не зберігалися.

Виправлення: Зробіть порядок завантаження явним через systemd залежності, збережіть правила належним чином і включіть перевірки в конфігураційне управління.

Контрольні списки / покроковий план

Контрольний список A — Хочете, щоб Docker працював далі, але хочете передбачувану безпеку

  1. Оберіть одну політичну площину: виберіть або нативний nftables зі стабільними ланцюгами, або iptables-nft для вставлення політик. Не імпровізуйте.
  2. Підтвердіть бекенд: переконайтеся, що iptables вказує на iptables-nft, а не legacy.
  3. Інвентаризуйте експозицію: перераховуйте опубліковані порти через docker ps і порівнюйте їх з nat-правилами nft.
  4. Застосуйте ingress-політику в DOCKER-USER: дозволяйте лише ті джерела, які маєте намір; інше відкидайте.
  5. Обробляйте IPv6 свідомо: дублюйте політику через inet таблиці або вимкніть IPv6 для Docker, якщо середовище цього не підтримує безпечно.
  6. Збережіть зміни: зберігайте джерела правил у CM і забезпечуйте їх застосування при завантаженні.
  7. Перевіряйте лічильниками: додайте лічильники до ключових правил і перевіряйте влучення під час тестів.
  8. Напишіть нотатку для операторів: «Опубліковані порти контролюються через DOCKER-USER; не відкривати порти в INPUT, очікуючи, що це спрацює.»

Контрольний список B — Хочете, щоб Docker перестав вводити правила (повна власність)

  1. Налаштуйте daemon.json Docker: відключіть iptables і визначіть ip-forward.
  2. Визначте адресну стратегію: закріпіть підмережі Docker bridge, щоб правила не ганялися за випадковістю.
  3. Напишіть nft nat правила: MASQUERADE для egress, DNAT для кожного дозволеного опублікованого порту.
  4. Напишіть nft filter правила: правила forward для встановлених потоків, дозволяйте лише необхідний ingress до контейнерів.
  5. Протестуйте поведінку після рестарту: перезапустіть Docker і перезавантажте хост; переконайтеся, що правила стабільні.
  6. Оновіть runbook-и: «Публікація порту в Docker нічого не робить, якщо не додано правила фаєрволу.»
  7. Автоматизуйте перевірку: CI або перевірка при завантаженні, що стверджує існування ключових ланцюгів і правильність політик.

Контрольний список C — Мінімальна політика, яка уникатиме сюрпризів (гарна стартова база)

  • За замовчуванням drop на хості INPUT.
  • За замовчуванням drop на FORWARD, якщо ви явно не дозволяєте пересилання Docker.
  • Явний ct state established,related accept на початку.
  • Явна політика в DOCKER-USER для опублікованих портів: дозволяти лише довірені CIDR.
  • Обмежуйте опубліковані порти до конкретних інтерфейсів, коли можливо (прив’язуйте до VPN IP, а не до 0.0.0.0).

Питання та відповіді

1) Чому Docker торкається мого фаєрвола взагалі?

Тому що контейнери за замовчуванням потребують NAT і пересилання, щоб бути корисними, а опубліковані порти потребують DNAT-правил. Docker оптимізований під «встановив і запустив», а не під «ваш модель відповідності». Якщо ви хочете іншої поведінки — налаштуйте її.

2) Я використовую nftables. Чому я бачу iptables-ланцюги як DOCKER-USER?

На Debian 13 iptables зазвичай використовує бекенд nft (iptables-nft). Docker програмує семантику iptables, яка під капотом стає об’єктами nftables. Різний синтаксис — той самий стан ядра.

3) Чи правильно розміщувати мої правила безпеки в DOCKER-USER?

Якщо ви дозволяєте Docker керувати plumbing iptables/nft, то так. Docker переходить у DOCKER-USER рано у форварді саме для того, щоб оператори могли застосовувати політику до Docker accept-прав. Використовуйте його.

4) Чому блокування в nft input не зупинило доступ до опублікованого контейнерного порту?

Тому що пакет може не проходити через INPUT. При DNAT ядро може трактувати вхідний трафік як пересланий на інший інтерфейс (docker bridge), тож воротами є FORWARD.

5) Чи варто відключати iptables-менеджмент Docker?

Тільки якщо у вас є вагомі причини і операційна зрілість, щоб самостійно володіти NAT/forwarding від початку до кінця. Інакше тримайте plumbing Docker і контролюйте політику в потрібному гачку.

6) Як запобігти випадковому опублікуванню сервісів у інтернет розробниками?

Комбінуйте контроли: встановіть default drop у DOCKER-USER для ingress з публічного інтерфейсу, вимагайте публікацію лише на loopback або приватний/VPN інтерфейс. Додавайте CI-перевірки для Compose-файлів і runtime-моніторинг опублікованих портів.

7) А як щодо rootless Docker?

Rootless змінює мережеву модель і часто зменшує пряме маніпулювання netfilter, але додає інші компоненти (slirp4netns, user namespaces) та інші компроміси продуктивності/функцій. Це може допомогти в ідеї «Docker не повинен перезаписувати правила фаєрвола», але не є універсальною заміною для продакшен-навантажень.

8) Чому правила зникають після перезавантаження?

Зазвичай через порядок завантаження і відсутність збереження. Docker може додавати правила при старті; сервіс nftables може пізніше очищувати або замінювати їх; або ви додавали правила вручну і ніколи не зберігали їх. Виправте це, роблячи модель власності явною і забезпечивши правильне завантаження правил потрібним сервісом у потрібний час.

9) Чи можу я керувати всім в одному nftables ruleset і все одно нормально використовувати Docker?

Так, але ви мусите узгодити з тим, як Docker очікує наявність ланцюгів і гачків, або відключити управління правилами Docker і відтворити потрібну nat/forward поведінку самостійно. Мета «єдиний ruleset» прийнятна; віра, що «сюрпризи зникнуть самі собою», — ні.

Висновок: наступні кроки, які ви можете зробити сьогодні

Якщо Docker здивував ваш nftables-фаєрвол на Debian 13, виправлення не в героїці. Воно в ясності. Визначте, хто володіє програмуванням netfilter, потім застосуйте політику в правильному гачку потрібними інструментами.

  1. Зробіть дамп живого ruleset (nft list ruleset) і визначте Docker-ланцюги та переходи.
  2. Інвентаризуйте опубліковані порти (docker ps) і зіставте їх з nat DNAT-правилами.
  3. Реалізуйте шлюз політики в DOCKER-USER, щоб обмежити вхідний доступ до опублікованих портів.
  4. Зробіть це стійким і протестуйте перезавантаження, щоб «працювало раз» стало «працює завжди».
  5. Документуйте шлях пакета для вашої команди: INPUT — не єдина двері.

Ваш фаєрвол має бути нудним. Docker не зробить його нудним за вас. Це ваша робота.

← Попередня
Народження 3D-акселераторів: коли GPU став окремим світом
Наступна →
MySQL проти PostgreSQL: чому ALTER TABLE стає кошмаром

Залишити коментар