Ubuntu 24.04: Docker + UFW = Несподівано відкриті порти — закрийте діру, не зламавши контейнери

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

Ви зачиняєте хост за допомогою UFW, відкриваєте лише SSH, розгортаєте кілька контейнерів і йдете додому. Потім сканер (або гірше — клієнт) знаходить вашу службу, яка слухає в публічному інтернеті, принаймні так вам здається. Ніщо не підриває довіру до фаєрвола так швидко, як фаєрвол, який технічно робить те, що йому сказали — просто не те, що ви мали на увазі.

На Ubuntu 24.04 Docker усе ще може пробивати дірки навколо UFW у спосіб, який дивує навіть компетентних операторів. Це не “Docker небезпечний” — це про розуміння маршруту пакетів, шарів інтерфейсу iptables/nftables і конкретних гачків, які використовує Docker — а потім про розміщення правильних контролів, щоб контейнери продовжували працювати, а порти не з’являлися випадково в інтернеті.

Що насправді відбувається: чому UFW «працює», але порти все ще відкриті

UFW не є фаєрволом. UFW — це дружня бібліотекарка, яка кладе правила фаєрвола у правильні шухляди. Справжній охоронець на дверях — netfilter (iptables/nftables). Docker, між тим, — це менеджер VIP, який підходить до охоронця й додає кілька записок «дозволити цим людям увійти» — часто в ту шухляду, за якою UFW не спостерігає.

Коли ви публікуєте порт у Docker (-p 0.0.0.0:8080:80 або у Compose через блок ports:), Docker встановлює правила NAT і фільтрації так, щоб трафік, який потрапляє на публічний інтерфейс хоста на цьому порту, DNAT-ився до IP контейнера. Ці правила вставляються в ланцюги типу PREROUTING (таблиця nat) і FORWARD (таблиця filter), а Docker також керує власними ланцюгами, наприклад DOCKER, DOCKER-ISOLATION-STAGE-* і, що важливо, DOCKER-USER.

Де тут UFW? UFW зазвичай керує правилами в ланцюгах на кшталт ufw-before-input, ufw-user-input, ufw-before-forward тощо. Воно може блокувати трафік, спрямований до локальних сервісів, але опубліковані порти контейнерів часто проходять шлях FORWARD після DNAT, і Docker вже їх дозволив. Тому UFW може весь день показувати «deny 8080/tcp», але пакети все ще форвардяться до контейнера, бо те відхилення застосовано в іншому ланцюгу/порядку, ніж ви очікували.

Ubuntu 24.04 додає ще один рівень плутанини для операторів: сучасні дистрибутиви дедалі частіше використовують nftables як рушій, але все ще надають сумісний інтерфейс iptables. Docker зазвичай все ще програмує правила iptables (через iptables-nft на Ubuntu), які відображаються в наборі правил nft. UFW також програмує правила, і взаємодія — це питання «хто виконується першим», а не «хто правильніший». Фаєрволи детерміновані; припущення операторів — ні.

Якщо запам’ятати одне правило: коли Docker публікує порт, ставтесь до цього так, ніби ви відкрили порт у фаєрволі, бо по суті це саме так — просто не там, де ви дивилися.

Короткий жарт №1: Фаєрволи — як офісні двері: будь-хто може зайти, якщо людина з адміндоступом постійно підпирає їх «для зручності».

