Docker + UFW: чому ваші порти все одно відкриті — як правильно їх заблокувати

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

Ви ввімкнули UFW. Ви встановили «deny incoming». Ви навіть відчули невеличкий прилив праведності.
А потім швидке сканування показує, що порт вашого контейнера все ще доступний з інтернету. Чудово.

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

Ментальна модель: куди насправді йдуть ваші пакети

Коли ви публікуєте порт за допомогою Docker (-p 8080:80), Docker не чемно питає UFW про дозвіл.
Він програмує пакетний фільтр ядра (iptables/nftables), щоб зробити DNAT і приймати трафік, бо «щоб працювало» переважає «чекати людей» у стандартному дизайні.

UFW, між тим, — це менеджер правил. Він записує набір ланцюгів і переходів у iptables (або nftables на деяких системах)
і робить це в певному порядку.
Порядок — це все. Перше відповідне правило перемагає.

Ось основна проблема: Docker вставляє правила в таблиці nat і filter, які можуть пропускати пересланий трафік до контейнерів
ще до того, як UFW у позиції «deny incoming» встигне щось сказати.
Трафік не є «вхідним на хост» у тому сенсі, як ви думаєте; він пересилається через хост у контейнер.

Шлях пакета, спрощено але точно

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

  • PREROUTING (nat): Docker робить DNAT адреси призначення на IP/порт контейнера.
  • FORWARD (filter): Ядро пересилає пакет з інтерфейсу хоста в docker bridge.
  • DOCKER / DOCKER-USER (filter): Ланцюги Docker вирішують, що пропускати.
  • Container: Сервіс отримує пакет.

Звичайні правила UFW «deny incoming» здебільшого живуть навколо ланцюга INPUT.
Але переслані пакети потрапляють у FORWARD, а не в INPUT.
Тож ви можете зачинити парадний вхід, залишивши бокові двері широко відкритими.

Виправлення не містить містики. Застосуйте вашу політику в шляху, яким користується Docker: у ланцюзі DOCKER-USER.
Цей ланцюг існує саме для того, щоб ви могли застосувати власні правила перед логікою приймання Docker.

Чому UFW «програє» Docker (і чому це не баг)

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

Публікація портів Docker реалізована правилами iptables, які:

  • роблять DNAT трафіку в nat/PREROUTING і nat/OUTPUT для локальних підключень.
  • дозволяють форвард у filter/FORWARD у бік docker0 (або користувацького bridge).
  • підтримують власні ланцюги (наприклад DOCKER) і вставляють переходи досить рано, щоб це мало значення.

UFW може контролювати форвард, але багато установок залишають форвард дозволеним або не прив’язують правила UFW так, щоб вони випередили Docker.
І якщо ваш ментальний образ — «deny incoming означає, що нічого не досягає моєї машини», ви не помітите різниці між INPUT та FORWARD.

Одна цитата, яка закарбувалася в пам’яті опсів за десятиліття — можна назвати це парафразом ідеї від Gene Kranz (NASA Flight Director):
парафраз: «Твердість і компетентність переважають кмітливість, коли щось йде не так.»
Мережеві екрани — не місце для кмітливості. Вам потрібні нудні й правильні правила.

Жарт №1: Тимчасове правило брандмауера, додане під час інциденту, має таку саму напіврозпадну властивість, як і радіоактивні відходи — хтось інший унаследує його.

