Ви ввімкнули UFW. Ви заборонили весь вхідний трафік. Ви відчули відповідальність. Потім ви запустили docker compose up
і — сюрприз — ваш контейнер усе одно доступний з інтернету. Не «може бути». Не «тільки з LAN».
Доступний. З. Усього.
Це одна з тих реальностей мереж Linux, що повторюється, бо налаштування за замовчуванням оптимізовані під «працює»,
а не під «безпечне». Хороша новина: ви можете ізолювати це на Ubuntu 24.04 без ламання Docker Compose,
без заміни UFW і без перетворення хоста на науковий проєкт з фаєрволів.
Ментальна модель: чому UFW і Docker конфліктують
UFW — це фронтенд. Він записує правила в системний фаєрвол (на Ubuntu 24.04 зазвичай під ним nftables,
але багато інструментів досі говорять «iptables семантика»). Docker також — фронтенд. Він пише правила фаєрволу, щоб
мережі контейнерів «просто працювали»: NAT для виходу, публікація портів для входу та ізоляція між бриджами.
Конфлікт виникає тому, що Docker вставляє правила в місця, які UFW не контролює, і з пріоритетом, що переважає ваш
високорівневий режим «заборонити вхід». Коли ви публікуєте порт (-p 8080:80 або в Compose ports:),
Docker програмує DNAT і правила фільтрації так, щоб пакети були переслані в контейнер. Ці пакети можуть ніколи не досягнути
правила, яке, як вам здається, їх заблокує. UFW каже «deny»; Docker каже «я пообіцяв, що порт працюватиме»; Docker перемагає.
Практичний висновок: ви не «вирішуєте» це додаванням ще більшої кількості UFW allow або deny правил. Ви вирішуєте це,
контролюючи конкретний шлях, яким Docker передає трафік. Це означає: розуміти шлях FORWARD,
знати ланцюжки Docker (особливо DOCKER-USER) і вирішити, хто звідки має доступ до чого.
Суха істина: фаєрвол — це не «вхідний vs вихідний». Це напрямок трафіку плюс рішення маршрутів. Контейнери
— не локальні процеси; вони сидять за віртуальним маршрутизатором. Тож ваша політика «вхідні» може не застосовуватись
до трафіку, що пересилається до них.
Цитата для тверезого погляду з надійності: «Сподівання — це не стратегія.» — Gene Kranz.
Жарт №1: мережі Docker — як ключ-картка готелю: зручна, доки не виявиться, що вона також відчиняє двері «тільки для персоналу».
Факти та історичний контекст для аргументів
- UFW походить з ери iptables і досі мислить у цих термінах, навіть коли бекенд — nftables.
- Docker історично покладався на iptables для реалізації NAT і публікації портів; це припущення дизайну зберігається на різних дистроях.
- Linux netfilter має кілька хуків (PREROUTING, INPUT, FORWARD, OUTPUT, POSTROUTING). «deny incoming» UFW зазвичай орієнтований на INPUT, не на FORWARD.
- Docker публікує порти з DNAT; пакети, адресовані IP хоста, можуть бути переписані на IP контейнера до того, як INPUT UFW стане релевантним.
- Ланцюжок DOCKER-USER існує спеціально для того, щоб оператори могли вставляти фільтрацію перед власними accept-правилами Docker.
- Політики за замовчуванням UFW змінювались з роками, але класичний патерн зберігся: deny за замовчуванням на INPUT, allow для established/related, керування конкретними allow.
- Ubuntu перейшла на nftables як рекомендований бекенд; команди «iptables» можуть фактично бути обгортками (iptables-nft), що генерують nft правила.
- “iptables=false” у Docker — це не безкоштовний подарунок; відключення керування правилами Docker ламає звичну мережеву поведінку, якщо ви не заміните її самі.
- Трафік між контейнерами на одному бриджі не є «вихідним»; це локальний L2/L3 всередині хоста і може обходити наївні наміри фаєрволу, якщо ви його не фільтруєте.
Швидкий план діагностики
Ви хочете найшвидший шлях від «порт відкритий» до «я точно знаю, який ланцюжок його дозволив». Ось порядок, який
швидко знаходить вузьке місце.
1) Підтвердіть, що саме відкрите (не довіряйтесь файлам Compose)
- Перевірте опубліковані порти з погляду Docker (
docker ps). - Перевірте слухаючі сокети на хості (
ss -ltnp). - Протестуйте з віддаленої системи або другого NIC/мережевого неймспейсу, якщо є.
2) Визначте шлях пакета
- Якщо це опублікований порт, ймовірно він проходить DNAT у nat PREROUTING, а потім фільтрується через FORWARD.
- Якщо це хост-мережа (
network_mode: host), трафік потрапляє в INPUT, як до будь-якого демона.
3) Проінспектуйте правило, що виграє
- Перевірте
DOCKER-USERперш за все (ваша точка контролю). - Потім перевірте власні ланцюжки Docker (
DOCKER,DOCKER-FORWARD). - Потім перевірте політику пересилання в UFW і чи бачить UFW цей пакет.
4) Виправте мінімальною зміною, яка робить безпекову позицію істинною
- Якщо потрібно «доступно з LAN», фільтруйте за підмережею джерела в
DOCKER-USER. - Якщо доступ потрібен тільки з контейнера-проксі, перестаньте публікувати порт і використайте внутрішні мережі.
- Якщо потрібно доступ лише з localhost, прив’яжіть опубліковані порти до 127.0.0.1.
Цільовий стан: що означає «ізольовано»
«Ізольовані контейнери» — поняття розмите. Насправді виберіть один із доведених цільових станів і впровадьте його явно:
-
За замовчуванням: нічого не публікується. Контейнери спілкуються через приватні мережі Compose. Лише реверс-проксі
(або єдина шлюзова служба) публікує 80/443. -
Вибіркова публікація. Кілька портів публікуються, але лише для конкретних мереж-джерел
(корпоративний VPN, офісні IP-діапазони) і ніколи в інтернет, якщо сервіс не має бути публічним. -
Патерн localhost-only для девелоперських інструментів на прод-хостах (так, іноді це потрібно): публікуйте на 127.0.0.1 і
вимагайте SSH-тунелю, VPN або доступу з хоста. - Жорстка ізоляція між Docker-бриджами. Схід-захід трафік між контейнерами дозволений лише там, де ви його явно оголосили.
Чого варто уникати: «deny inbound за замовчуванням» в UFW та одночасне дозволяння Docker вільно публікувати порти. Це політичне
протиріччя з передбачуваним результатом.
Практичні завдання (команди, виводи, рішення)
Ось завдання, які я реально виконую на Ubuntu 24.04 під час діагностики або загартування UFW + Docker. Кожне містить
команду, що типовий вивід означає та як ухвалюється рішення.
Завдання 1: Перевірити стан UFW і політики за замовчуванням
cr0x@server:~$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
Значення: «routed» контролює forwarding (FORWARD). Якщо він вимкнений, UFW може не поліціювати пересланий
трафік так, як ви припускаєте.
Рішення: Якщо контейнери відкриті, вам треба вирішити поведінку forwarding (зазвичай через DOCKER-USER),
а не тільки INPUT-правила.
Завдання 2: Підтвердити, що Docker управляє iptables/nft правилами
cr0x@server:~$ sudo docker info --format '{{json .SecurityOptions}}'
["name=apparmor","name=seccomp,profile=builtin","name=cgroupns"]
Значення: Це безпосередньо не показує iptables, але підтверджує звичайне середовище Docker. Далі перевіряйте
конфіг демона.
Рішення: Перевірте /etc/docker/daemon.json перед тим, як робити припущення щодо поведінки Docker з фаєрволом.
Завдання 3: Перевірити налаштування iptables демона Docker
cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
"iptables": true,
"ip-forward": true
}
Значення: Docker програмуватиме правила фаєрволу. Це звична/стандартна ситуація.
Рішення: Залиште так. Вимкнення цього призводить до того, що люди випадково винаходять NAT вночі.
Завдання 4: Перелічити опубліковані порти з точки зору Docker
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Ports}}'
NAMES PORTS
web 0.0.0.0:8080->80/tcp
db 5432/tcp
prometheus 127.0.0.1:9090->9090/tcp
Значення: 0.0.0.0:8080 доступний світу (залежно від фаєрволу). 127.0.0.1:9090
— лише локально.
Рішення: Якщо сервісу не потрібно публічного доступу, приберіть опублікований порт або прив’яжіть його до localhost.
Завдання 5: Перевірити слухаючі сокети на хості (перевірка реальності)
cr0x@server:~$ sudo ss -ltnp | head -n 12
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=2214,fd=4))
LISTEN 0 4096 127.0.0.1:9090 0.0.0.0:* users:(("docker-proxy",pid=2311,fd=4))
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1042,fd=3))
Значення: Docker-proxy (або kernel NAT) приймає підключення на хості. Якщо він прив’язаний до 0.0.0.0,
ваш фаєрвол має бути налаштований правильно, інакше ви відкриті.
Рішення: Розглядайте прив’язки 0.0.0.0 як «публічні, поки не доведено протилежне».
Завдання 6: Визначити, який бекенд фаєрволу ви фактично використовуєте
cr0x@server:~$ sudo update-alternatives --display iptables | sed -n '1,12p'
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
slave iptables-restore is /usr/sbin/iptables-restore
slave iptables-save is /usr/sbin/iptables-save
Значення: Ви використовуєте iptables-nft-совместимість, що нормально. Важлива послідовність.
Рішення: Уникайте змішування сирих nft правил, які конфліктують з iptables-nft, якщо ви не повністю контролюєте набір правил.
Завдання 7: Переглянути ланцюжок DOCKER-USER (ваш хук політики)
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -j RETURN
Значення: Перед accept-правилами Docker немає обмежень.
Рішення: Саме сюди додайте «deny за замовчуванням, дозволити необхідне» для опублікованих портів.
Завдання 8: Перевірити політику FORWARD і Docker forwarding ланцюжки
cr0x@server:~$ sudo iptables -S FORWARD
-P FORWARD DROP
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-FORWARD
Значення: Default DROP — добре, але перехід в DOCKER-FORWARD означає, що Docker все ще може дозволяти певні потоки.
Рішення: Наведіть свої обмеження в DOCKER-USER, перед тим як DOCKER-FORWARD прийме трафік.
Завдання 9: Перелічити NAT-правила Docker для опублікованих портів
cr0x@server:~$ sudo iptables -t nat -S DOCKER | head -n 20
-N DOCKER
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.18.0.3:80
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 9090 -j DNAT --to-destination 172.18.0.5:9090
Значення: DNAT переписує трафік, адресований хост-порту 8080, у контейнер. Ось чому INPUT-правила
не є повною картиною.
Рішення: Якщо хочете блокувати публічний доступ, блокуйте його у filter FORWARD/DOCKER-USER за джерельною IP.
Завдання 10: Додати базову «deny для опублікованих портів Docker» у DOCKER-USER
cr0x@server:~$ sudo iptables -I DOCKER-USER 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
cr0x@server:~$ sudo iptables -I DOCKER-USER 2 -i lo -j ACCEPT
cr0x@server:~$ sudo iptables -I DOCKER-USER 3 -s 10.0.0.0/8 -j ACCEPT
cr0x@server:~$ sudo iptables -I DOCKER-USER 4 -s 192.168.0.0/16 -j ACCEPT
cr0x@server:~$ sudo iptables -A DOCKER-USER -j DROP
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-USER -i lo -j ACCEPT
-A DOCKER-USER -s 10.0.0.0/8 -j ACCEPT
-A DOCKER-USER -s 192.168.0.0/16 -j ACCEPT
-A DOCKER-USER -j RETURN
-A DOCKER-USER -j DROP
Значення: Приклад показує типову підводну каменю: Docker (або попередні правила) можуть вже містити RETURN.
Якщо RETURN стоїть перед DROP, ваш DROP не спрацює.
Рішення: Переконайтесь, що DROP стоїть перед будь-яким безумовним RETURN, або чисто замініть вміст ланцюжка.
На практиці потрібно: allow established, allow trusted sources, потім drop, потім return (або просто drop).
Завдання 11: Чисто переписати DOCKER-USER, щоб уникнути несподіваного порядку правил
cr0x@server:~$ sudo iptables -F DOCKER-USER
cr0x@server:~$ sudo iptables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
cr0x@server:~$ sudo iptables -A DOCKER-USER -s 10.10.0.0/16 -j ACCEPT
cr0x@server:~$ sudo iptables -A DOCKER-USER -s 192.168.50.0/24 -j ACCEPT
cr0x@server:~$ sudo iptables -A DOCKER-USER -j DROP
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-USER -s 10.10.0.0/16 -j ACCEPT
-A DOCKER-USER -s 192.168.50.0/24 -j ACCEPT
-A DOCKER-USER -j DROP
Значення: Детерміністично: якщо ви не з дозволених підмереж і не встановлене з’єднання — вас відкинуть до того, як Docker щось дозволить.
Рішення: Використовуйте це як базу на хостах, що не повинні публікувати довільні сервіси в інтернет.
Завдання 12: Перевірити, що UFW не мовчки дозволяє маршрутизований трафік
cr0x@server:~$ sudo grep -nE 'DEFAULT_FORWARD_POLICY|IPV6' /etc/default/ufw
7:DEFAULT_FORWARD_POLICY="DROP"
18:IPV6=yes
Значення: Політика forward за замовчуванням — DROP. Добре. IPv6 увімкнено; якщо ігнорувати його, можна «захистити» IPv4 і при цьому протікати по IPv6.
Рішення: Якщо ви використовуєте IPv6 (ймовірно, так), дублюйте політику для ip6tables/nft.
Завдання 13: Перевірити «before» правила UFW для взаємодії з Docker
cr0x@server:~$ sudo sed -n '1,140p' /etc/ufw/before.rules | sed -n '1,40p'
#
# rules.before
#
# Rules that should be run before the ufw command line added rules. Custom
# rules should be added to one of these chains:
# ufw-before-input
# ufw-before-output
# ufw-before-forward
#
Значення: UFW очікує, що ви додасте політики на шляху пересилання у ufw-before-forward, якщо потрібно.
Рішення: Віддавайте перевагу DOCKER-USER для обмежень, специфічних для Docker. Використовуйте ufw-before-forward для ширших маршрутних політик.
Завдання 14: Підтвердити Docker-мережі та bridge-інтерфейси
cr0x@server:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a1b2c3d4e5f6 bridge bridge local
b2c3d4e5f6a1 myapp_default bridge local
c3d4e5f6a1b2 host host local
d4e5f6a1b2c3 none null local
Значення: Кожен визначений користувачем бридж (наприклад myapp_default) може мати власний інтерфейс і поведінку ізоляції.
Рішення: Якщо прагнете обмежити east-west трафік, думайте по бриджах, а не тільки про docker0.
Завдання 15: Відобразити опублікований порт на конкретний IP контейнера (для точних правил)
cr0x@server:~$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web
172.18.0.3
Значення: Ви можете написати правило в DOCKER-USER, яке дозволить трафік лише до цього контейнера/порту (корисно для винятків).
Рішення: Віддавайте перевагу правилам на підмережі; IP контейнера змінюються. Використовуйте статичні IP лише коли це справді потрібно.
Завдання 16: Тестуйте з ненадійного джерела і спостерігайте лічильники
cr0x@server:~$ sudo iptables -L DOCKER-USER -v -n
Chain DOCKER-USER (1 references)
pkts bytes target prot opt in out source destination
40 2400 ACCEPT all -- * * 10.10.0.0/16 0.0.0.0/0
12 720 DROP all -- * * 0.0.0.0/0 0.0.0.0/0
Значення: Лічильники інкрементуються. Ваші правила фактично бачать трафік. Це найкраще відчуття з фаєрволами.
Рішення: Якщо лічильники не рухаються, ви фільтруєте не в тому ланцюжку/хуку (часто при використанні host networking).
Завдання 17: Зберегти правила через перезавантаження (не покладайтесь на пам’ять)
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
Reading package lists... Done
Building dependency tree... Done
Suggested packages:
firewalld
The following NEW packages will be installed:
iptables-persistent netfilter-persistent
Setting up iptables-persistent ...
Saving current rules to /etc/iptables/rules.v4...
Saving current rules to /etc/iptables/rules.v6...
Значення: Ваші поточні v4/v6 правила збережені і відновлюються під час завантаження.
Рішення: Якщо ви використовуєте правила DOCKER-USER, збережіть їх. Інакше наступний ребут «виправить» ваш фаєрвол назад до небезпечного стану.
Завдання 18: Перевірити IPv6-експозицію (тихий підступ)
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Ports}}' | sed -n '1,5p'
NAMES PORTS
web :::8080->80/tcp
prometheus 127.0.0.1:9090->9090/tcp
Значення: :::8080 — це IPv6-any. Якщо ви заблокуєте лише IPv4, вас все ще знайдуть по IPv6.
Рішення: Дзеркальні політики DOCKER-USER в ip6tables (або переконайтесь, що nft покриває обидві сімейства).
Жарт №2: IPv6 — як запасний ключ під килимком: усі забувають про нього, поки не знайде неправильна людина.
Шаблони Compose, які не підривають фаєрвол
1) Не публікуйте те, що не потрібно
Найчистіше правило фаєрволу — те, якого не потрібно, бо порт взагалі не відкритий. Compose
полегшує публікацію всього під час розробки і потім це легко забути.
Віддавайте перевагу expose: (внутрішнє) над ports: (опубліковане). Так, «expose» здебільшого документаційний плюс поведінка всередині Docker,
але основна ідея: внутрішні сервіси повинні бути доступні лише сусіднім контейнерам.
2) Прив’язуйте хост-порти до localhost для панелей адміністратора
Якщо потрібні Grafana/Prometheus/панелі адміністрування на сервері, прив’яжіть до 127.0.0.1 і доступайтесь через SSH-тунель або VPN.
Це дозволяє не ганятись за винятками фаєрволу.
В Compose:
ports: ["127.0.0.1:9090:9090"].
Так просто. Це не «безпека через приховання»; це відмова маршрутизувати трафік взагалі.
3) Використовуйте реверс-проксі як єдиний публічний край
Найкращий продовжуваний патерн: опублікуйте 80/443 лише для контейнера реверс-проксі (або daemon на хості), а все інше
тримайте в внутрішніх мережах Docker. Тоді ваші правила фаєрволу можуть бути нудними й передбачуваними.
Додайте мережу з «internal: true» для бекендів. Це каже Docker не надавати зовнішню доступність через цю мережу.
Це не вирішує всі випадки, але штовхає вас до здорової топології.
4) Уникайте network_mode: host, якщо ви цього не маєте на увазі
Host networking обминає більшість віртуальної мережі Docker і робить контейнер подібним до процесу хоста.
Це змінює, які хук-ланцюжки фаєрволу застосовуються. Також створює цікаві колізії портів.
Використовуйте його для чутливих до продуктивності моніторингових агентів або спеціалізованих мережевих інструментів, коли є підстава. Інакше
це скорочення, що стає тягарем під час реагування на інциденти.
5) Оголосіть окремі мережі для «публічного краю» і «приватного бекенду»
Окремі мережі дають розмежування, яке простіше аналізувати. Одна мережа підключена до проксі й додатка,
інша — лише до бекендів. Також можна обмежити маршрутизацію між мережами на рівні фаєрволу, якщо потрібно.
Ланцюжок DOCKER-USER: ваша точка впливу
Документація Docker ввела DOCKER-USER, бо оператори потребували стабільної точки вставки, яку Docker не буде перезаписувати.
Ланцюжок викликається з FORWARD рано (залежно від версії), і це правильне місце для примусового застосування «хто звідки може
досягати опублікованих портів контейнерів».
Думайте про нього як про «рівень політики» над «інфраструктурним рівнем» Docker. Docker налаштовує інфраструктуру, щоб пакети могли дістатися до контейнерів.
Ви визначаєте політику, щоб лише потрібні пакети насправді проходили.
Що поміщати в DOCKER-USER
- Спочатку дозволяйте established/related (повернення пакетів має працювати).
- Дозвольте з довірених підмереж (VPN, офіс, діапазон bastion).
- За бажанням дозволяйте конкретні публічні сервіси (наприклад 80/443 лише для проксі).
- Відкидайте все інше, що пересилається до Docker-мереж.
Що не варто поміщати в DOCKER-USER
- Правила за IP кожного контейнера, якщо ви не зафіксували IP і не готові до відповідального обслуговування.
- Правила, які припускають, що існує лише docker0 (Compose створює кілька бриджів).
- Правила, що відкидають без дозволу для established-з’єднань (ви зламаєте шляхи повернення для вихідного трафіку).
Патерни інтеграції UFW, що працюють на Ubuntu 24.04
Є два загальні підходи, що не закінчуються плачевно:
-
Тримайте UFW для сервісів хоста; контроль за ingress контейнерів — у DOCKER-USER. Це моя типовий рекомендований підхід.
UFW залишається операторським інструментом для SSH, node exporter і т.і. DOCKER-USER стає «периметром контейнерів». -
Виносьте більше політик у forward-ланцюжок UFW (
ufw-before-forwardта ін.). Це може працювати, але ви тепер
дебагатимете взаємодії між ланцюжками, якими керує UFW, і тими, що керує Docker. Це робиться; просто не найшвидший шлях.
Моя упереджена виробнича позиція
Тримайте deny incoming UFW за замовчуванням. Дозволяйте SSH лише з довірених джерел (VPN або bastion). Публікуйте лише 80/443 публічно.
Усе інше або:
- не публікується взагалі (внутрішні мережі Docker), або
- публікується на 127.0.0.1, або
- дозволено лише з приватних підмереж через DOCKER-USER.
IPv6: вирішіть, а потім застосуйте
На Ubuntu 24.04 IPv6 — не виняток. Якщо сервер має AAAA, він доступний. Якщо Docker публікує на ::,
вам потрібна стратегія для v6. Це може бути «вимкнути IPv6 скрізь» (рішуче, іноді коректно) або «відфільтрувати його належно»
(більш поширене в сучасних середовищах).
Три корпоративні історії з практики
Інцидент через хибне припущення: «deny incoming означає deny incoming»
Середня SaaS компанія перенесла кілька внутрішніх інструментів на свіжий кластер Ubuntu 24.04 VM. Платформна команда
мала стандарт: UFW увімкнено, deny inbound за замовчуванням, дозволити SSH з діапазону VPN. Вони використали Docker Compose для розгортання
внутрішньої панелі, бази даних і стеку метрик. Здавалося охайно.
Хибне припущення було тихим і класичним: вони вважали, що «deny incoming» UFW заблокує все, що доступне через
IP хоста, включно з портами контейнерів. Під час рутинного зовнішнього сканування (не навіть повноцінного pentest) хтось помітив,
що сторінка входу панелі відкрита на високому порту. Вона не мала бути публічною. І вона не була нещодавно оновлена.
Перша реакція була «але UFW активний; воно не може бути доступним». Друга реакція — подивитись docker ps і побачити
0.0.0.0:PORT->CONTAINER. Третя — незручна: вони побудували безпекову позицію на абстракції UI, а не на реальності потоку пакетів.
Виправлення не було героїчним. Вони припинили публікацію порту панелі, поставили її за існуючим реверс-проксі
і ввели allowlist DOCKER-USER для кількох адмін-сервісів, що дійсно потребували прямого доступу з VPN-підмережі.
Урок зрозумілий: «incoming» і «forwarded» — не одне й те саме, і Docker живе в зоні forwarded.
Оптимізація, що відплатилася: вимкнення iptables-менеджменту Docker
Інша організація мала ревʼю безпеки, яке не любило «додатки, що змінюють правила фаєрволу». Це розумно.
Команда вирішила виставити "iptables": false в конфігу демона Docker і керувати всім лише через UFW.
Вони зробили це в staging, бачили, що контейнери запускаються, і назвали це перемогою.
Перший відкат був тонким: вихідна підключеність контейнерів стала нестабільною в способи, які було важко пов’язати.
Деякі образи тягнулися повільно, деякі вебхуки таймаутились, і DNS періодично фейлив в залежності від вузла.
Це було не «впав», а «дивно». Дивно дорого.
Другий відкат був операційним: кожен проект Compose тепер вимагав кастомних NAT і forwarding правил. Розробники
не знали, куди які порти роутингуються, бо це вже не виражалось у файлі Compose. Правила фаєрволу стали знанням племені.
Зміни зайняли більше часу, і реагування на інциденти сповільнилось.
Зрештою вони відкотили зміну. Docker повернувся до управління iptables/nft plumbing. Команда безпеки отримала те, чого вони справді хотіли — контроль політики —
через обмеження в DOCKER-USER і стандартний шаблон дозволених підмереж і публічних портів. «Не дозволяйте Docker торкатись iptables» звучало чисто.
На практиці це замінило загальний механізм на власний. Це не безпека; це борг із значком.
Нудно, але правильно: збереження правил і тест ребута врятували день
Регульована компанія проводила квартальні вікна обслуговування, де хости перезавантажувалися, ядра оновлювались, і вирізувались звичні «це має бути нормально» зміни.
Одна команда мала звичку: після будь-якої зміни фаєрволу вони (1) зберігали правила,
(2) перезавантажували канарковий вузол, і (3) перевіряли експозицію ззовні підмережі. Нудно. Повторювано. Надокучливо правильно.
Під час одного вікна вони оновили Docker і освіжили політики UFW. Все виглядало нормально — поки канарка не перезавантажилась.
Зовнішня перевірка показала сервісний порт відкритим, який мав бути лише для VPN. Команда не панікувала; вони слідували чеклісту і помітили, що ланцюжок DOCKER-USER існує, але порожній після ребута. Правила не збереглись.
Оскільки вони впіймали це на канарці, вплив був малим: вони перевстановили інструменти збереження, зберегли v4 і v6 правила і перевірили ще раз.
Решта флоту пройшла з виправленою базою. Жодного інциденту. Ніяких листів клієнтам. Жодних великих нарад.
Мораль не гламурна: якщо ви не тестуєте ребут — у вас немає конфігурації. У вас є настрій.
Типові помилки: симптом → причина → виправлення
1) «UFW увімкнено, але порт контейнера все ще доступний»
Симптом: Віддалені клієнти можуть достукатися до host:8080, навіть за умов deny inbound за замовчуванням.
Причина: Трафік DNAT-иться і пересилається; політика INPUT UFW не застосовується. Правила Docker дозволяють його.
Виправлення: Додайте allowlist + drop політику в DOCKER-USER, або перестаньте публікувати порт.
2) «Я додав DROP у DOCKER-USER, але нічого не змінилося»
Симптом: Лічильники не рухаються; порт все ще відкритий.
Причина: Ви використовуєте host networking, або ваш DROP знаходиться після безумовного RETURN, або ви фільтруєте лише IPv4.
Виправлення: Перевірте iptables -S DOCKER-USER на порядок правил; перевірте network_mode: host; дзеркально застосуйте правила для ip6tables.
3) «Після ребута все знову відкрите»
Симптом: Політика працює до перезапуску.
Причина: Правила DOCKER-USER не збережені; змінений лише runtime стан.
Виправлення: Використовуйте iptables-persistent або systemd-юніт, що відновлює правила перед запуском Docker.
4) «Лише деякі користувачі можуть підключитись; інші таймаутяться»
Симптом: Користувачі VPN працюють, офісні користувачі — ні (або навпаки).
Причина: Allowlist за джерелом не включає всі реальні підмережі клієнтів; NAT змінює вигляд джерела.
Виправлення: Перевірте джерельний IP клієнта на сервері (tcpdump/conntrack), потім свідомо розширте allowlist.
5) «Трафік між контейнерами несподівано заблокований»
Симптом: Додаток не може достукатись до БД, хоча обидва в одному проекті Compose.
Причина: Надмірний DROP у DOCKER-USER без виключень для внутрішнього bridge-трафіку.
Виправлення: Дозвольте established/related, і якщо ви відкидаєте за замовчуванням — додайте явні дозволи для внутрішніх підмереж або інтерфейсів до DROP.
6) «IPv4 закрито, але сканери все одно бачать відкриті порти»
Симптом: Зовнішнє сканування показує відкриті порти незважаючи на IPv4-правила.
Причина: Експозиція по IPv6 (:::) і відсутність політики для ip6tables/nft.
Виправлення: Впровадьте відповідну політику для IPv6 або вимкніть IPv6 свідомо (хост + Docker) і перевірте.
7) «Docker Compose оновлення ламають мій фаєрвол»
Симптом: Після compose up правила змінились і доступ змінився.
Причина: Ваші обмеження знаходяться в Docker-керованих ланцюжках замість DOCKER-USER, або ви покладаєтесь на назви інтерфейсів, що змінюються.
Виправлення: Тримайте політику в DOCKER-USER і використовуйте стабільні критерії збігу (підмережі джерел, порти призначення, стан conntrack).
Чеклісти / покроковий план
План A (рекомендовано): публікуйте тільки крайові порти, обмежте все інше
-
Інвентар відкритих портів: запустіть
docker psтаss -ltnp; перелічіть усе, що прив’язане до 0.0.0.0 або :::. -
Видаліть випадкову публікацію: приберіть
ports:з внутрішніх сервісів; використайте внутрішні мережі. -
Прив’язуйте адмін-інструменти до localhost: використайте
127.0.0.1:PORT:PORTв Compose коли потрібно. - Визначте довірені діапазони джерел: VPN-підмережі, офісні підмережі, IP-адреси bastion. Запишіть їх.
- Застосуйте в DOCKER-USER: allow established/related, allow довірені діапазони, drop решту.
- Дзеркально для IPv6: додайте еквівалентні ip6tables правила або забезпечте покриття в nft для обох сімейств.
- Збережіть правила: встановіть інструменти для збереження і перевірте ребут.
- Перевірте зовні: протестуйте з ненадійної мережі і з довіреної мережі.
- Моніторьте лічильники: слідкуйте за лічильниками DOCKER-USER під час тестів; підтвердіть, що правила дійсно в шляху.
План B: політика UFW-центрик (тільки якщо ви любите трасувати ланцюжки)
- Встановіть політику forward UFW у DROP (вже поширено) і переконайтесь, що фільтрація routed увімкнена за потреби.
- Додайте явні дозволи для пересилання для Docker-бриджів і опублікованих сервісів у ufw-before-forward.
- Перевірте, що Docker не вставляє accept-правила, які обходять ваш намір (все одно, швидше за все, ви опинитесь біля DOCKER-USER).
План C: «Нічого ніколи не публікується» (для внутрішніх платформ)
- Запровадьте CI-перевірку, що відхиляє Compose-файли з
ports:, якщо не схвалено. - Вимагайте ingress через стандартизований шар реверс-проксі і внутрішню систему виявлення сервісів.
- Універсально відкидайте пересланий трафік до Docker-бриджів з ненадійних джерел.
Питання й відповіді (FAQ)
1) Чому UFW не блокує опубліковані порти Docker за замовчуванням?
Тому що опублікований трафік контейнера зазвичай пересилається після NAT, і позиція «deny incoming» UFW здебільшого керує
INPUT. Docker встановлює правила пересилання/NAT, щоб публікація портів гарантувалася.
2) Чи варто вимикати iptables-менеджмент Docker?
Ні, не як перший крок безпеки. Це замінює стандартний, добре зрозумілий механізм ручними NAT і forwarding правилами, що тепер належать вам назавжди. Використовуйте DOCKER-USER для політики, а Docker нехай підтримує plumbing.
3) Який один найкращий фікс, що не ламає Compose?
Додайте allowlist + drop правила в DOCKER-USER, потім приберіть непотрібні ports: з Compose. Це зберігає мережу Docker працюючою і запобігає «неочікуваній інтернет-експозиції».
4) Чи виживуть правила DOCKER-USER після рестарту Docker?
Зазвичай вони переживають перезапуск демона Docker, бо ланцюжок призначений для політик користувача, але вони не обов’язково
переживуть ребут хоста, якщо ви їх не збережете. Збережіть правила явно.
5) Як обмежити опублікований порт лише LAN?
Дозвольте LAN-підмережі у DOCKER-USER і відкиньте все інше. Альтернативно, прив’яжіть опублікований порт до IP інтерфейсу, що існує лише в LAN, але фільтрація за джерелом зазвичай зрозуміліша.
6) Що робити з контейнерами, що мають бути публічними (наприклад веб-додаток)?
Публікуйте лише реверс-проксі (80/443) публічно. Тримайте аплікаційні контейнери непублічними у внутрішніх мережах. Якщо потрібно публікувати додаток напряму, дозволяйте лише ті порти з 0.0.0.0/0 і залишайте все інше відкинутим.
7) Чи змінюється щось для контейнерів у host-network?
Так. Контейнери в режимі host поводяться як процеси хоста; трафік потрапляє в INPUT, а не FORWARD. Для них правильна точка контролю — правила UFW, а не DOCKER-USER.
8) Як дізнатись, чи IPv6 експонує мої контейнери?
Шукайте ::: у виводі docker ps або в ss -ltnp. Потім тестуйте з IPv6-спроможного зовнішнього хоста і підтвердіть, що ip6tables/nft політика відповідає намірам для IPv4.
9) Чи можна робити все це суто в nftables?
Можна, але якщо Docker використовує iptables-nft сумісність, уникайте конфліктного керування правилами. Практичний підхід на Ubuntu: зберігайте iptables-nft поведінку Docker і застосовуйте політику в DOCKER-USER (та еквівалент для v6).
10) Який найчистіший спосіб уникнути правил для кожного контейнера?
Не публікуйте порти для внутрішніх сервісів. Використовуйте внутрішні Docker-мережі плюс один крайовий проксі. Тоді ваш фаєрвол переважно: «дозволити 80/443, дозволити SSH з VPN, відкинути решту», а DOCKER-USER контролює, щоб контейнери цього не обійшли.
Висновок: наступні кроки, що тримаються
Надійний спосіб захистити Docker на Ubuntu 24.04 — не боротись з мережами Docker. Нехай Docker робить plumbing. Ви — політику.
Розмістіть політику там, де вона дійсно має значення: на шляху пересилання, перед accept-правилами Docker, через DOCKER-USER.
Якщо хочете практичну задачу на наступну годину:
- Запустіть інвентар:
docker ps,ss -ltnpі перевірте IPv6-прив’язки. - Приберіть випадкові
ports:у Compose; замініть їх внутрішніми мережами або localhost-прив’язками. - Впровадьте allowlist DOCKER-USER для довірених джерел, потім дефолтний drop.
- Збережіть правила для v4 і v6 і перевірте ребут на канарковому хості.
- Запишіть модель очікуваної експозиції (публічний край vs VPN-only vs internal-only), щоб наступний інженер не повернув «неочікуваний інтернет».
Ви отримаєте рідкісне в контейнерному світі: фаєрвол-позицію, що відповідає тому, що ви вважали налаштованим. Це не лише безпека.
Це операційний розум.