Цікаві факти та трохи історії (щоб поведінка стала зрозумілою)

  1. Docker вибрав iptables рано, бо він був універсальним. У середині 2010-х мережеві підсистеми Linux були фрагментовані; iptables був найменшим із гірших спільним знаменником для NAT і форвардингу.
  2. UFW — передусім генератор правил для iptables. Це інструмент політики, а не власник netfilter у рантаймі. Воно пише правила; воно не «володіє» netfilter.
  3. Опубліковані порти використовують DNAT, а не лише сокети, що слухають. Ось чому ss -lntp може показувати docker-proxy або прив’язаний порт, але справжня магія — це NAT + форвардинг.
  4. Історично Docker використовував юзерленд-проксі для публікації портів. Новіші версії Docker вважають за краще використовувати NAT у ядрі, коли це можливо, але поведінка залежить від версії та налаштувань; це змінює те, що ви бачите у ss.
  5. Ланцюг DOCKER-USER існує саме для того, щоб ви могли перевизначати Docker. Docker додав його після років запитів операторів про стабільну точку підключення, яку Docker не буде переписувати.
  6. «deny» UFW не автоматично застосовується до форвардного трафіку. За замовчуванням UFW часто настроєне навколо INPUT (до хоста), а не FORWARD (через хост до контейнерів).
  7. iptables в Ubuntu часто працює поверх бекуnду nft. Багато операторів все ще думають в термінах iptables; під капотом nft виконує правила (і пріоритети мають значення).
  8. Групи безпеки в хмарі можуть приховувати проблему. У жорстко контрольованих VPC хостовий фаєрвол може бути зайвим; перенесіть той самий хост у локальну інфраструктуру — і сюрприз стає заголовком.
  9. Rootless Docker змінює картину. У rootless-режимі мережі шлях експозиції та фільтрації може суттєво відрізнятися; не можна бездумно застосовувати ту саму iptables-рецептуру.

Нічого з цього не є дрібною курйозністю. Саме тому аргумент «але UFW увімкнено» руйнується під час розбору інциденту.

Одна цитата, ідея в парафразі: доктор Річард Кук (інженерія стійкості) відомий ідеєю: відмови трапляються там, де нормальна робота зіштовхується зі складністю, а не через те, що люди необережні.

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

Коли хтось каже «UFW увімкнено, але порт відкритий», не сперечайтесь. Не робіть припущень. Запустіть короткий, відтворюваний план і приймайте рішення на основі доказів.

Перше: підтвердіть, що саме відкрито

  • З іншої машини: проскануйте публічний IP хоста на заявлені порти.
  • На хості: перевірте сокети, що слухають, і опубліковані Docker-порти.
  • Рішення: чи то це процес хоста, чи опублікування контейнера, чи балансер/NAT попереду?

Друге: відобразіть шлях експозиції

  • Знайдіть контейнер і відповідне відображення портів.
  • Перевірте, чи трафік йде через INPUT (до хоста), чи через FORWARD (до контейнера через DNAT).
  • Рішення: чи потрібно блокувати в DOCKER-USER, налаштувати політику форвардингу UFW або змінити прив’язки публікації Docker?

Третє: перевіряйте порядок правил, а не лише їх наявність

  • Перелічіть правила iptables/nft з номерами рядків/лічильниками.
  • Пошукайте правила ACCEPT від Docker, що стоять перед drop-правилами UFW у відповідному ланцюгу.
  • Рішення: розмістіть примусове застосування в DOCKER-USER (рекомендовано) або явно переструктуруйте форвард-обробку UFW.

Четверте: застосуйте мінімальне виправлення, потім повторно перевірте зовні

  • Почніть з «deny за замовчуванням для опублікованих портів» у DOCKER-USER, потім дозволяйте тільки те, що потрібно.
  • Повторно запустіть зовнішній скан і переконайтеся, що перевірки стану контейнерів також проходять.
  • Рішення: якщо продуктивний трафік зламався, відкотіть і перейдіть до «прив’язувати опубліковані порти до довірених IP» як безпечного проміжного кроку.

Практичні завдання: команди, виводи та як приймати рішення

Ось завдання, які я насправді виконую під час триажу та загартування. Кожне має: команду, реалістичний вивід, що це означає, і яке рішення прийняти.

Завдання 1: Підтвердити статус UFW і базову політику

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    Anywhere

Що це означає: UFW увімкнено, вхід за замовчуванням — deny, але routed (форвард) — disabled. Опублікований трафік Docker зазвичай їде через FORWARD, а не INPUT.

Рішення: Не припускайте, що «deny incoming» покриває контейнери. Перевірте форвардинг і правила Docker.

Завдання 2: Перевірити, які порти слухають на хості