Цікаві факти та коротка історія, корисна о 3 ночі

  1. iptables — це упорядкована оцінка: правила перевіряються зверху вниз; перше збігається й перемагає. «Я додав deny» нічого не означає, якщо воно знаходиться під accept.
  2. Docker популяризував модель «хост як маршрутизатор»: ранній Docker за замовчуванням використовував bridge-мережі й NAT, фактично перетворюючи кожен хост на маленький edge-router.
  3. UFW — це фронтенд: він не «працює поряд» з iptables; він записує правила iptables і керує ланцюгами. Якщо хтось інший змінює iptables, UFW не є ясновидець.
  4. Ланцюг DOCKER-USER існує не просто так: його додали, щоб адміністратори могли застосовувати політики попереду правил, які керує Docker, не воюючи постійно з Docker.
  5. FORWARD часто ігнорують: багато команд зміцнюють INPUT, але забувають, що політика форварду має значення, коли в грі з’являються контейнерні bridge.
  6. conntrack — це станова клеюча логіка: «ESTABLISHED,RELATED» дозволи можуть зробити порт «відкритим» для існуючих потоків навіть після того, як ви його «закрили».
  7. Публікація прив’язується до 0.0.0.0 за замовчуванням: якщо ви не вказали IP, Docker буде експонувати на всіх інтерфейсах хоста. Це включає публічні інтерфейси.
  8. Міграція на nftables нерівномірна: сучасні дистрибутиви можуть використовувати nftables за замовчуванням, але взаємодія Docker і UFW все ще може проходити через сумісні шари iptables-compat.

Швидкий план діагностики

Коли хтось каже «UFW увімкнено, але порт все ще відкритий», не сперечайтесь про філософію. Виконайте план.
Мета — знайти, де саме відбувається accept: INPUT, FORWARD чи Docker DNAT.

Перше: підтвердьте, що саме експонується

  • Перевірте опубліковані порти Docker і їхні адреси прив’язки.
  • Підтвердіть, які сокети слухають на хості.
  • Протестуйте з зовнішньої точки зору (не з того ж хоста).

Друге: нанесення карти шляху пакета

  • Перегляньте правила iptables/nftables: особливо nat PREROUTING і filter FORWARD.
  • Знайдіть ланцюги Docker і їхній порядок переходів.
  • Перевірте ланцюг DOCKER-USER — чи існує він і чи щось робить?

Третє: вирішіть правильну стратегію обмеження

  • Якщо сервіс повинен бути лише внутрішнім: прив’яжіть публікацію до 127.0.0.1 або приватного інтерфейсу.
  • Якщо він має бути публічним, але обмеженим: застосуйте правила в DOCKER-USER за IP джерела, інтерфейсом або портом призначення.
  • Якщо потрібна справжня периметрова політика: віддавайте перевагу виділеному файрволу попереду (cloud SG/NACL, апаратний або хостовий фільтр з суворою політикою DOCKER-USER).

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

Це можна виконати на типовому Ubuntu-хості з Docker і UFW. Підлаштуйте імена інтерфейсів за потреби.
Кожне завдання включає: команду, що означає вивід і яке рішення прийняти.

Task 1: Confirm UFW status and default policy

cr0x@server:~$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    203.0.113.0/24

Значення: «disabled (routed)» — червоний прапорець: пересланий трафік не керується UFW.
Рішення: Якщо ви покладаєтеся на UFW, щоб блокувати експозицію Docker, потрібно вирішити питання пересланого/маршрутизованого трафіку (або застосувати політику в DOCKER-USER).

Task 2: List Docker’s published ports with bind addresses

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
NAMES          IMAGE           PORTS
web            nginx:alpine    0.0.0.0:8080->80/tcp
metrics        prom/prometheus 127.0.0.1:9090->9090/tcp

Значення: web експонований на всіх інтерфейсах; metrics лише для localhost і не буде доступний зовні.
Рішення: Якщо сервіс не має бути публічним, переприв’язка до 127.0.0.1 — найпростіший виграш.

Task 3: Verify what’s listening on the host (sockets)

cr0x@server:~$ sudo ss -lntp | awk 'NR==1 || /:8080|:9090|:22/'
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=1542,fd=4))
LISTEN 0      4096   127.0.0.1:9090    0.0.0.0:*       users:(("docker-proxy",pid=1611,fd=4))
LISTEN 0      4096   0.0.0.0:22        0.0.0.0:*       users:(("sshd",pid=912,fd=3))

Значення: Docker (або docker-proxy) слухає на 0.0.0.0:8080, що справді доступне, якщо не відфільтровано на рівні пакетного фільтра.
Рішення: Не припускайте, що «це в контейнері, тож ізольовано». Ставтеся до цього як до будь-якого іншого прослуховувача.

