Звіт про інцидент завжди починається однаково: «Змін у мережі не вносилися». Потім ви дивитеся на контейнер — і бачите, що він підключений до трьох мереж,
одна з яких доволі публічна, і він радісно відповідає на запити на порту, який ніхто не пам’ятає, що публікував.
Контейнери з кількома мережами — звична річ у продакшені: фронтенди з’єднуються з бекендами, агенти мають доступ і до контрольної, і до дата-площини,
моніторинг охоплює різні середовища. Водночас це ідеальний шлях випадково витекти трафіку не туди, якщо ви не розглядаєте Docker-мережі як справжню систему маршрутизації й брандмауерів —
а саме так вона й працює.
Що насправді йде не так із контейнерами, підключеними до кількох мереж
Контейнер, приєднаний до кількох Docker-мереж, фактично має кілька точок доступу. Він отримує кілька інтерфейсів, кілька маршрутів і іноді кілька DNS-представлень.
Docker також встановлює на хості правила NAT і фільтрації, які визначають, хто до чого може дістатися. Це нормально — доки ви не робите помилкового припущення про «внутрішні»
мережі, маршрути за замовчуванням, публікацію портів або про те, як Compose «ізолює» сервіси.
Випадкове відкриття доступу зазвичай трапляється одним із чотирьох шляхів:
- Публікація портів на хості, а хост доступний із мереж, про які ви не подумали (Wi‑Fi, корпоративна LAN, VPN, з’єднання VPC у хмарі тощо).
-
Мережеве мостування за дизайном: контейнер, підключений і до «frontend», і до «backend», стає точкою повороту. Якщо сервіс прив’язується до 0.0.0.0,
він слухає на всіх інтерфейсах контейнера — включно з тим, який вважається «невірним». -
Неочікувана маршрутизація: «маршрут за замовчуванням» всередині контейнера вказує через ту мережу, яку Docker визначив як основну. Це може бути
не та мережа, яку ви хотіли для вихідного трафіку. -
Дрейф брандмауера: правила iptables/nft змінювалися, були відключені, частково перезаписані інструментами безпеки хоста або «оптимізовані»
кимось, хто не любить складність (і також полюбляє інцидентні наради).
Ось неприємна правда: у багатомережевих конфігураціях «працює» і «безпечне» — різні речі. Якщо ви явно не контролюєте привʼязки, маршрути й політику,
ви отримаєте те, що вийде з налаштувань за замовчуванням. За замовчуванням не стоїть модель загроз.
Факти та контекст, які варто знати
- Початкова мережа Docker була одним мостом (docker0) з NAT; «мережі, визначені користувачем» з’явилися пізніше, щоб виправити особливості DNS/виявлення сервісів та ізоляції.
- Міжконтейнерна комунікація раніше керувалася одним прапорцем демонa (
--icc) для стандартного мосту; мережі, визначені користувачем, змінили правила гри. - Публікація портів існувала до сучасних робочих процесів Compose і була спроектована для зручності розробника; безпека в продакшені — це те, що ви додаєте, а не те, що гарантується.
- Історично Docker керував iptables напряму; на системах, що перейшли на nftables, шар перекладу може створювати сюрпризи в порядку правил і налагодженні.
- Оверлейні мережі (Swarm) додали зашифровані опції VXLAN, але шифрування вирішує лише підслуховування, а не відкриття через неправильні публікації портів чи невірні приєднання.
- Macvlan/ipvlan додали, щоб задовольнити потреби «реальної мережі»; вони також обходять приємні припущення людей щодо ізоляції Docker-bridge.
- «Internal» мережі в Docker блокують зовнішню маршрутизацію, але не заважають доступу контейнерів, підключених до цієї мережі, і не очищають публікацію портів.
- Rootless Docker змінює підкапотну логіку; ви отримуєте user-mode мережі на зразок slirp4netns і інші характеристики продуктивності та брандмауера.
- За замовчуванням мережі Compose зручні, але не оборонні; мережа за замовчуванням не «безпечна», вона просто «існує».
Одна цитата, яку варто тримати на екрані, бо вона пояснює більшість простоїв і більшість «стрільців у ногу» в безпеці в одному реченні:
Надія — не стратегія.
— парафразована ідея, часто приписувана практиці надійності/операцій
Ментальна модель: Docker будує для вас маршрутизатори й брандмауери
Перестаньте думати про Docker-мережі як про «мітки». Це конкретні L2/L3 конструкції з Linux-примітивами під ними: мости, veth-пари, неймспейси, маршрути,
стан conntrack, NAT і фільтрувальні ланцюги. Коли ви підключаєте контейнер до кількох мереж, Docker створює в netns контейнера кілька veth-інтерфейсів
і зазвичай встановлює маршрути так, що одна з цих мереж стає шлюзом за замовчуванням.
Вам потрібно міркувати про три різні «площини»:
- Площина контейнера: які інтерфейси існують всередині контейнера, які IP, які маршрути, на яких адресах привʼязані сервіси.
- Площина хоста: правила iptables/nftables, налаштування мостів, rp_filter, пересилання пакетів та будь-які інші інструменти безпеки хоста.
- Верхня мережна площина: досяжність хоста (публічний IP, VPN, корпоративна LAN, таблиці маршрутизації хмари, security groups).
Якщо будь-яка площина неправильно змодельована, ви отримаєте «він був внутрішнім» і дуже публічний урок смирення.
Жарт №1: Docker-мережі як офісний Wi‑Fi — хтось завжди вважає його «приватним», поки принтер не доведе зворотне.
Поширені шляхи витоку (і чому вони дивують)
1) Публікація портів привʼязується ширше, ніж ви думаєте
-p 8080:8080 публікує на всі інтерфейси хоста за замовчуванням. Якщо хост доступний через VPN, піринговий VPC або корпоративну підмережу, ви щойно опублікували
і туди. Публікація — не «локальна», вона «хост-широка». Контейнер може бути приєднаний до десяти мереж; публікація цього не враховує.
Виправлення просте і нудне: привʼязуйтеся до конкретного IP хоста при публікації і ставтесь до «0.0.0.0» як до запаху проблем у продакшені.
2) «Internal» мережі — не силове поле
Прапорець мережі Docker --internal перешкоджає контейнерам цієї мережі виходити в зовнішній світ через стандартну поведінку шлюзу Docker.
Він не перешкоджає іншим контейнерам у тій самій мережі дістатися до них і не захищає публічні порти на хості.
3) Багатомережевий контейнер слухає на невірному інтерфейсі
Сервіси, які привʼязуються до 0.0.0.0 всередині контейнера, слухають на всіх інтерфейсах контейнера. Якщо ви підключаєте контейнер і до
frontend, і до backend, він може бути доступний з обох мереж, якщо ви явно не привʼязали його або не відфільтрували на рівні контейнера/хоста.
4) DNS і service discovery вказують на «невірний» IP
Вбудований DNS Docker повертає A-записи залежно від мережевого охоплення того, хто виконує запит. У багатомережевих сценаріях ви можете опинитися з іменем сервісу,
яке розвʼязується в IP у мережі, якої ви не мали на увазі для цього трафіку. Це виглядає як періодичні таймаути: інколи ви потрапляєте на «хороший» шлях,
інколи на «заблокований».
5) Вибір маршруту і неочікуваний шлюз за замовчуванням
Першою приєднаною мережею зазвичай стає маршрут за замовчуванням. Потім хтось додає «моніторингову» мережу або Compose приєднує мережі в іншому порядку, ніж ви очікували,
і раптом вихідний трафік йде не туди. Це може порушити припущення ACL і змусити логи походити із несподіваних IP-адрес.
6) Інструменти брандмауера хоста конфліктують із ланцюгами Docker
Docker вставляє правила в iptables. Інструменти захисту хоста також вставляють правила. «Команди безпеки» іноді додають агент, який «керує політикою брандмауера»
і не розуміє потреб Docker, тому він стирає або перевпорядковує правила. Потім з’єднання ламаються — або, гірше, починає працювати невірне зʼєднання.
Практичні завдання: аудит, підтвердження та рішення (команди + виводи)
Ви не захищаєте Docker-мережі «за відчуттями». Ви захищаєте їх, постійно відповідаючи на питання «хто звідки може дістатися?» з доказами.
Нижче — практичні завдання, які можна виконати на Linux Docker-хості. Кожне містить: команду, що означає вивід, і рішення, яке вам слід прийняти.
Завдання 1: Перелічити контейнери та їх публічні порти
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
NAMES IMAGE PORTS
api myco/api:1.9.2 0.0.0.0:8080->8080/tcp
postgres postgres:16 5432/tcp
nginx nginx:1.25 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
Значення: Усе, що показує 0.0.0.0:PORT, відкрито на всіх інтерфейсах хоста. Записи типу 5432/tcp без відображення на хості
не публікуються; вони доступні лише на Docker-мережах (якщо не використовується мережа host).
Рішення: Для кожного відображення 0.0.0.0 вирішіть, чи має воно бути доступним із усіх мереж, до яких підключається хост. Якщо ні — привʼяжіться до конкретного IP або приберіть публікацію.
Завдання 2: Подивитися, до яких мереж підключено контейнер
cr0x@server:~$ docker inspect api --format '{{json .NetworkSettings.Networks}}'
{"frontend":{"IPAddress":"172.20.0.10"},"backend":{"IPAddress":"172.21.0.10"}}
Значення: Контейнер у двох мережах. Якщо сервіс всередині привʼязаний до 0.0.0.0, він слухає на обох інтерфейсах.
Рішення: Якщо контейнер повинен приймати трафік тільки з однієї мережі, або від’єднайте його від іншої мережі, або привʼяжіть сервіс до потрібного IP інтерфейсу.
Завдання 3: Переглянути параметри Docker-мережі щодо охоплення та приєднаних кінцевих точок
cr0x@server:~$ docker network inspect backend --format '{{.Name}} internal={{.Internal}} driver={{.Driver}} subnet={{(index .IPAM.Config 0).Subnet}}'
backend internal=false driver=bridge subnet=172.21.0.0/16
Значення: Це мережа користувача типу bridge, не internal, з маршрутизованою приватною підмережею.
Рішення: Якщо бекенд-сервіси ніколи не повинні виходити в інтернет, розгляньте можливість створення цієї мережі з --internal і явним наданням потрібного egress через проксі.
Завдання 4: Знайти PID контейнера і зайти в його мережевий неймспейс
cr0x@server:~$ docker inspect -f '{{.State.Pid}}' api
23147
cr0x@server:~$ sudo nsenter -t 23147 -n ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
inet 127.0.0.1/8 scope host lo
42: eth0@if43: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
inet 172.20.0.10/16 brd 172.20.255.255 scope global eth0
44: eth1@if45: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
inet 172.21.0.10/16 brd 172.21.255.255 scope global eth1
Значення: Два інтерфейси, дві підмережі. Ваш сервіс може бути доступний з обох.
Рішення: Якщо тільки один інтерфейс має приймати вхідні з’єднання, налаштуйте додаток на привʼязку до конкретного IP (наприклад listen 172.20.0.10) або додайте правила брандмауера.
Завдання 5: Перевірити маршрути контейнера (хто шлюз за замовчуванням?)
cr0x@server:~$ sudo nsenter -t 23147 -n ip route
default via 172.20.0.1 dev eth0
172.20.0.0/16 dev eth0 proto kernel scope link src 172.20.0.10
172.21.0.0/16 dev eth1 proto kernel scope link src 172.21.0.10
Значення: Вихідний трафік йде через frontend (eth0). Якщо ви очікували лише вихід через backend, ви вже помилилися.
Рішення: Визначте, яка мережа має бути маршрутом за замовчуванням. Якщо це має бути інша мережа, змініть спосіб підключення контейнера (порядок приєднання) або використайте політичну маршрутизацію всередині контейнера (складно, крихко).
Завдання 6: Переконатися, на якій адресі/порті процес насправді слухає
cr0x@server:~$ sudo nsenter -t 23147 -n ss -lntp
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:(("api",pid=1,fd=7))
Значення: Сервіс слухає на всіх інтерфейсах. У багатомережевому режимі це зазвичай не те, чого ви хочете.
Рішення: Змініть сервіс так, щоб він привʼязувався до конкретного IP контейнера, або заблокуйте небажаний інтерфейс брандмауером. Не «довіряйте», що ніхто не дістанеться до бекенд-мережі.
Завдання 7: Підтвердити, які IP хоста насправді слухають (публікація портів)
cr0x@server:~$ sudo ss -lntp | grep -E '(:80 |:443 |:8080 )'
LISTEN 0 4096 0.0.0.0:80 0.0.0.0:* users:(("docker-proxy",pid=1943,fd=4))
LISTEN 0 4096 0.0.0.0:443 0.0.0.0:* users:(("docker-proxy",pid=1951,fd=4))
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("docker-proxy",pid=2022,fd=4))
Значення: Існують слухачі на всьому хості. Навіть якщо ви «лише думали» про внутрішній доступ, хост тепер бере участь.
Рішення: Якщо потрібно обмежити доступ, публікуйте як -p 127.0.0.1:8080:8080 або на конкретну LAN/VIP-адресу. Потім поставте перед цим реальний проксі.
Завдання 8: Інспектувати правила NAT і фільтрації, які встановив Docker
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.20.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 172.21.0.0/16 ! -o br-acde1234 -j MASQUERADE
-A DOCKER ! -i br-acde1234 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.20.0.10:8080
Значення: DNAT перенаправляє host:8080 до контейнера. Існують правила MASQUERADE для обох підмереж.
Рішення: Якщо ви покладаєтеся на політику брандмауера, переконайтеся, що вона реалізована у правильному ланцюгу (часто DOCKER-USER) і відповідає вашому наміру для кожного публічного порту.
Завдання 9: Перевірити ланцюг DOCKER-USER (тут слід розміщувати вашу політику)
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -j RETURN
Значення: Політики немає. Усе, що дозволяє Docker, дозволено.
Рішення: Додайте явні правила allow/deny тут, щоб обмежити, хто може потрапити на публіковані порти (джерельні підмережі, інтерфейси). Робіть це до того, як інциденти змусать вас робити це під тиском.
Завдання 10: Підтвердити міжконтейнерну доступність із «невірної» мережі
cr0x@server:~$ docker exec -it postgres bash -lc 'nc -vz 172.21.0.10 8080; echo exit_code=$?'
Connection to 172.21.0.10 8080 port [tcp/*] succeeded!
exit_code=0
Значення: Контейнер, який вважався лише бекендом, може дістатися API у бекенд-мережі. Це може бути правильно — або це може означати, що ваша дата-площина тепер торкається контрольної.
Рішення: Вирішіть, чи має бекенд-мережі бути дозволено ініціювати трафік до цього сервісу. Якщо ні — забезпечте це правилами брандмауера або розділіть відповідальності на окремі сервіси.
Завдання 11: Перевірити, чи DNS-відповіді відрізняються між мережами (тонкий випадок)
cr0x@server:~$ docker exec -it nginx sh -lc 'getent hosts api'
172.20.0.10 api
cr0x@server:~$ docker exec -it postgres sh -lc 'getent hosts api'
172.21.0.10 api
Значення: Одне імʼя — різні IP залежно від виконувача запиту. Це очікувана поведінка — і поширена корінна причина «працює з X, але не з Y».
Рішення: Якщо сервіс має бути доступний лише через одну мережу, не підключайте його до іншої мережі. DNS-«обмеження» не є кордоном безпеки; це зручність.
Завдання 12: Показати порядок мереж і «первинну» мережу в Compose
cr0x@server:~$ docker inspect api --format '{{.Name}} {{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}'
/api frontend backend
Значення: Видно порядок приєднання, але він не завжди стабільний при редагуваннях, якщо ви дозволяєте Compose автоматично генерувати мережі або рефакторите.
Рішення: У Compose будьте явними щодо мереж і подумайте про явне визначення мережі для маршруту за замовчуванням (через контроль порядку приєднання й мінімізацію багатомережевого підключення).
Завдання 13: Виявити контейнери, які випадково використовують мережу хоста
cr0x@server:~$ docker inspect -f '{{.Name}} network_mode={{.HostConfig.NetworkMode}}' $(docker ps -q)
/api network_mode=default
/postgres network_mode=default
/node-exporter network_mode=host
Значення: Один контейнер обходить ізоляцію мереж Docker і ділить стек хоста. Іноді це потрібно, часто — ризиковано.
Рішення: Якщо контейнер використовує network_mode=host, ставтеся до нього як до процесу на хості. Аудитуйте його адреси прослуховування та правила брандмауера так само, як для будь-якого демона.
Завдання 14: Перевірити, чи Docker управляє iptables (або ні)
cr0x@server:~$ docker info | grep -i iptables
iptables: true
Значення: Docker керує правилами iptables. Якщо тут false (або правила відсутні), публікація портів і поведінка підключень працюють інакше і часто небезпечно.
Рішення: Якщо ваше середовище відключає управління iptables Docker-ом, ви повинні реалізувати еквівалентну політику самі. Не використовуйте «iptables: false» легковажно, якщо ви любите відлагоджувати чорні діри.
Завдання 15: Прослідкувати публікований порт від хоста до контейнера
cr0x@server:~$ sudo conntrack -L -p tcp --dport 8080 2>/dev/null | head
tcp 6 431999 ESTABLISHED src=10.10.5.22 dst=10.10.5.10 sport=51432 dport=8080 src=172.20.0.10 dst=10.10.5.22 sport=8080 dport=51432 [ASSURED] mark=0 use=1
Значення: Ви бачите реальне зʼєднання, яке NAT-иться до IP контейнера. Це підтверджує шлях трафіку і джерела.
Рішення: Використайте це, щоб перевірити, чи запити надходять звідти, де ви очікуєте. Якщо джерела «сюрпризують», виправте публікацію привʼязкою або правилами брандмауера.
Завдання 16: Визначити, які мости існують і яким підмережам відповідають
cr0x@server:~$ ip -br link | grep -E 'docker0|br-'
docker0 UP 0a:58:0a:f4:00:01 <BROADCAST,MULTICAST,UP,LOWER_UP>
br-acde1234 UP 02:42:8f:11:aa:01 <BROADCAST,MULTICAST,UP,LOWER_UP>
br_bf001122 UP 02:42:6a:77:bb:01 <BROADCAST,MULTICAST,UP,LOWER_UP>
cr0x@server:~$ ip -4 addr show br-acde1234 | sed -n '1,8p'
12: br-acde1234: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
inet 172.21.0.1/16 brd 172.21.255.255 scope global br-acde1234
Значення: Кожен мережевий міст користувача відповідає Linux-bridge-пристрою з gateway-IP на хості.
Рішення: Якщо ви аудитуєте відкриття доступу, ці gateway-IP та підмережі мають значення для брандмауера та розуміння, як трафік виходить із неймспейсу контейнера.
Швидкий план діагностики
Коли щось «витікає», «не може підключитися» або «підключається з невірного місця», потрібна швидка послідовність дій, яка сходиться до рішення. Ось вона.
Ставтеся до неї як до настанов для виклику: перше/друге/третє, без блукань.
Перше: доведіть, чи порт відкритий на хості
- Запустіть
docker psі подивіться на відображення0.0.0.0:. - Запустіть
ss -lntpна хості і підтвердьте наявність слушача (docker-proxy або шлях NAT ядра).
Якщо він публікується: припускайте, що його може дістатися будь-яка мережа, до якої підключений хост, поки не доведено протилежне. Ваша історія «внутрішній» вже під питанням.
Друге: визначте мережі контейнера, IP та маршрут за замовчуванням
docker inspect CONTAINERдля мереж і IP.nsenter ... ip routeщоб побачити, яка мережа — шлюз за замовчуванням.nsenter ... ss -lntpщоб перевірити, чи сервіс слухає на 0.0.0.0 або на конкретному IP.
Якщо слухає на 0.0.0.0: він доступний із усіх приєднаних мереж, якщо щось це не блокує.
Третє: перевірте політику в єдиному місці, де її надійно застосувати
- Перевірте
iptables -S DOCKER-USER(або nft-еквівалент) на предмет явних allow/deny. - Підтвердьте, що Docker керує iptables (
docker info). - Протестуйте з контейнера в «невірній» мережі за допомогою
ncабоcurl.
Якщо DOCKER-USER порожній: у вас немає політики; у вас є лише надія.
Четверте: якщо поведінка нестабільна, підозрюйте DNS і багатоваріантні IP імен сервісів
- Запустіть
getent hosts serviceз викликачів у різних мережах. - Шукайте різні відповіді, що призводять до різних ACL-результатів.
Три корпоративні міні-історії з «передової»
Міні-історія 1: Інцидент через хибне припущення
Середня компанія запускала внутрішній admin API в Docker. «Внутрішній» означало «у бекенд-мережі в Compose», тож команда почувала себе в безпеці. Вони також
публікували порт на хості для зручності, бо декілька скриптів ops запускалися з хоста, і ніхто не хотів приєднуватися до мережі контейнера лише щоб скористатися localhost.
Під час зміни мережі хост приєднали до ширшої корпоративної підмережі в рамках консолідації VPN. Нічого в файлі Compose не змінилося. Порт все ще був публічний
на 0.0.0.0. Раптом інструмент іншої команди виявив API під час рутинного сканування. Це не було зловмисно.
Це був типовий цікавий скан, який трапляється у великих мережах, коли люди намагаються навести інвентар.
Admin API вимагав автентифікацію, але мав також дебаг-ендпоінт, який повертав інформацію про версію, метадані збірки та внутрішню карту хостів. Цього виявилося
достатньо, щоб полегшити соціальний інженеринг пізніше. Початковий «інцидент» не був крадіжкою даних; це було випадкове розкриття, яке розширило потенційну площу ураження.
Постмортем виявив банальну корінну причину: команда прирівняла «не на фронтенд-мережі Docker» до «недоступний». Вони ніколи не змоделювали хост як мережеву систему.
Публікація — це питання хоста, не контейнера. Виправлення було таким же банальним: публікувати тільки на 127.0.0.1 і ставити перед цим автентифікований проксі,
додати правила DOCKER-USER для обмеження решти публічних портів за джерельними підмережами.
Міні-історія 2: Оптимізація, що зіграла злий жарт
В іншій організації був сервіс, що мав проблеми з продуктивністю при зверненні до бази через Docker-bridge. Хтось запропонував macvlan для «майже нативної мережі»
і меншої затримки. Створили macvlan-мережу, підʼєднали її до production VLAN, дали контейнерам перше місце IP і святкували невелике покращення латентності.
Потім почалося неприємне: сервіс усе ще був підключений до внутрішнього bridge для service discovery і доступу до sidecar, що жив лише там. Тепер контейнер мав
дві мережеві ідентичності: одну в прод VLAN, одну на внутрішньому bridge. Сервіс привʼязався до 0.0.0.0 як звично. Тож він слухав на обох.
ACL бази даних припускали, що лише певні підмережі можуть звертатися до неї, але сервіс тепер міг ініціювати підключення з нового діапазону джерельних IP.
Частина трафіку почала йти іншим шляхом, бо маршрутом за замовчуванням став macvlan. Спостережуваність швидко стала дивною.
Справжня проблема не в macvlan. Проблема — в припущенні, що «додавання швидшої мережі» не змінює експозицію чи ідентичність. Змінює. Змінює джерельні IP,
маршрути і які інтерфейси отримують трафік. Інцидент не був «зламом», це були «таємничі збої автентифікації і непослідовна робота фаєрволу», що часто сповіщають
про майбутні проблеми безпеки.
Остаточне виправлення: припинити мультихомінг цього сервісу. Перенесли sidecar-функціонал у prod VLAN або, як альтернативу, перемістили додаток в одну bridge-мережу
і погодилися на втрату продуктивності. Друге виправлення — управління: нові мережі вимагали короткого огляду, що включав «на яких інтерфейсах сервіс буде привʼязаний і який маршрут за замовчуванням після зміни?»
Міні-історія 3: Нудна, але правильна практика, що врятувала ситуацію
Платформа в фінансовому секторі мала правило: на кожному хості є базова політика DOCKER-USER, і кожен публічний порт має бути виправданий з вказанням діапазону джерел.
Це не було сексуальним. Це був чекбокс в інфраструктурі як коді. Інженери скаржилися, тихо, як скаржаться інженери, коли їх не пускають робити випадкові ризикові дії.
Одної п’ятниці команда розгорнула контейнер для налагодження з простим веб-інтерфейсом. Хтось додав -p 0.0.0.0:9000:9000, бо потрібен був доступ з ноутбука.
Забули видалити. Контейнер також приєднали до другої мережі, щоб дістатися внутрішніх сервісів. Це був точний рецепт випадкового відкриття доступу.
Але базова політика блокувала вхід до публікованих портів, якщо джерело не було з маленької підмережі jump-host. Тому веб-інтерфейс був доступний там, де потрібен був,
і не був доступний звідусіль. Наступного тижня аудит виявив відкритий порт на хості, але не зміг дістатися до сервісу з загальної мережі. Квиток безпеки був дратівливим,
але це не був інцидент.
Урок не в «аудити корисні». Урок в тому, що нудні значення за замовчуванням кращі за героїчні чистки. Якщо ваша позиція в безпеці залежить від того, щоб хтось пам’ятав
прибрати тимчасовий прапорець — ваша позиція «згодом буде скомпрометована». Команда зберегла базову політику і додала в CI-guard перевірку, яка помічає публікації 0.0.0.0
у зміненнях Compose для огляду.
Шаблони жорсткого захисту, які дійсно працюють
1) Мінімізуйте мультихомінг. Віддавайте перевагу одній мережі на сервіс.
Багатомережеві контейнери іноді необхідні. Вони також множать складність. Якщо сервісу потрібно спілкуватися з двома різними речами, подумайте:
чи можна доступ до одного з них організувати через проксі на одній мережі? Чи можна розбити сервіс на два компоненти — по мережі на кожний — з вузьким, аудитованим API між ними?
Якщо неодмінно потрібно мультихомінгувати: ставтеся до контейнера як до обʼєкта поруч із маршрутизатором. Привʼязуйте явно, логування джерельних IP і брандмауер.
2) Публікуйте порти на конкретні IP хоста, а не на 0.0.0.0
Поведінка за замовчуванням — зручність для розробника. У продакшені будьте явними:
-p 127.0.0.1:...коли сервіс лише для локального доступу за зворотним проксі.-p 10.10.5.10:...коли сервіс має привʼязуватися до конкретного інтерфейсу/VIP.
Цей простий вибір виключає цілий клас інцидентів «тепер доступно через VPN».
3) Застосовуйте політику в DOCKER-USER (брандмауер хоста), а не на людську памʼять
Docker рекомендує DOCKER-USER як стабільне місце для власної політики, бо воно оцінюється перед власними правилами Docker.
Базова політика може виглядати так: дозволити встановлені зʼєднання, дозволити конкретні джерела до конкретних портів, решту відкидати.
Налаштовуйте під середовище; не робіть cargo-cult.
4) «Internal» мережі — для контролю egress, не для inbound-сегментації
Використовуйте --internal, щоб запобігти випадковому виходу в інтернет із чутливих шарів. Але не вважайте це вхідним кордоном. Якщо контейнер приєднано,
він може спілкуватися. Ваш кордон — це членство, політика брандмауера й привʼязка додатку.
5) Привʼязуйте сервіси до потрібного інтерфейсу/IP всередині контейнера
Якщо сервіс має бути доступним лише з фронтенд-мережі, привʼяжіть його до IP або інтерфейсу цієї мережі. Це недооцінене, бо це «конфігурація додатка», а не «конфігурація Docker’а»,
але це один із найефективніших кроків.
6) Ставтеся до macvlan/ipvlan як до розміщення контейнерів прямо в LAN (тому що так і є)
З macvlan контейнер стає рівноправним вузлом реальної мережі зі своїм MAC і IP. Це потужно. Це також спосіб обійти приємні припущення про периметр, на які люди покладалися, коли все було за docker0.
7) Логуйте й оповіщайте про приєднання до мереж і публікацію портів
Витоки зазвичай трапляються через «невеликі зміни». Тому стежте за ними:
- Нові публіковані порти
- Контейнери, приєднані до додаткових мереж
- Нові мережі з драйверами на кшталт macvlan
Жарт №2: Якщо ваша модель безпеки залежить від «ніхто ніколи не запустить -p 0.0.0.0», у мене є мережевий міст, який я хочу вам продати.
Docker Compose і багатомережевість: звички безпечні за замовчуванням
Compose робить приєднання до кількох мереж простим. Це чудово, поки не робить їх занадто простими. Ось звички, що вбережуть вас від проблем.
Будьте явними щодо мереж і їх призначення
Називайте мережі за функцією, а не за командою. frontend, service-mesh, db, mgmt кращі за net1 і shared.
Якщо ви не можете назвати — ви, ймовірно, не зможете його захистити.
Віддавайте перевагу «проксі публікує, додатки — ні»
Поширена схема: лише зворотний проксі публікує порти на хості. Решта сервісів — лише у внутрішніх мережах. Це зменшує кількість публічних портів для аудиту.
Це не ідеально, але реальне зменшення поверхні атаки.
Використовуйте окремі сервіси замість одного сервісу з двома мережами, коли можливо
Якщо ви підключаєте сервіс і до frontend, і до db, він стає містком між зонами. Іноді це правильно. Часто — лінивість.
Віддавайте перевагу однохомінговому додатку і однохомінговому DB-клієнту або sidecar, якщо потрібна крос-зональна функціональність.
Контролюйте привʼязки публікацій у Compose
Порти в Compose підтримують привʼязку IP хоста. Використовуйте це. Якщо ви не вказуєте host IP, ви просите «всі інтерфейси».
Поширені помилки: симптом → корінна причина → виправлення
1) Симптом: «Сервіс внутрішній, але хтось дістався до нього через VPN»
Корінна причина: Порт публікували на 0.0.0.0 на хості, доступному через VPN/корпоративну маршрутизацію.
Виправлення: Привʼязати публікацію до 127.0.0.1 або конкретного IP інтерфейсу; додати правила DOCKER-USER, що обмежують джерела; поставити перед сервісом автентифікований проксі.
2) Симптом: «Бекенд-контейнери можуть потрапити на адміністраторський ендпоінт, якого не повинні»
Корінна причина: Багатомережевий контейнер слухає на 0.0.0.0 всередині контейнера, відкриваючи сервіс на всіх приєднаних мережах.
Виправлення: Привʼязати додаток до потрібного інтерфейсу/IP; відʼєднати непотрібні мережі; додати мережеву політику через брандмауер хоста, де це доречно.
3) Симптом: «Перехідні таймаути між сервісами»
Корінна причина: DNS розвʼязує одне і те саме імʼя сервісу в різні IP залежно від мережі викликача; деякі шляхи заблоковано або вони асиметричні.
Виправлення: Зменшити багатомережеві підключення; використовувати явні імена хостів для кожного рівня мережі; перевіряти за допомогою getent hosts з кожного викликуча.
4) Симптом: «Вихідний трафік зʼявився з нового діапазону джерельних IP»
Корінна причина: Маршрут за замовчуванням всередині контейнера змінився через порядок приєднання мереж; egress тепер використовує інший інтерфейс (macvlan, новий міст).
Виправлення: Контролюйте порядок приєднання і мінімізуйте мережі; примусьте egress через проксі; якщо це критично, реалізуйте політичну маршрутизацію обережно і тестуйте після кожного рестарту.
5) Симптом: «Публіковані порти перестали працювати після жорсткішого захисту хоста»
Корінна причина: Інструменти брандмауера хоста перевпорядкували або очистили ланцюги, якими керує Docker; зміни політики FORWARD зламали NAT-перенаправлення.
Виправлення: Узгодьте керування брандмауером хоста з Docker (використовуйте DOCKER-USER для політики); забезпечте дозволи на пересилання; перевіряйте правила після оновлень агентів.
6) Симптом: «Контейнер доступний із мереж, що не повинні бачити Docker-підмережі»
Корінна причина: Зовнішня маршрутизація/піринг тепер досягає RFC1918-діапазонів, що використовуються Docker-містами; «приватне» не означає «ізольоване».
Виправлення: Обирайте неперетинні підмережі для Docker; обмежуйте вхід на кордоні мережі й на хості; уникайте рекламування Docker-підмереж нагору по стеку.
7) Симптом: «Внутрішня мережа все ще дозволяє вхідний доступ»
Корінна причина: Неправильне розуміння: --internal блокує зовнішню маршрутизацію, але не доступ контейнерів, що приєднані; публікація портів — окрема річ.
Виправлення: Комбінуйте --internal з суворим членством у мережі й правилами брандмауера; не публікуйте сервіси лише для внутрішньої мережі.
Контрольні списки / поетапний план
Чеклист A: Перед тим як приєднати контейнер до другої мережі
- Перелічте поточні слушачі всередині контейнера (
ss -lntp). Якщо він привʼязаний до 0.0.0.0, припускайте, що буде доступним у новій мережі. - Визначте, яка мережа має бути маршрутом за замовчуванням (
ip route) і якою має бути ваша ідентичність при виході. - Підтвердіть очікування DNS: чи клієнти будуть розвʼязувати імʼя сервісу у правильний IP у кожній мережі?
- Документуйте причину мультихомінгу поряд із файлом Compose у репозиторії. Якщо це не задокументовано — це не справжнє.
Чеклист B: Коли ви експонуєте сервіс через -p / порти Compose
- Ніколи не публікуйте без явного host IP у продакшені, якщо ви справді не маєте на увазі «всі інтерфейси».
- Визначте дозволені діапазони джерел і реалізуйте їх у DOCKER-USER.
- Перевірте за допомогою
ss -lntpі тестового підключення з дозволеного і забороненого джерела. - Логуйте запити з джерельними IP; вони знадобляться пізніше.
Чеклист C: Базовий контроль хоста, який запобігає «ой» відкриттям
- Створіть базову політику DOCKER-USER, яка за замовчуванням відкидає трафік до публікованих портів, окрім затверджених джерел.
- Оповіщуйте про нові публіковані порти та нові приєднання до мереж (хоча б у процесі огляду змін).
- Запобігайте перетинам підмереж: обирайте Docker-підмережі, що не конфліктують з корпоративними/VPN/хмарними діапазонами.
- Стандартизуйтесь на одному менеджері брандмауера (iptables-nft vs nft) і тестуйте поведінку Docker після оновлень ОС.
Покроковий план усунення для виявленого випадкового відкриття
- Підтвердіть відкриття:
docker ps,ssна хості та віддалений тест з підозрілої мережі. - Негайна ізоляція: видаліть публікацію або привʼяжіть до
127.0.0.1. - Додайте обмежувальні правила DOCKER-USER, якщо порт має залишатися публічним.
- Виправте корінну причину: приберіть непотрібні мережі, привʼяжіть сервіс до правильного інтерфейсу і додайте регресійні перевірки в CI/огляді.
- Після змін перевірте: тестуйте з кожної мережевої зони і підтвердіть, що DNS-розвʼязування та маршрути працюють як задумано.
Поширені запитання
1) Якщо мій контейнер у «internal» Docker-мережі, чи він у безпеці?
Безпечніший для egress, але не автоматично захищений для inbound. Будь-який контейнер у цій мережі все ще може дістатися до нього. І публікація портів на хості ігнорує «internal».
2) Чому -p 8080:8080 відкриває більше місць, ніж я очікував?
Тому що це привʼязує до всіх інтерфейсів хоста за замовчуванням. Ваш хост підключений до більше мереж, ніж ви памʼятаєте, особливо з VPN і хмарною маршрутизацією.
3) Чи можна покладатися на розділення Docker-мереж як на кордон безпеки?
Розглядайте його як один шар, а не як єдиний захист. Справжні кордони потребують явної політики (правила DOCKER-USER або еквівалент), мінімальних мережевих приєднань і правильної привʼязки додатку.
4) Як зупинити багатомережевий сервіс від прослуховування на «невірній» мережі?
Налаштуйте додаток на привʼязку до конкретного IP/інтерфейсу контейнера. Якщо це неможливо, блокуйте небажаний вхід на брандмауері хоста або переробіть дизайн, щоб уникнути мультихомінгу.
5) Чому одне й те саме імʼя сервісу розвʼязується в різні IP?
Вбудований DNS Docker повертає відповіді, обмежені мережею викликуча. Це зручно для service discovery і часта причина плутаних підключень.
6) Чи є macvlan небезпечним сам по собі?
Ні. Він просто чесний. Він розміщує контейнери в реальній мережі, а це означає, що безпекова модель має бути реальною: VLAN, ACL і аудит.
7) Де розміщувати правила брандмауера, щоб Docker їх не перезаписав?
Використовуйте ланцюг DOCKER-USER для iptables-настроєнь. Він створений для вашої політики і оцінюється перед власними правилами Docker.
8) Який найшвидший спосіб довести випадкове відкриття?
Перевірте docker ps на наявність 0.0.0.0:PORT, підтвердіть через ss -lntp на хості, потім протестуйте з іншого сегмента мережі (або контейнера в іншій мережі).
9) Чи змінює rootless Docker ризики відкриття?
Він змінює підкапотну логіку і кілька значень за замовчуванням, але не скасовує потребу контролювати публікацію портів і багатомережеві привʼязки. Вам все ще потрібна модель і політика.
Висновок: практичні наступні кроки
Якщо ви запускаєте контейнери з кількома мережами, прийміть, що ви займаєтеся справжньою мережею. Потім дійте відповідно.
- Аудит публікованих портів і усунення привʼязок
0.0.0.0, де вони не є строго необхідними. - Для кожного багатомережевого контейнера перевірте: інтерфейси, маршрути і адреси прослуховування. Виправте привʼязки
0.0.0.0, які не повинні там бути. - Впровадьте базову політику DOCKER-USER і зробіть її частиною налаштування хостів, а не племінною релігією.
- Зменшіть мультихомінг за дизайном: одна мережа на сервіс, якщо ви не можете обґрунтувати площу ураження.
- Зробіть приєднання до мереж і публікацію портів предметом огляду, а не п’ятничною імпровізацією.
Вам не потрібна ідеальна безпека, щоб зупинити випадкове відкриття. Потрібні явні привʼязки, явна політика і менше сюрпризів. Це просто хороші операційні практики.