cr0x@server:~$ sudo ss -lntp
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=1186,fd=3))
LISTEN 0      4096   0.0.0.0:8080       0.0.0.0:*     users:(("docker-proxy",pid=4123,fd=4))
LISTEN 0      4096   127.0.0.1:9090     0.0.0.0:*     users:(("prometheus",pid=2201,fd=7))

Що це означає: Порт 8080 прив’язаний на всі інтерфейси через docker-proxy. Це сильна підказка, що експозиція пов’язана з контейнером, а не з випадковим демоном хоста.

Рішення: Знайдіть, який контейнер опублікував 8080 і чи повинен він бути публічним.

Завдання 3: Чітко перелічити опубліковані Docker-порти

cr0x@server:~$ sudo docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}"
NAMES           IMAGE                 PORTS
webapp          ghcr.io/acme/web:1.7  0.0.0.0:8080->80/tcp
redis           redis:7               6379/tcp
metrics-gw      prom/pushgateway      0.0.0.0:9091->9091/tcp

Що це означає: webapp і metrics-gw опубліковані публічно. Redis — ні (тільки всередині контейнера).

Рішення: Якщо ці сервіси мають бути приватними, виправте прив’язки і/або наведіть політику фаєрвола.

Завдання 4: Перевірити мережеві налаштування контейнера

cr0x@server:~$ sudo docker inspect webapp --format '{{json .NetworkSettings.Ports}}'
{"80/tcp":[{"HostIp":"0.0.0.0","HostPort":"8080"}]}

Що це означає: Явно опубліковано на всі інтерфейси. Одна рядок JSON — і у вас «несподівано відкритий порт».

Рішення: Або прив’язати до конкретного IP (наприклад, 127.0.0.1), або застосувати політику у DOCKER-USER для контролю експозиції.

Завдання 5: Визначити публічний інтерфейс і IP хоста

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

Що це означає: Публічний IP на ens3. Міст Docker — docker0. Знання інтерфейсів важлива для цілеспрямованих правил.

Рішення: Якщо хочете локальний доступ, прив’язуйте порти до 127.0.0.1 або приватного інтерфейсу, а не до 0.0.0.0.

Завдання 6: Перевірити політику UFW для routed/forward і ядровий форвардинг

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

Що це означає: Форвардинг увімкнено (Docker зазвичай його вмикає). «routed disabled» UFW не зупиняє ядро від форвардингу, якщо правила дозволяють.

Рішення: Розглядайте фільтрацію FORWARD як обов’язкову на хостах з контейнерами.

Завдання 7: Подивитися порядок ланцюга FORWARD (де живе правда)

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

Що це означає: Docker вставляє DOCKER-USER зверху. Це ваш важіль керування. Якщо ви ним не користуєтесь, правила ACCEPT Docker вирішують долю трафіку.

Рішення: Розміщуйте обмежувальну політику в DOCKER-USER, а не в довільних правилах INPUT UFW.

Завдання 8: Переглянути ланцюг DOCKER-USER (зазвичай порожній)

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

Що це означає: Обмежень немає; все повертається до правил Docker після RETURN.

Рішення: Додайте явні allow/deny сюди, щоб контролювати експозицію опублікованих портів.

Завдання 9: Подивитися NAT-правила, що виконують форвардинг портів

cr0x@server:~$ sudo iptables -t nat -S DOCKER | sed -n '1,8p'
-N DOCKER
-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 9091 -j DNAT --to-destination 172.17.0.3:9091

Що це означає: Трафік на хост-порт 8080 DNAT-иться до IP контейнера. Ось чому «закривати порт» потрібно також у шляху форвардингу.

Рішення: Не боріться з NAT за допомогою INPUT-drops. Контролюйте форвардинг через DOCKER-USER (або змініть прив’язку публікації).

Завдання 10: Перевірити зовні (бо локальні перевірки брешуть)

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

Що це означає: Порт доступний з мережевої точки зору. Якщо ви запускаєте це з того самого хоста, можна отримати хибну впевненість через loopback-маршрутизацію.