Task 4: Confirm the host’s public interfaces and addresses

cr0x@server:~$ ip -br addr
lo               UNKNOWN        127.0.0.1/8 ::1/128
ens3             UP             198.51.100.10/24 fe80::5054:ff:fe12:3456/64
docker0          DOWN           172.17.0.1/16

Значення: Усе, що прив’язане до 0.0.0.0, доступне через ens3, якщо не відфільтровано.
Рішення: Вирішіть, чи потрібна експозиція на цьому інтерфейсі; якщо ні — прив’яжіть явно або заблокуйте на цьому інтерфейсі.

Task 5: Inspect Docker’s iptables NAT rules (where DNAT happens)

cr0x@server:~$ sudo iptables -t nat -S | sed -n '1,120p'
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 9090 -j DNAT --to-destination 172.17.0.3:9090

Значення: Трафік на порт 8080 DNAT’иться до контейнера. Правила INPUT UFW не зупиняють DNAT.
Рішення: Ви маєте контролювати прийняття форварду (filter/FORWARD) або застосувати політику в DOCKER-USER.

Task 6: Inspect the filter table FORWARD chain (the usual “gotcha”)

cr0x@server:~$ sudo iptables -S FORWARD
-P FORWARD DROP
-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 FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT

Значення: Навіть при політиці FORWARD DROP Docker встановлює accepts. Важливо, що він стрибає в DOCKER-USER першим.
Рішення: Розмістіть deny/allow правила в DOCKER-USER, щоб випередити accepts Docker.

Task 7: Check what’s in DOCKER-USER right now

cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -j RETURN

Значення: Політики немає. Усе потрапляє на RETURN і потім застосовуються дозволи Docker.
Рішення: Додайте явну політику сюди. Порожній DOCKER-USER — це «довірся мені, бро» мережування.

Task 8: Add a default deny for forwarded container traffic, then allow what you mean

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

Значення: Підключення з публічного інтерфейсу до docker0 заборонено, крім TCP/8080 з довіреної підмережі.
Рішення: Це позиція «робити експозицію явною». Додавайте дозволи по порту/сервісу, а потім тримайте drop.

Task 9: Confirm counters are moving where you expect

cr0x@server:~$ sudo iptables -L DOCKER-USER -v -n
Chain DOCKER-USER (1 references)
 pkts bytes target  prot opt in   out     source           destination
   12   720 ACCEPT  tcp  --  ens3 docker0 203.0.113.0/24  0.0.0.0/0            tcp dpt:8080
  305 18300 DROP    all  --  ens3 docker0 0.0.0.0/0        0.0.0.0/0
    0     0 RETURN  all  --  *    *       0.0.0.0/0        0.0.0.0/0

Значення: Ви активно відкидаєте спроби доступу і дозволяєте лише потрібні джерела.
Рішення: Якщо лічильники DROP несподівано зростають, ймовірно у вас експонований порт, про який ви не знали.

Task 10: Make the rules persistent (or they will vanish on reboot)

cr0x@server:~$ sudo apt-get update
cr0x@server:~$ sudo apt-get install -y iptables-persistent
cr0x@server:~$ sudo netfilter-persistent save
run-parts: executing /usr/share/netfilter-persistent/plugins.d/15-ip4tables save
run-parts: executing /usr/share/netfilter-persistent/plugins.d/25-ip6tables save

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

Task 11: Bind a published port to localhost (often the best fix)

cr0x@server:~$ docker run -d --name internal-admin -p 127.0.0.1:8081:80 nginx:alpine
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Ports}}' | grep internal-admin
internal-admin  127.0.0.1:8081->80/tcp

Значення: Сервіс доступний лише з самого хоста (або через SSH-тунель/реверс-проксі).
Рішення: Використовуйте це для дашбордів, адмін-панелей і всього, до чого ви заходите через bastion.

Task 12: Verify UFW’s routed policy if you insist on UFW managing forwarding

cr0x@server:~$ sudo ufw status verbose | grep -i routed
Default: deny (incoming), allow (outgoing), disabled (routed)

Значення: Політика routed вимкнена; UFW не керує FORWARD.
Рішення: Або увімкніть routed policy і інтегруйте уважно, або припиніть думати, що тільки UFW керує експозицією Docker — використовуйте DOCKER-USER.

Task 13: Check UFW’s forwarding setting in sysctl (the kernel can veto you)

cr0x@server:~$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

Значення: IP forwarding увімкнено (звично для Docker-хостів).
Рішення: Якщо ви вимкнете форвард, щоб «виправити» експозицію, очікуйте, що мереження контейнерів зламається. Це грубий інструмент, а не план.

Task 14: See the full rule ordering around Docker and UFW

cr0x@server:~$ sudo iptables -S | sed -n '1,120p'
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-USER
-N ufw-before-input
-N ufw-user-input
-A INPUT -j ufw-before-input
-A INPUT -j ufw-user-input
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER
-A DOCKER-USER -j RETURN

Значення: Docker підключається до FORWARD рано. UFW в основному підключається до INPUT. Ось і вся історія в 10 рядках.
Рішення: Припиніть очікувати, що політика INPUT контролюватиме пересланий трафік.

Task 15: Quick external check from another machine (reality test)

cr0x@server:~$ nc -vz 198.51.100.10 8080
Connection to 198.51.100.10 8080 port [tcp/http-alt] succeeded!

Значення: Порт доступний зовні.
Рішення: Якщо це не передбачалося, змініть адресу прив’язки або правила в DOCKER-USER, потім повторіть тест, поки з ненадійних джерел це не перестане проходити.

Task 16: Confirm Docker’s “iptables management” setting (and why you usually shouldn’t disable it)

cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
  "log-driver": "journald",
  "iptables": true
}

Значення: Docker керує iptables — це нормальна поведінка за замовчуванням.
Рішення: Не перемикайте це в false, якщо ви не готові повністю брати на себе відповідальність за NAT/форвард/ізоляцію і дебагувати дивні збої потім.

Шаблони локдауну, які реально працюють

Pattern A: Bind to localhost or a private interface whenever possible

Якщо сервіс використовується лише локальними процесами, реверс-проксі або SSH-тунелем, прив’язуйте його до 127.0.0.1.
Це чистіше за правила файрвола, бо ядро ніколи не робить сокет публічним.

У Docker Compose це означає:
публікувати як 127.0.0.1:PORT:PORT.
Це нудно. Це ефективно. Ви все ще можете поставити Nginx/Traefik/Caddy попереду, щоб свідомо керувати публічним трафіком.

Pattern B: Use DOCKER-USER as the policy gate for published ports

Думайте про DOCKER-USER як про «ланцюг команди безпеки».
Помістіть тут дефолтні drop для трафіку з публічних інтерфейсів у docker bridges,
а потім додайте явні дозволи для того, що має бути досяжним.

Безпечне за замовчуванням для хоста, що виходить в інтернет:

  • Дозволяйте established/related (або нехай Docker обробляє це).
  • Дозволяйте лише явно потрібні опубліковані порти з явних джерел.
  • Відкидайте решту трафіку з публічних інтерфейсів до docker bridge.

Pattern C: Put a real edge in front of Docker hosts

Хостові файрволи хороші, але вони не замінять мережеві обмежувачі рівня мережі.
Якщо ви можете використати cloud security group, виділений файрвол-апарат або навіть окрему ingress-ноду — зробіть це.
Захист в глибину — не слоган; це те, що рятує, коли «один поганий Compose-файл» не має перетворюватися на ваш тиждень.

Pattern D: Prefer reverse proxy ingress over publishing every service

Якщо кожен контейнер публікує свій порт у світ — ви створили зоопарк портів.
Реверс-проксі централізує TLS, автентифікацію та рішення щодо експозиції. Також знижує цікавість сканерів.

Жарт №2: Якщо ви публікуєте -p 0.0.0.0:2375:2375 для Docker API — вітаю, ви винайшли віддалений root.