Рішення: Зовнішня валідація є обов’язковою, а не факультативною.

Завдання 11: Застосувати політику «deny за замовчуванням» для форвардингу контейнерів у DOCKER-USER

cr0x@server:~$ sudo iptables -I DOCKER-USER 1 -i ens3 -o docker0 -j DROP
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -i ens3 -o docker0 -j DROP
-A DOCKER-USER -j RETURN

Що це означає: Трафік, що надходить з публічного інтерфейсу і форвардиться в docker0, відкидається до того, як спрацюють правила дозволу Docker.

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

Завдання 12: Додати конкретний виняток allow (тільки те, що ви маєте намір експонувати)

cr0x@server:~$ sudo iptables -I DOCKER-USER 1 -i ens3 -o docker0 -p tcp --dport 80 -j ACCEPT
cr0x@server:~$ sudo iptables -L DOCKER-USER -n --line-numbers
Chain DOCKER-USER (1 references)
num  target   prot opt source      destination
1    ACCEPT   tcp  --  0.0.0.0/0   0.0.0.0/0   tcp dpt:80
2    DROP     all  --  0.0.0.0/0   0.0.0.0/0
3    RETURN   all  --  0.0.0.0/0   0.0.0.0/0

Що це означає: Ви дозволяєте форвардований трафік до порту призначення 80 (порт контейнера після DNAT), одночасно відкидаючи все інше з ens3 до docker0.

Рішення: Підтримуйте цей allow-list навмисно. Якщо ви не можете пояснити кожен дозволений порт одним реченням, його не повинно бути в списку.

Завдання 13: Зробити UFW дружнім до форвардингу (тільки якщо ви наполягаєте на UFW-центрованій політиці)

cr0x@server:~$ sudo grep -n '^DEFAULT_FORWARD_POLICY' /etc/default/ufw
19:DEFAULT_FORWARD_POLICY="DROP"

Що це означає: UFW налаштовано на drop форвардингу за замовчуванням (добре), але Docker може мати правила, що все одно дозволяють окремі пересилки.

Рішення: Тримайте це як DROP. Якщо воно встановлене в ACCEPT — змініть назад, якщо вам не подобається сюрприз-експозиція.

Завдання 14: Перевірити, чи керує UFW ланцюгом DOCKER-USER (зазвичай ні)

cr0x@server:~$ sudo iptables -S | grep -E 'ufw|DOCKER-USER' | sed -n '1,12p'
-N DOCKER-USER
-A FORWARD -j DOCKER-USER
-N ufw-before-forward
-N ufw-user-forward

Що це означає: UFW і Docker співіснують, але UFW за замовчуванням не вставляє політику в DOCKER-USER. Ось чому «ufw deny 8080» не допомогло.

Рішення: Визначте, хто володіє політикою форвардингу контейнерів. Моя пропозиція: DOCKER-USER під управлінням конфігураційного менеджменту, а не ручних правок.

Завдання 15: Персистувати зміни iptables через ребут (бо ребути трапляються о 3 ранку)

cr0x@server:~$ sudo apt-get update
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Reading package lists... Done
cr0x@server:~$ sudo apt-get install -y iptables-persistent
Setting up iptables-persistent (1.0.20) ...
Saving current rules to /etc/iptables/rules.v4...
Saving current rules to /etc/iptables/rules.v6...

Що це означає: Поточні правила iptables збережені на диск і будуть відновлені під час старту.

Рішення: Якщо ви покладаєтеся на правила DOCKER-USER — персистіть їх. Інакше «виправлення» зникне після техобслуговування.

Завдання 16: Повторно перевірити експозицію після застосування політики DOCKER-USER

cr0x@server:~$ nc -vz 203.0.113.10 8080
nc: connect to 203.0.113.10 port 8080 (tcp) failed: Connection timed out

Що це означає: Порт більше недоступний зовні (таймаут типово для drop). Це бажаний результат.

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

Стратегії виправлення, що закривають діру без ламання контейнерів

У вас є три розумні стратегії. Оберіть одну свідомо. Змішування їх на ходу — саме так ви отримаєте правила, що працюють лише коли Місяць у потрібній фазі.

Стратегія A (рекомендовано): Використовуйте DOCKER-USER як точку застосування політики

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

Модель: за замовчуванням drop для трафіку з публічного інтерфейсу(ів) в docker0; додавайте allow-правила для конкретних портів призначення або CIDR-ів; залишайте внутрішній east-west трафік без змін.

Плюси: Стабільно, явно, витримує зміну контейнерів, не залежить від уявлень UFW про форвардинг. Працює з Compose, Swarm-подібними патернами і звичайним bridge-мережевим сценарієм.

Мінуси: Ще одне місце для управління політикою. Потрібно персистувати правила і віддавати їх конфігураційному менеджменту.

Стратегія B: Не публікуйте на 0.0.0.0 (прив’язуйте до конкретних IP)

Якщо сервіс призначений лише для локального доступу або через реверс-проксі, не публікуйте його широко.

У Compose замість:

cr0x@server:~$ cat compose.yaml | sed -n '1,20p'
services:
  webapp:
    image: ghcr.io/acme/web:1.7
    ports:
      - "8080:80"

Використовуйте явні прив’язки:

cr0x@server:~$ cat compose.yaml | sed -n '1,20p'
services:
  webapp:
    image: ghcr.io/acme/web:1.7
    ports:
      - "127.0.0.1:8080:80"

Плюси: Проста ментальна модель. Жоден порт не доступний зовні, якщо ви цього прямо не сказали. Чудово, коли ви ставите перед контейнерами Nginx/Traefik/Caddy на хості.

Мінуси: Легко регресувати («просто прибрати 127.0.0.1 для тесту»), і це не вирішує випадки, коли потрібен зовнішній доступ лише з певних мереж.

Стратегія C: Змушуйте UFW керувати routed-трафіком (просунуто, хитке)

Ви можете насадити більше політики в форвард-ланцюги UFW і покладатися на ufw route правила. Це може працювати, але ви боретеся з тим фактом, що Docker має власне бачення і оновлює правила при старті контейнерів.

Якщо йдете цим шляхом, потрібно:

  • Тримати UFW forward policy в DROP
  • Використовувати ufw route правила навмисно
  • Аудіювати вставлення Docker-прав після кожного оновлення Docker

Особисто я віддаю перевагу DOCKER-USER, бо він створений саме для цього. UFW відмінний для «хостових сервісів» і загальної політики; він не надто підходить, щоб бути єдиним джерелом істини для форвардингу контейнерів, якщо вам подобається дебаг у вихідні.

Короткий жарт №2: NAT — це мережевий еквівалент спільної електронної таблиці: всі на неї покладаються, ніхто їй не довіряє, і вона завжди робить щось, чого ви не дозволяли.

Три корпоративні міні-історії з реального життя

Міні-історія №1: Інцидент через неправильне припущення

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

Команда продукту розгорнула новий внутрішній дашборд-контейнер і опублікувала його з -p 8080:80, щоб швидко переглядати. Вони припустили, що «UFW заблокує доступ з інтернету». Він не заблокував. Сервіс був доступний звідусіль, і вже за день хтось поза компанією почав його пінгувати.

Розбір інциденту був незручним, бо ніхто не зробив нічого відверто неправильного. Інженер не був недбалим; він просто наклав концепцію хост-фаєрвола на форвардинг контейнерів. SRE на чергуванні відтворив проблему миттєво: UFW відмовляв 8080/tcp на INPUT, але трафік ніколи не потребував INPUT після DNAT.

Виправлення — дві строки в DOCKER-USER плюс правило в їхніх правилах Compose: «немає опублікованих портів без явної IP-прив’язки». Культурне виправлення було краще: вони оновили чекліст, додавши зовнішнє сканування та аудит порядку правил щоразу після встановлення Docker.

Міні-історія №2: Оптимізація, що повернулася бумерангом