Три корпоративні міні-історії з передової

1) Інцидент через неправильне припущення

Середня SaaS-компанія перенесла застарілий додаток у контейнери на кількох VPS-хостах.
План міграції був розумний: зменшити слід, зменшити витрати і покладатися на UFW, бо «ми вже його використовуємо скрізь».
Рев’ю безпеки було формальною перевіркою. UFW: увімкнено. Default deny: увімкнено. Відвантажити.

За кілька тижнів новий внутрішній інструмент було запущено як контейнер з -p 8080:8080.
Він призначався для внутрішнього адмін-доступу через VPN. Інженер припустив, що UFW заблокує непатріотичний трафік, бо «deny incoming».
Але не заблокував. Порт був доступний з публічної IP. Не гучно, не драматично — просто доступний.

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

Постмортем не був про те, щоб звинувачувати Docker або UFW.
Він був про прогалину в ментальній моделі: команда сприймала контейнери як процеси «всередині хоста», а не як кінцеві точки, до яких доходить трафік через форвард і DNAT.
Коли вони застосували дефолтний drop у DOCKER-USER і переприв’язали внутрішні порти до localhost, цей клас відмов зник.

Неприємний висновок: «Фаєрвол увімкнено» не є контролем безпеки. Тестована політика — це контроль безпеки.

2) Оптимізація, що відкотилася

Команда в enterprise працювала десятками сервісів на вузол і захотіла «швидшого мереження».
Хтось вирішив, що програмування iptables Docker — це «овергол» і вимкнув інтеграцію iptables у конфігурації демона.
Ідея була керувати файрволом через UFW і тримати систему «чистою».

Перші дні пройшли без проблем, бо більшість east-west трафіку була на тому ж хості і кешований стан conntrack приховав деякі проблеми.
Потім пройшов звичайний ребут хоста — вікно патчінгу, без драм.
Раптом деякі сервіси стали недоступні з інших хостів, а деякі опубліковані порти поводилися несумісно.

Команда витратила години на пошук примар: то чи DNS, то чи overlay мережі, чи змінилася назва bridge?
Насправді корінь був простішим: коли Docker не керував iptables, правила NAT і форварду не створювались надійно після рестартів,
і власні правила UFW не відтворювали потрібну «планку» Docker.

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

3) Нудна, але правильна практика, що врятувала ситуацію

Платформа фінансових послуг запускала контейнерні робочі навантаження на загартованих образах Ubuntu.
Нічого особливого. Їхня секретна зброя — нудна операційна дисципліна: на кожному хості був стандартний «аудит експозиції», що запускали щодня.
Він викидав docker ps порт-мапінги, ss -lntp прослуховувачі і відфільтрований вигляд iptables -S у центральний лог-індекс.

Одного дня розробник змерджив зміну в Compose, яка випадково опублікувала debug endpoint на 0.0.0.0:6060.
Сервіс не був «вразливим» у CVE-сенсі; він просто не мав бути доступним ззовні.
Протягом години diff аудиту викликав алерт: новий публічний прослуховувач, не в списку дозволених.

На виклику не потрібно було ні з ким сперечатися. Було доказ: новий прослуховувач і нове правило DNAT.
Вони відкотили зміну у Compose, перезапустили і алерт зник.
Ніякого впливу на клієнтів. Ніякої паніки. Ніяких безрезультатних зустрічей.

Урок не в тому, що «аудити круті». Урок в тому, що нудна, повторювана верифікація перемагає одноразову впевненість.
Ось як виглядає «робота по надійності», коли вона реально працює.

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

1) «UFW deny incoming, але порт контейнера все ще досяжний»

Симптоми: Зовнішнє сканування дістає до HOST:published_port, хоча UFW має його блокувати на папері.

Корінна причина: Трафік пересилається (FORWARD chain) після DNAT, а не доставляється в INPUT. Правила Docker дозволяють його.

Виправлення: Додайте політику в DOCKER-USER (drop за замовчуванням з публічного IF до docker bridge; дозволяти явно). Або прив’яжіть порт до localhost/приватного IP.