Інша організація мала проблеми з продуктивністю на завантаженому edge-хості. Хтось вирішив «спростити мережу», агресивно вимикаючи компоненти, які здавалися зайвими. Вони зменшили логування фаєрвола, видалили деякі ланцюги, які не впізнавали, і намагалися зробити UFW єдиним інструментом.

Це працювало — до тих пір, поки оновлення Docker не ввело назад свої ланцюги і не переставило частини FORWARD. Раптом сервіс, що мав бути доступним лише з внутрішньої підмережі, став доступним із ширших мереж. Оператор присягав, що «нічого не міняв в фаєрволі», що технічно було правдою: конфіг UFW не змінювався. Правила Docker змінилися.

Наслідком було не лише експозиція. Дебаг став складнішим, бо їхня «оптимізація» прибрала крихти: лічильники скинулися, логи стали тихішими, і не було встановленого місця для політики, яке Docker не переписував би. Вони змушені були заново вивчити потік правил під тиском.

Вони відновилися, зробивши нудну річ, якої намагалися уникнути: визначили єдину документовану політику фаєрвола хоста, застосували її через DOCKER-USER і залишили UFW для хостових сервісів. Вплив на продуктивність був несуттєвим порівняно з часом, витраченим на розбір інциденту.

Міні-історія №3: Нудна, але правильна практика, що врятувала день

Команда фінансових послуг працювала з контейнерними хостами в рамках жорсткого процесу змін. Це було не гламурно. На кожному хості був невеликий скрипт «мережеві інваріанти», який запускався в CI і ще раз на хості після деплоя: перелік опублікованих портів, diff iptables-ланцюгів, зовнішнє тестування доступності з контрольної підмережі-сканера.

Одного п’ятничного дня розробник оновив Compose-файл і випадково змінив мапінг порту з 127.0.0.1:9000:9000 на 9000:9000. На його ноутбуці це спростило життя. У продакшені це могло відкрити адміністративну консоль.

Тест інваріантів впав у CI, бо сканер дістався до порту 9000 на стажувальному хості. Pipeline зупинив деплой. Нікому не довелося бути героєм, ніхто не мав би казати, що «він би це помітив під час рев’ю». Скрипт спіймав це, бо був зроблений саме для таких помилок.

Вони виправили прив’язку Compose, перезапустили тест і задеплоїли. Це не було захопливо. У цьому і сенс.

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

1) «UFW відмовляє 8080, але він все ще доступний»

Симптом: ufw status не показує allow-правила, але зовнішні сканування підключаються.

Корінна причина: Трафік DNAT-иться і форвардиться до контейнера; INPUT-правила UFW не зупиняють FORWARD-трафік, який Docker дозволив.

Виправлення: Застосуйте політику в DOCKER-USER (drop public-to-docker0), а потім дозвольте лише потрібні порти; або прив’яжіть порти до 127.0.0.1.

2) «Я відкинув трафік у DOCKER-USER і тепер усе зламалося»

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

Корінна причина: Надто широка DOCKER-USER-правила відкинула весь форвардинг, а не лише публічний інгрес в docker0. Поширена помилка: відкидати без обмеження інтерфейсів.

Виправлення: Сфокусуйте правила: -i ens3 -o docker0 для інгресу до контейнерів, а -i docker0 -o ens3 (егрез) лишайте неушкодженими, якщо ви цього справді не хочете.

3) «Після ребуту порти знову відкриті»

Симптом: Ви виправили вчора; сьогодні все повернулося.

Корінна причина: DOCKER-USER-правила додані інтерактивно і не персистовані; Docker потім відновив свої правила під час старту.

Виправлення: Персистуйте правила через iptables-persistent або керуйте ними через systemd-юнит/конфігураційний менеджмент, що запускається після Docker.

4) «Тільки деякі опубліковані порти блокуються; інші проходять»

Симптом: Порт 8080 заблоковано, але 9091 досі доступний.

Корінна причина: Ваш allowlist/denylist базується на портах хоста, але ваше match-правило в DOCKER-USER стосується порту призначення після DNAT (порт контейнера). Або навпаки.