2) «Я додав правила UFW, щоб заблокувати порт, нічого не змінилося»

Симптоми: ufw deny 8080/tcp не впливає на опубліковані порти.

Корінна причина: Блок стосується INPUT; трафік DNAT’иться і пересилається.

Виправлення: Блокуйте в DOCKER-USER або припиніть публікувати на 0.0.0.0.

3) «Усе зламалося після ввімкнення суворого файрвола»

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

Корінна причина: Надто широкі DROP в FORWARD/DOCKER-USER без дозволів для established-з’єднань або необхідних шляхів egress.

Виправлення: Залишайте механіку форварду Docker, але застосовуйте таргетовану політику: дозволяйте established/related, потрібний трафік bridge, а потім відкидайте публічний ingress до docker bridges.

4) «Мої правила працювали до перезавантаження»

Симптоми: Після рестарту порти знову доступні.

Корінна причина: Правила iptables, додані вручну, не були збережені; Docker перебудував свої правила; ваші зникли.

Виправлення: Збережіть правила через iptables-persistent/netfilter-persistent або закодуйте їх у конфігураційному менеджменті. Перевіряйте після перезавантаження тестом.

5) «Я заблокував експозицію контейнера, але localhost-доступ теж зламався»

Симптоми: Реверс-проксі на хості не може достукатися до бекендів; health checks падають.

Корінна причина: Правило drop в DOCKER-USER занадто широке (відкидає трафік, що походить з хоста або внутрішніх інтерфейсів).

Виправлення: Спрямуйте правила по інтерфейсу (-i ens3 -o docker0) і/або по діапазонах джерел; залиште хост→bridge трафік дозволеним.

6) «UFW і Docker воюють; правила виглядають дубльованими і дивними»

Симптоми: Багато ланцюгів; плутанина; несумісна поведінка між хостами.

Корінна причина: Змішування бекендів nftables/iptables, відмінності дистрибутивів або кілька інструментів, що керують станом файрвола.

Виправлення: Виберіть одну площину керування. Перевірте, чи використовуєте ви iptables-nft сумісність. Стандартизуйтесь на образах. Тестуйте реальний шлях пакета, а не намір.

Чеклісти / покроковий план

План 1: Безпечно заблокувати існуючий Docker-хост (придатно для продакшену)

  1. Інвентаризація експозиції: перелік опублікованих портів і прослуховувачів.

    • Використовуйте docker ps і ss -lntp.
    • Рішення: які порти повинні бути публічними, приватними або лише localhost?
  2. Визначте публічні інтерфейси: не гадьте.

    • Використовуйте ip -br addr.
    • Рішення: які інтерфейси повинні мати доступ до контейнерів?
  3. Підтвердіть порядок ланцюгів Docker: переконайтеся, що DOCKER-USER викликається рано.

    • Використовуйте iptables -S FORWARD.
    • Рішення: якщо DOCKER-USER відсутній, у вас нестандартна конфігурація — виправте це перед продовженням.
  4. Реалізуйте вузьке правило drop для публічного ingress до docker bridges.

    • Почніть із drop, обмеженого інтерфейсом: -i public -o docker0.
    • Рішення: додавайте дозволи вище для потрібних портів/джерел.
  5. Тестуйте ззовні і спостерігайте лічильники.

    • Використовуйте nc -vz з іншої машини; перевіряйте лічильники iptables.
    • Рішення: ітеруйте, поки доступ матиме лише те, що потрібно.
  6. Збережіть і автоматизуйте.

    • Використовуйте iptables-persistent або конфігураційний менеджмент.
    • Рішення: додайте CI/CD gate або нічний аудит, щоб виявляти нові експозиції.

План 2: Будуйте нові хости з нульовою випадковою експозицією як дефолтом

  1. Визначте стратегію ingress: один реверс-проксі або невеликий набір публічних портів.
  2. Вимагайте явних адрес прив’язки в Compose для всього, що не має бути публічним (127.0.0.1:...).
  3. Відвантажуйте дефолтну політику DOCKER-USER: drop з публічного IF до docker bridge; дозволяти лише порти проксі.
  4. Додайте задачу аудиту експозиції (прослуховувачі + опубліковані порти + diff iptables).
  5. Тестуйте поведінку після перезавантаження: збереження файрвола і порядок рестарту Docker.

Питання та відповіді (FAQ)

1) Чому «ufw deny 8080/tcp» не блокує опублікований порт Docker?

Тому що трафік DNAT’иться і пересилається до контейнера. Він не обробляється через INPUT так, як би це робив процес на хості.
Потрібна політика в FORWARD/DOCKER-USER або прибрати публічну прив’язку.

2) Чи обходить Docker UFW навмисно?

Docker програмує iptables, щоб автоматично реалізувати NAT і форвард. Це може суперечити вашим очікуванням, але це не прихована функція.
Намірене місце для адміністратора — це DOCKER-USER.

3) Чи варто вимикати управління iptables у Docker?

Зазвичай — ні. Якщо вимкнути, ви бере на себе відповідальність за NAT, форвард, ізоляцію і крайні випадки після рестартів.
Нехай Docker робить сантехніку; застосовуйте політику в DOCKER-USER і уважно прив’язуйте порти.

4) Який найбезпечніший дефолт набору правил для DOCKER-USER?

Для хостів з доступом в інтернет: дозволяйте явні опубліковані порти від явних джерел, потім відкидайте трафік з публічних інтерфейсів до docker bridges.
Не блокуйте локальний трафік хоста, якщо для цього нема причини.

5) Чи можна виправити це лише за допомогою UFW?

Можна, але це крихке, якщо ви не глибоко розумієте, як UFW прив’язується до FORWARD і як Docker вставляє свої ланцюги.
Операційно безпечніший шлях — використовувати DOCKER-USER для політики доступу до контейнерів і UFW для INPUT хоста.

6) Чому прив’язка до 127.0.0.1 працює так добре?

Тому що вона прибирає експозицію на рівні сокета. Ніяких трюків пакетного фільтра не потрібно.
Це різниця між «заблоковано» і «недосяжно».

7) А як щодо IPv6?

Якщо IPv6 увімкнено, потрібно застосувати еквівалентну політику для ip6tables/nft.
Інакше ви «закриєте» IPv4 і ненавмисно лишите IPv6 широко відкритим. Аудитуйте обидва стеку.

8) Чому іноді я бачу docker-proxy, а іноді ні?

Поведінка userland proxy Docker змінювалася з часом і може бути переключена. Навіть без docker-proxy
iptables DNAT все одно може публікувати порти. Завжди перевіряйте і сокети, і правила iptables.

9) Якщо я використовую реверс-проксі у контейнері, чи потрібні DOCKER-USER правила?

Так, якщо ви хочете страховки. Проксі зменшує поверхню атаки, але один випадковий -p на бекенді все ще може його експонувати.
DOCKER-USER робить таку помилку нефатальною.

Наступні кроки, які варто зробити сьогодні

Припиніть вважати «UFW увімкнено» як кінцевий результат безпеки. На Docker-хості це лише початкова умова.
Ваша реальна задача — робити експозицію свідомою: прив’язуйте те, що можна — до localhost, а решту відфільтровуйте в DOCKER-USER.

  1. Зробіть інвентар: docker ps, ss -lntp, перевірка зовні.
  2. Перевірте порядок правил: підтвердіть, що FORWARD стрибає в DOCKER-USER рано.
  3. Реалізуйте дефолтний drop з публічних інтерфейсів до docker bridges у DOCKER-USER, а потім дозволяйте лише те, що потрібно.
  4. Збережіть правила і протестуйте поведінку після перезавантаження.
  5. Додайте нудний нічний аудит експозиції. Саме нудні речі зберігають вашу роботу.
← Попередня
IKEv2/IPsec: коли це кращий вибір, ніж WireGuard або OpenVPN
Наступна →
Debian 13: Nginx раптово повертає 403/404 — права доступу чи конфігурація, як визначити миттєво

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