Виправлення: Визначте, по чому ви співпадатимете. У DOCKER-USER часто --dport стосується порту контейнера після DNAT на шляху FORWARD. Підтверджуйте за лічильниками і тестуйте кожен порт.

5) «Правило ufw route нічого не змінило»

Симптом: Ви додаєте UFW route-правило, але зв’язність не змінюється.

Корінна причина: Порядок правил: Docker-accept у FORWARD може все ще дозволяти трафік до того, як відпрацює UFW route-ланцюг, залежно від вставки правил.

Виправлення: Віддавайте перевагу DOCKER-USER для контролю інгресу до контейнерів. Якщо ви мусите використовувати UFW routing, перевірте порядок ланцюгів за допомогою iptables -S FORWARD і лічильники.

6) «Ззовні закрито, але моніторинг всередині показує відкрито»

Симптом: Внутрішні health-check-и проходять; зовнішні не проходять; хтось називає це хибним позитивом.

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

Виправлення: Перевіряйте з тієї самої мережевої перспективи, що й ваш threat model (інтернет/VPC-границя). Пишіть правила, які явно дозволяють внутрішні/VPN джерела і відкидають публічні джерела.

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

Покроковий план жорсткого захисту (робіть це на кожному Docker-хості)

  1. Інвентаризація експозиції: перелічіть опубліковані порти і хто за них відповідає (docker ps, ss).
  2. Вирішіть політику: які сервіси публічні, які приватні, а які — тільки через VPN.
  3. Встановіть початкову позицію: за замовчуванням drop трафіку з публічного інтерфейсу → docker bridge у DOCKER-USER.
  4. Додайте явні дозволи: лише для сервісів, що мають бути публічними (або для певних CIDR-ів).
  5. Прив’язуйте приватні сервіси: змініть Compose/Docker run, щоб публікувати на 127.0.0.1 або приватний IP.
  6. Персистуйте правила: упевніться, що DOCKER-USER-правила переживуть ребут і рестарт Docker.
  7. Повторно перевірте зовні: запустіть перевірку портів ззовні вашої мережі хоста.
  8. Запишіть інваріанти: додайте CI-перевірки, що провалюють деплой, якщо Compose-файл публікує у 0.0.0.0 без дозволу.
  9. Операціоналізуйте аудит: періодичне сканування + diff правил фаєрвола і опублікованих портів.

Мінімальний рецепт «secure-by-default» для DOCKER-USER

Це базова конфігурація, яку я люблю для інтернет-орієнтованих хостів з Docker bridge-мережами. Підлаштуйте імена інтерфейсів і порти під себе.

cr0x@server:~$ sudo iptables -I DOCKER-USER 1 -i ens3 -o docker0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
cr0x@server:~$ sudo iptables -I DOCKER-USER 2 -i ens3 -o docker0 -p tcp --dport 443 -j ACCEPT
cr0x@server:~$ sudo iptables -I DOCKER-USER 3 -i ens3 -o docker0 -p tcp --dport 80 -j ACCEPT
cr0x@server:~$ sudo iptables -I DOCKER-USER 4 -i ens3 -o docker0 -j DROP
cr0x@server:~$ sudo iptables -L DOCKER-USER -n --line-numbers
Chain DOCKER-USER (1 references)
num  target     prot opt source      destination
1    ACCEPT     all  --  0.0.0.0/0   0.0.0.0/0   ctstate RELATED,ESTABLISHED
2    ACCEPT     tcp  --  0.0.0.0/0   0.0.0.0/0   tcp dpt:443
3    ACCEPT     tcp  --  0.0.0.0/0   0.0.0.0/0   tcp dpt:80
4    DROP       all  --  0.0.0.0/0   0.0.0.0/0
5    RETURN     all  --  0.0.0.0/0   0.0.0.0/0

Що це робить: Дозволяє встановлені потоки й лише форвардить нові публічні підключення до контейнерів на 80/443. Все інше з публічного інтерфейсу в docker0 відкидається.

Чого це не робить: Воно не захищає контейнери всередині, не замінює TLS і не вирішує проблеми автентифікації додатків. Воно просто не дає випадковій експозиції стати політикою.

Поширені запитання

1) Чому Docker «оминує» UFW?

Це не стільки обхід, скільки використання іншого шляху пакетів. Опубліковані порти контейнерів зазвичай використовують NAT і ланцюг FORWARD; правила UFW, які ви ставите, часто спрямовані на INPUT. Різні ланцюги — різні результати.

2) Чи це специфічно для Ubuntu 24.04?

Ні, але підхід Ubuntu 24.04 з бекуnдом nftables і сучасними дефолтами полегшує непорозуміння. Основна поведінка існує скрізь, де Docker керує правилами iptables.

3) Чи варто вимикати iptables-інтеграцію Docker?

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

4) Яке найнадійніше швидке виправлення під час інциденту?

Додайте scoped DROP у DOCKER-USER для public interface → docker0, потім додайте явні ACCEPT для портів, які потрібно зберегти публічними. Негайно повторно перевірте зовні.

5) Якщо я прив’яжу 127.0.0.1:8080:80, чи цього достатньо?

Це сильний контроль для «доступ лише локально», особливо коли зовнішній трафік завершується реверс-проксі. Але це не допоможе, якщо сервіс має бути доступним з приватної підмережі/VPN без проксі — тоді потрібні allow-правила DOCKER-USER за source CIDR.

6) Чи це впливає на IPv6 також?

Так, і про це часто забувають. Якщо IPv6 увімкнено і Docker публікує на IPv6, потрібні відповідні політики в ip6tables/nft для v6. Не припускайте, що правила v4 покривають v6.

7) Чому не покладатися лише на групи безпеки хмари?

Групи безпеки — чудові, але вони не завжди присутні (on-prem), і вони не захищають від внутрішнього латерального руху так, як хостова політика. Також: оператори регулярно копіюють робочі навантаження між середовищами. Хостова політика має бути правильною сама по собі.

8) Як уникнути регресій, коли команди змінюють Compose-файли?

Впровадьте конвенції: заборонити naked "8080:80" для внутрішніх сервісів; вимагати явної IP-прив’язки або review-label. Додайте CI, який парсить Compose і провалює збірку, коли порт публікується на 0.0.0.0 несподівано.

9) Чи зламають правила DOCKER-USER трафік контейнер→контейнер?

Ні, якщо ви правильно їх обмежите. Зосередьтеся на інгресі з публічного інтерфейсу в docker0. Залишайте трафік, що походить із docker0, без змін, якщо у вас немає специфічних вимог щодо еґрезного контролю.

Висновок: практичні наступні кроки

Якщо ви запускаєте Docker на Ubuntu 24.04 і вважаєте, що лише UFW контролює експозицію, то ви поставили пастку для свого майбутнього «я». Виправлення не драматичне: зрозумійте, що опубліковані порти контейнерів живуть у шляху форвардингу/NAT, а потім наведіть політику там, де Docker дає вам стабільний гачок — DOCKER-USER.

Наступні кроки, які не витратять ваш час даремно:

  1. Запустіть інвентарні завдання: ss, docker ps і зовнішній тест доступності.
  2. Додайте за замовчуванням drop з public interface → docker0 у DOCKER-USER, потім дозвольте тільки те, що має бути публічним.
  3. Змініть внутрішні сервіси так, щоб прив’язувати порти до 127.0.0.1 (або приватного IP), щоб випадкові зміни не ставали експозицією.
  4. Персистуйте ваші правила і додайте регресійний тест у CI. Нудно. Правильно. Ефективно.

Ваша мета — не «перемогти» Docker. Ви прагнете зробити свій намір однозначним для фільтра пакетів. Оце й є вся гра.

← Попередня
Мінімальний профіль фаєрвола Debian 13: що дозволяти і що блокувати (без параної)
Наступна →
Реплікація MariaDB vs Percona Server: коли крайні випадки підводять

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