Ви відправляєте контейнер у продакшн. Публікуєте порт. У стенді все працює. Потім хтось запускає скан і знаходить вашу адмін-панель доступною з публічного інтернету — через IPv6 — тоді як ваш IPv4-фаєрвол виглядає бездоганно.
Цей сценарій трапляється часто, він тонкий і принизливий у тому самому дусі, який полюбляють інциденти в продакшні: конфіг правильна, намір «безпечний», а трафік все одно доходить. Зупинимо це.
Що насправді означає «витік IPv6» (у термінах Docker)
Коли кажуть «витік Docker IPv6», зазвичай мають на увазі одне з трьох:
-
Опубліковані порти доступні через IPv6, хоча оператор рахував лише IPv4.
Приклад: ви виконали-p 8080:80, вважали, що це прив’язується виключно до0.0.0.0, і забули, що на деяких системах це також прив’язується до::(всі IPv6-адреси). Тепер сервіс доступний из інтернету для будь-кого, хто бачить глобальну IPv6-адресу вашого хоста. -
Фаєрвол захищає IPv4, але не IPv6.
Ви загартували iptables, але залишили ip6tables/шлях nftables для v6 відкритим. Результат — «роздвоєна» безпека: в одному сімействі протоколів «заблоковано», в іншому — широко відкрито. -
Поведінка NAT/форвардингу Docker відрізняється для IPv6.
IPv4-мережі Docker часто використовують NAT і звичні патерни iptables. IPv6 за дизайном очікує маршрутизації. Якщо ви явно не фільтруєте форвардинг і вхід для IPv6, ви можете випадково надати контейнерам глобально досяжні адреси або дозволити вхідний форвардинг, якого ви не планували.
Термін «витік» емоційно точний: здається, ніби дані просочилися крізь шов, про який ви не знали. Технічно це не «витік». Це доступний сокет, створений дефолтною поведінкою і відсутністю явної політики.
Чому це відбувається: механіки, що підводять
1) Семантика прив’язки: 0.0.0.0 — не те саме, що ::, а «всі інтерфейси» неоднозначне
У Docker -p 8080:80 означає «опублікувати цей порт контейнера на хості». На багатьох налаштуваннях Docker за замовчуванням публікує на всіх інтерфейсах хоста. На двостекових системах «всі інтерфейси» може включати IPv6.
Чи з’явиться порт на IPv6 залежить від поведінки ядра щодо dual-stack, режиму проксі Docker і того, як ваша дистрибуція трактує net.ipv6.bindv6only. Деякі програми прив’язуються лише до v4; інші — за замовчуванням роблять dual-stack. Docker може публікувати через правила iptables і/або через userland-проксі залежно від версії та конфігурації.
2) Ваша політика фаєрвола діє лише в тому сімействі, яке ви забезпечили
Якщо ви використовуєте правила iptables як «межу безпеки», потрібно пам’ятати: існують два набори таблиць — IPv4 і IPv6. Або, в сучасних розгортаннях, nftables, де ви все одно маєте переконатися, що правила охоплюють ip6 так само, як і ip.
Класичний facepalm: UFW налаштовано і «активовано», але IPv6 вимкнено всередині UFW (або дозволено за замовчуванням), тоді як хост має глобальну IPv6-адресу. Ваш чекліст комплаєнсу бачить «фаєрвол встановлено». Атакувальники бачать «відкритий порт».
3) Поведінка ланцюга FORWARD Docker та гачок DOCKER-USER
Docker змінює фільтрацію пакетів, щоб зробити мережі контейнерів зручними. Зручність — це просто ризик з кращим маркетингом. Docker додасть правила, щоб дозволити форвардинг до мереж контейнерів і реалізувати опубліковані порти.
Docker також надає важливий гачок: DOCKER-USER. Він обчислюється перед власними правилами Docker. Якщо ви хочете політику типу «дозволяти вхід лише з цих CIDR» або «заблокувати все, крім конкретних опублікованих портів», DOCKER-USER — місце, де це зафіксувати.
Але багато команд реалізують DOCKER-USER тільки для IPv4, потім припускають паритет для IPv6. Паритет не встановлюється автоматично. Вам потрібен той самий намір у ip6tables/nftables.
4) IPv6 спроектовано для кінцевої досяжності наскрізь
Брак IPv4 змусив всіх переходити на NAT. Це нормалізувало ідею, що «приватні IP» відносно безпечні за замовчуванням. IPv6 змінює цю модель: адреси можна маршрутизувати глобально, отже фільтрувати потрібно навмисно. Коли ви даєте контейнеру глобальну IPv6 (або форвардите до нього), ви повертаєтесь у світ до-NAT.
Переклад: припиніть вважати NAT фаєрволом. NAT — побічний ефект, не контроль. Ваш контроль — це політика фільтрації й адреси прив’язки.
5) Хмарні платформи дають вам IPv6 і без вашого великого прохання
У кількох хмарах увімкнення IPv6 на VPC/сабнеті або інстансі — це «невеликий чекбокс», який змінює модель загроз для кожного опублікованого порту на кожному хості. Якщо інстанс має глобальну IPv6, а ваша security group / хостовий фаєрвол її не блокує, ваш Docker-публіш може бути публічним.
Жарт #1: IPv6 не страшний. Це просто IPv4 з достатньою адресною множиною, щоб виділити адресу кожному тостеру, включно з тим, що в кімнаті відпочинку, який постійно «таємничо» перезавантажується.
Цікаві факти та історичний контекст (IPv6 + контейнери)
- IPv6 стандартизували наприкінці 1990-х, а ядро RFC оновлювалося пізніше; він був «майбутнім» довше, ніж деякі production-системи існують.
- Рання мережа Docker (близько 2013–2014) сильно опиралася на iptables, і багато команд засвоїли правило «Docker = NAT». IPv6 ускладнює це припущення.
- IPv6 прибрав контрольну суму з заголовка, щоб пришвидшити маршрутизацію; експлуатаційно це переклало складність на кінцеві вузли й extension headers — добре для продуктивності, змішано для засобів безпеки.
- «Happy Eyeballs» (гонитва dual-stack-зʼєднань) змушує клієнтів вибирати v6 або v4 динамічно; оператори іноді ремонтують не те протокол через те, що клієнт тихо віддав перевагу IPv6.
- Linux має потужну підтримку IPv6 десятиліттями, але політики фаєрвола часто відставали — багато дистрибуцій історично поставлялися з дозволеними IPv6-правилами, навіть коли IPv4 було затиснуто.
- IPv6 privacy extensions (тимчасові адреси) зменшили відстеження, але також ускладнили білінг і реагування на інциденти, бо адреси хостів ротауються.
- Раніше userland-проксі Docker був частішим; у сучасних налаштуваннях часто покладаються на правила NAT у ядрі. Який шлях ви використовуєте може змінити значення «listening on ::».
- Адресація контейнерів залежить від драйвера: bridge, host networking, macvlan/ipvlan — ризик експозиції IPv6 не є однаковим. Деякі драйвери роблять маршрутизацію тривіальною.
- Багато баз комплаєнсу історично фокусувалися на IPv4, тому аудити «пройшли», поки продакшн був доступний через IPv6. Це не злість; це інерція.
Швидкий план діагностики (перевірити першим/другим/третім)
Коли ви підозрюєте «витік Docker IPv6» і хочете відповідь до наступного запрошення на зустріч, зробіть це в порядку:
Перший: підтвердіть, що хост дійсно має глобальну IPv6
- Чи має хост глобальну IPv6-адресу на публічному інтерфейсі?
- Чи є дефолтний маршрут IPv6?
- Чи доходить вхідний IPv6 до хоста (security group / крайовий фаєрвол)?
Якщо хост не досяжний глобально через IPv6, ваша проблема ймовірно внутрішня латеральна експозиція (ще погано, але інше виправлення).
Другий: визначте, що слухає на IPv6 і чому
- Чи сервіс прив’язаний до
::на хості? - Чи Docker публікує порт по v6 або лише форвардить?
- Чи контейнер сам слухає на IPv6 всередині свого неймспейсу?
Третій: знайдіть прогалину в політиці (фільтрація проти форвардингу)
- Input chain: чи хост дозволяє вхід до опублікованого порту на IPv6?
- Forward chain: чи трафік форвардиться у docker bridge по IPv6?
- DOCKER-USER: чи є у вас явні deny/allowlist для v4 і v6?
Четвертий: визначте ваш намір
Вам потрібен один із цих явних намірів, а не відчуття:
- «Ніякого IPv6 на цьому хості.» Вимкніть його на рівні хоста і Docker.
- «IPv6 дозволено, але за замовчуванням нічого публічного.» За замовчуванням відхиляйте вхідні та форвардинг для v6; дозволяйте лише те, що потрібно.
- «Публічний IPv6 допустимий, але лише для цих сервісів.» Публікуйте з явною привʼязкою і фільтруйте точно.
Практичні завдання (команди, виводи та рішення)
Нижче — перевірені в полі завдання. Кожне містить: команду, приклад виводу, що це означає і яке рішення слід прийняти далі.
Запускайте їх під root або через sudo там, де потрібно; мета — отримати відповіді, а не вигравати у «чистоту привілеїв».
Завдання 1: Перевірте, чи хост має глобальну IPv6-адресу
cr0x@server:~$ ip -6 addr show scope global
2: eth0 inet6 2001:db8:1234:5678:abcd:ef01:2345:6789/64 scope global dynamic
valid_lft 86234sec preferred_lft 14234sec
Значення: У вас є глобальна IPv6 на eth0. Якщо ваш фаєрвол ліберальний, опубліковані порти Docker можуть бути доступні з інтернету.
Рішення: Якщо вам не потрібен IPv6 — плануйте його відключити. Якщо потрібен — застосуйте явну IPv6-фільтрацію.
Завдання 2: Підтвердьте наявність дефолтного IPv6-маршруту
cr0x@server:~$ ip -6 route show default
default via fe80::1 dev eth0 proto ra metric 100 pref medium
Значення: Хост може виходити в IPv6-інтернет. Якщо вхід також дозволений нагорі, ви в зоні ризику.
Рішення: Розглядайте цей хост як здатний до інтернет-з’єднання через IPv6; зараз же перевіряйте слухачі і правила фаєрвола.
Завдання 3: Перелічіть слухаючі сокети й знайдіть v6-привʼязки
cr0x@server:~$ 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=911,fd=3))
LISTEN 0 4096 [::]:8080 [::]:* users:(("docker-proxy",pid=2341,fd=4))
Значення: Порт 8080 слухає на всіх IPv6-адресах ([::]) через docker-proxy. Це вектор публічної експозиції.
Рішення: Прив’яжіть до конкретної адреси, зніміть публікацію або відфільтруйте цей порт для IPv6.
Завдання 4: Знайдіть, який контейнер опублікував порт
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Ports}}'
NAMES PORTS
billing-api 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp
metrics-sidecar 127.0.0.1:9100->9100/tcp
Значення: Контейнер billing-api опублікований як на IPv4, так і на IPv6. Порт метрик коректно обмежено loopback.
Рішення: Якщо billing-api не має бути публічним, змініть публікацію на 127.0.0.1:8080:80 (і [::1] за потреби) або зніміть публікацію хост-порту.
Завдання 5: Перегляньте IPv6-налаштування демона Docker
cr0x@server:~$ cat /etc/docker/daemon.json
{
"ipv6": true,
"fixed-cidr-v6": "fd00:dead:beef::/48",
"ip6tables": true
}
Значення: IPv6 у Docker увімкнено, і Docker намагатиметься керувати ip6tables-правилами. Це не обов’язково небезпечно — але це не політика безпеки.
Рішення: Якщо вам не потрібен IPv6 всередині Docker, встановіть "ipv6": false (і приберіть v6 CIDR). Якщо потрібен — забезпечте default-deny у DOCKER-USER для IPv6.
Завдання 6: Перевірте, чи Docker використовує nftables або legacy iptables
cr0x@server:~$ update-alternatives --display iptables | sed -n '1,8p'
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
Значення: Ви на iptables-nft backend. Правила все одно працюють, але для розбору потрібно розуміти nftables під капотом.
Рішення: Використовуйте nft list ruleset, щоб підтвердити наявність IPv6-фільтрації; не припускайте, що вивід iptables дає повну картину, якщо інші інструменти керують nft.
Завдання 7: Перевірте політику фільтра IPv6 (ip6tables) на предмет «ACCEPT all»
cr0x@server:~$ ip6tables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-USER
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER
Значення: INPUT/FORWARD за замовчуванням ACCEPT для IPv6. Це не просто «відкрито», це «активно запрошує».
Рішення: Перейдіть на default-deny або принаймні додайте сильну політику в DOCKER-USER і звужуйте INPUT лише до потрібних сервісів.
Завдання 8: Підтвердьте, що правила форвардингу Docker для IPv6 існують (і не є вашим єдиним контролем)
cr0x@server:~$ ip6tables -L FORWARD -n -v --line-numbers | sed -n '1,20p'
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER-USER all * * ::/0 ::/0
2 0 0 DOCKER all * * ::/0 ::/0
Значення: Docker вставив гачки. Якщо DOCKER-USER порожній, трафік фактично пропускається. Docker зробив свою частину (підключення), не вашу (політику).
Рішення: Заповніть DOCKER-USER для IPv6, щоб накласти ваш фактичний намір експозиції.
Завдання 9: Додайте політику default-deny для IPv6 у DOCKER-USER (обережно)
cr0x@server:~$ ip6tables -I DOCKER-USER 1 -i eth0 -p tcp -m multiport --dports 80,443 -j ACCEPT
cr0x@server:~$ ip6tables -I DOCKER-USER 2 -i eth0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
cr0x@server:~$ ip6tables -A DOCKER-USER -i eth0 -j DROP
cr0x@server:~$ ip6tables -L DOCKER-USER -n -v --line-numbers
Chain DOCKER-USER (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 ACCEPT tcp eth0 * ::/0 ::/0 multiport dports 80,443
2 120 9540 ACCEPT all eth0 * ::/0 ::/0 ctstate RELATED,ESTABLISHED
3 4 240 DROP all eth0 * ::/0 ::/0
Значення: Ви дозволяєте лише 80/443 вхідних з IPv6 для форвардного трафіку на eth0, плюс встановлені з’єднання; все інше відкидається.
Рішення: Якщо порти контейнерів ніколи не повинні бути доступні з публічного інтерфейсу — залишайте DROP. Якщо потрібні специфічні порти — явно додайте їх у allowlist.
Завдання 10: Перевірте опубліковані порти у зручному для людей вигляді
cr0x@server:~$ docker port billing-api
80/tcp -> 0.0.0.0:8080
80/tcp -> [::]:8080
Значення: Dual-stack-публікація реальна, а не теоретична.
Рішення: Якщо ви хочете «лише внутрішній доступ», перепублікуйте на loopback або приберіть хост-публікацію й використовуйте reverse proxy з жорсткими привʼязками.
Завдання 11: Перевірте форвардинг ядра і поведінку RA для IPv6, що можуть вас здивувати
cr0x@server:~$ sysctl net.ipv6.conf.all.forwarding net.ipv6.conf.default.accept_ra
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.default.accept_ra = 2
Значення: IPv6-форвардинг увімкнено. Це нормально для маршрутизаторів, але ризиковано для загального хоста, бо змінює спосіб проходження пакетів між інтерфейсами. accept_ra=2 означає приймати router advertisements навіть коли форвардинг увімкнено — корисно в деяких хмарах, але потенційно небезпечно, якщо ви не розумієте маршрутних намірів.
Рішення: Якщо це не маршрутизуючий хост — подумайте про відключення форвардингу і контролюйте експозицію Docker через вхідні правила і привʼязки портів. Якщо форвардинг потрібен — жорстко зафіксуйте FORWARD-політики.
Завдання 12: Подивіться, чи контейнер має IPv6-адреси й маршрути
cr0x@server:~$ docker exec -it billing-api sh -lc 'ip -6 addr; ip -6 route'
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536
inet6 ::1/128 scope host
42: eth0@if43: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
inet6 fd00:dead:beef::42/64 scope global
default via fd00:dead:beef::1 dev eth0 metric 1024
Значення: Контейнер має сталу IPv6-адресу на префіксі ULA. Це не глобально маршрутизована адреса само по собі, але вона доступна там, куди маршрутується цей префікс (часто «всередині організації», що теж може бути надто великою областю).
Рішення: Вирішіть, чи потрібні контейнерам IPv6 взагалі. Якщо так — вирішіть, куди цей префікс маршрутується, і фільтруйте відповідно.
Завдання 13: Підтвердьте, що Docker думає про IPv6-конфіг мережі
cr0x@server:~$ docker network inspect bridge --format '{{json .EnableIPv6}} {{json .IPAM.Config}}'
false [{"Subnet":"172.17.0.0/16","Gateway":"172.17.0.1"}]
Значення: За замовчуванням bridge IPv6 тут вимкнено. Якщо ви все одно бачите IPv6-експозицію, ймовірно це через опубліковані порти на хості, а не через глобальні v6-адреси контейнерів.
Рішення: Зосередьтесь на слухачах хоста і фаєрволі, а не на адресності контейнера.
Завдання 14: Перевірте досяжність ззовні (або симулюйте її)
cr0x@server:~$ curl -g -6 -v 'http://[2001:db8:1234:5678:abcd:ef01:2345:6789]:8080/' 2>&1 | sed -n '1,12p'
* Trying 2001:db8:1234:5678:abcd:ef01:2345:6789:8080...
* Connected to 2001:db8:1234:5678:abcd:ef01:2345:6789 (2001:db8:1234:5678:abcd:ef01:2345:6789) port 8080 (#0)
> GET / HTTP/1.1
> Host: [2001:db8:1234:5678:abcd:ef01:2345:6789]:8080
> User-Agent: curl/7.88.1
> Accept: */*
Значення: Якщо це зʼєднується з хоста зовні вашої мережі — у вас публічна експозиція. Якщо воно зʼєднується лише внутрішньо — у вас все одно є експозиція, просто інша аудиторія (співробітники, VPN-користувачі, сусідні робочі навантаження).
Рішення: Якщо це не повинно бути доступним — зупиніть привʼязку і/або заблокуйте її фаєрволом зараз. Не чекайте на тикет, який перетвориться на постмортем.
Завдання 15: Перегляньте ruleset nftables для покриття IPv6 (сучасні системи)
cr0x@server:~$ nft list ruleset | sed -n '1,80p'
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif "lo" accept
tcp dport 22 accept
}
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
}
}
Значення: Використання таблиці inet охоплює й IPv4, і IPv6 одним набором правил. Зазвичай це бажано: менше шансів «забути IPv6».
Рішення: Якщо ви не використовуєте таблицю inet (або еквівалент), серйозно розгляньте міграцію. Подвійне підтримування політик v4/v6 — це джерело витоків.
Завдання 16: Аудит перемикача ip6tables у Docker (і не думайте, що він вас врятує)
cr0x@server:~$ docker info | sed -n '1,60p' | grep -E 'IPv6|iptables|Security Options'
IPv6: true
iptables: true
Security Options:
apparmor
seccomp
Значення: Docker запрограмує правила, але не втілить ваш намір. Завдання Docker — забезпечити підключення; ваше завдання — «підключення з обмеженнями».
Рішення: Розглядайте це як «наявність трубопроводу». Все одно реалізуйте deny-by-default і явні allowlist-и.
Три міні-історії з корпоративного життя
Міні-історія 1: Інцидент через хибне припущення
Середня SaaS-компанія випустила нову внутрішню панель. Вона мала бути доступна лише через корпоративний VPN. Додаток жив у Docker-контейнері за простим -p 3000:3000 на утилітному хості. Доступ по IPv4 блокувався на периферії; все здавалося гаразд.
Хибне припущення було тихим: «якщо IPv4 заблоковано, то й заблоковано». Їхні правила security group були орієнтовані на IPv4, а фаєрволи хоста — лише iptables. Тим часом команда мережі в хмарі увімкнула IPv6 на сабнеті в рамках більшого міграційного процесу, бо «він нам скоро знадобиться».
Вже за день автоматичний скан знайшов панель на глобальній IPv6 хоста. Не через якийсь хитрий експлойт — просто звичайне TCP-зʼєднання до опублікованого порту. Панель вимагала логін, але мала флоу скидання пароля з можливістю вичерпання емейлів. Це переросло в помітний інцидент: огляд безпеки, примусові скидання, незручні внутрішні комунікації.
Постмортем був болючим з простої причини: ніхто не зробив нічого «дивного». Docker робив своє. Хмара робила своє. Фаєрвол робив те, що йому сказали — по IPv4.
Виправлення було нудним і ефективним: default-drop входів по IPv6 на хості, явні allowlist-и для опублікованих сервісів і правило, що кожна публікація сервісу має вказувати адресу, а не використовувати неявний «всі інтерфейси».
Міні-історія 2: Оптимізація, яка відізвалася боком
Інша компанія серйозно взялася за латентність і прибрала крок з reverse proxy. Вони перейшли від «Nginx на хості термінує TLS і проксить у Docker-мережі» до «публікувати порти контейнерів прямо на хості, щоб менше кроків». Менше оверхеду, менше конфігів, менше випадків перезавантаження сертифікатів. На папері виглядало чисто.
Те, що вони виграли в мілісекундах, вони віддали в експозицію. Reverse proxy був привʼязаний до конкретних інтерфейсів, мав суворі allowlist-и і чітку IPv6-позицію. Пряме публікування нічого з цього не успадкувало. Кілька сервісів раптово слухали на [::], бо вузол був dual-stack. Декілька «внутрішніх» адмін-ендпоінтів стали доступні з будь-якої IPv6-мережі офісу — і з публічного інтернету в одному середовищі, де upstream-фільтри були ліберальними.
Першим симптомом навіть не була безпека. Це була дивна поведінка клієнтів: деякі офісні клієнти зʼєднувалися по IPv6, інші — по IPv4, а логування фіксувало лише v4-заголовки. Коли інцидент-респонс спробував простежити запит, вони ганялися за привидами: «Немає відповідного v4-джерела».
Вони відкотилися до проксі, а потім повторно ввели пряме публікування лише для ретельно відібраних сервісів з явними привʼязками адрес і паритетом фаєрволів для обох стеків. Оптимізація не була хибною; вона була неповною. Мережевий стек не оцінює ваші наміри.
Міні-історія 3: Нудна, але правильна практика, яка врятувала день
Фінтех-команда мала політику, що виглядала майже старомодно: кожна зміна публікації контейнерів вимагала автоматичної перевірки, яка порівнювала очікувані слухачі з фактичними. Це був по суті скриптований diff ss + docker ps, що запускався в CI для інфраструктурних змін. Інженери зітхали. Це ловило нудні питання. Це не було гламурно.
Під час стандартного перестроювання хоста базовий образ змінився і приніс собою дефолтну IPv6-конфігурацію плюс nftables. Docker і сервіси продовжували працювати, ніхто не помітив. Але перевірка позначила нового слухача: внутрішній метрик-ендпоінт тепер доступний на [::]:9100.
Команда поставилася до цього як до дефекту, а не цікавинки. Вони виправили, прив’язавши метрику до loopback і додавши nftables inet-правило, щоб за замовчуванням відкидати несподіваний вхід. Ні інциденту, ні пейджеру, ні екстреного вікна змін.
Це та профілактика, яка здається марною, поки не побачиш альтернативу. Це також практика, яку можна виправдати перед керівництвом без паніки: «Ми зупиняємо випадкову експозицію ще до випуску».
Шаблони жорсткого захисту, які дійсно працюють
Шаблон A: Явні привʼязки для кожного опублікованого порту
Якщо ви візьмете з цього статті лише одну річ — нехай це буде ось що: ніколи не публікуйте без явної адреси привʼязки.
- Сервіс лише для внутрішнього користування: публікуйте на
127.0.0.1(і опціонально::1, якщо вам дійсно потрібен IPv6 loopback). - Публічний сервіс: публікуйте на конкретну публічну адресу інтерфейсу (v4 і/або v6), а не на wildcard.
«Але Docker Compose не робить цього простим.» Робить. Просто треба бути явним.
Шаблон B: Default-deny для вхідних з’єднань по IPv6, а потім дозволяйте необхідне
Якщо хост має глобальну IPv6, ставтеся до нього так само серйозно, як до публічного IPv4. Така ж гігієна. Це означає:
- INPUT: відкидати вхідні, крім SSH (з allowlist-діапазонів), вашого reverse proxy і того, що дійсно потрібно.
- FORWARD: за замовчуванням drop; дозволяти встановлені потоки і тільки той форвардинг, який ви маєте на увазі для Docker-мереж.
- DOCKER-USER: розмістіть тут політику експозиції контейнерів, щоб оновлення Docker не «допомогли» скасувати ваш намір.
Шаблон C: Віддавайте перевагу єдиній площині політики (nftables inet) де можливо
Якщо ваша платформа коректно підтримує nftables, таблиця inet дає один набір правил, що застосовується і до IPv4, і до IPv6. Менше наборів правил — менше прогалин.
Це не робить вас автоматично безпечним. Це просто зменшує кількість місць, де можна забути зробити безпеку.
Шаблон D: Якщо вам не потрібен Docker IPv6 — вимкніть його навмисно
Це не ідеологічне твердження. Це операційне. Якщо у вас немає вимоги для IPv6 у контейнерах, ви купуєте складність без вигоди.
Вимкніть на рівні демона Docker і, за потреби, на рівні хоста. Хостове відключення може мати побічні ефекти в сучасних хмарах, тому робіть це з відкритими очима.
Шаблон E: Пропускайте публічний інгрес через одну контрольну точку
Reverse proxy або ingress controller — це не просто «ще один крок». Це місце, де ви централізуєте:
- TLS-політику та ротацію сертифікатів
- Аутентифікацію, rate limiting, обмеження розміру запитів
- Логи доступу, які можна реально корелювати
- Явну поведінку привʼязки v4/v6
Пряме публікування портів підходить для справді публічних сервісів з гарною гігієною. Але це пастка для «внутрішніх» сервісів, які ніколи не були спроєктовані для зовнішнього доступу.
Цитата (перефразована думка) від James Hamilton (Amazon/AWS reliability engineering): «Усе ламається; проектуйте так, щоб відмови були локалізовані і відновлювані.»
Жарт #2: Перше правило клубу IPv6 — ти не говориш про клуб IPv6. Друге правило — твій контейнер про нього говорить.
Типові помилки: симптом → корінна причина → виправлення
Ось ті, що я бачу найчастіше у реальних системах. Симптоми зазвичай вводять в оману, бо перевірки IPv4 виглядають в порядку.
1) Симптом: «Наш порт заблокований фаєрволом, але сканери все одно влучають»
Корінна причина: IPv4-фаєрвол налаштовано; IPv6 за замовчуванням дозволено (або upstream дозволяє). Сервіс опубліковано на [::].
Виправлення: Додайте еквівалентну IPv6-фільтрацію (переважно nftables inet), або явно привʼяжіть публікацію лише до IPv4, і перевірте ss -lntp.
2) Симптом: «Лише деякі клієнти можуть дістатися сервісу; логи не сходяться»
Корінна причина: Dual-stack клієнти іноді віддають перевагу IPv6. Ваша система спостереження і allowlist-и були лише для IPv4.
Виправлення: Переконайтесь, що логування фіксує віддалені v6-адреси, оновіть allowlist-и для IPv6-діапазонів або відключіть IPv6-експозицію для цього сервісу.
3) Симптом: «Ми вимкнули iptables-правила, але Docker все одно експонує порти»
Корінна причина: Userland-проксі або слухачі на хості все ще приймають зʼєднання; або існують правила nftables попри вигляд iptables.
Виправлення: Підтвердьте фактичні слухачі за допомогою ss. Перегляньте nftables через nft list ruleset. Не покладайтеся на вигляд лише одного інструменту.
4) Симптом: «Контейнер має ULA IPv6, але він доступний з інших мереж»
Корінна причина: ULA може маршрутизуватися всередині мережі. Це не «приватне» в сенсі NAT; це «не глобально призначене». Внутрішня маршрутизація зробила його доступним.
Виправлення: Фільтруйте на межах, обмежте маршрути або уникайте призначення IPv6 контейнерам, які його не потребують.
5) Симптом: «Ми додали правила DOCKER-USER; IPv6 все одно тече»
Корінна причина: Правила додані лише для iptables (IPv4), а не для ip6tables; або правила співпадають з неправильним інтерфейсом; або трафік потрапляє на INPUT (слухач хоста), а не в FORWARD.
Виправлення: Дзеркально застосуйте політику в ip6tables або використайте nftables inet. Підтвердьте, чи сокет — на рівні хоста (docker-proxy) чи форвардиться.
6) Симптом: «Вимкнення IPv6 зламало оновлення пакетів / метадані хмари»
Корінна причина: Ваше середовище очікує IPv6 для певних кінцевих точок, або DNS повертає AAAA першою і поведінка резолвера змінюється.
Виправлення: Не відключайте IPv6 на хості без тестування. Натомість тримайте IPv6 ввімкненим, але застосуйте default-deny inbound і явну публікацію портів.
Чеклісти / покроковий план
Крок за кроком: Заблокуйте Docker-хост, який випадково став dual-stack
-
Інвентар експозиції.
Запустітьss -lntpіdocker ps, щоб знайти опубліковані порти і v6-слухачів. -
Визначте позицію.
Виберіть одну: без IPv6, IPv6 лише внутрішній або IPv6 публічний для конкретних сервісів. -
Виправте привʼязки.
Оновіть Compose/definition сервісів, щоб публікувати з явними адресами (loopback для внутрішніх). -
Застосуйте політику у фаєрволі.
Використовуйте nftablesinet, де можливо; інакше дзеркально налаштуйте iptables/ip6tables. -
Використовуйте DOCKER-USER для політики форварду контейнерів.
Default-drop, allowlist для потрібного вхідного трафіку. -
Підтвердіть зовні.
Перевірте досяжність IPv6 за допомогою curl до глобальної IPv6 хоста і опублікованих портів. -
Закріпіть зміни.
Збережіть правила фаєрвола. Переконайтесь, що перезапуск Docker не зітре вашу політику. Додайте CI-перевірку, яка сигналізує про нові слухачі.
Чекліст: Що «достатньо безпечно» для більшості команд
- Кожен опублікований порт має вказану адресу привʼязки (без неявного wildcard).
- Політика фаєрвола — default-deny для вхідних IPv6 (та IPv4) з явними дозволами.
- Ланцюг DOCKER-USER існує і містить наміри організації (v4 і v6).
- Щонайменше одне зовнішнє сканування/health-check тестує досяжність IPv6, а не тільки IPv4.
- Логи включають віддалені IPv6-адреси, і система алертингу їх не втрачає при парсингу.
- Security groups / NACLs у хмарі включають правила IPv6, переглянуті так само ретельно, як IPv4.
Чекліст: Якщо ви дійсно хочете «жодного IPv6 тут»
- Демон Docker:
"ipv6": false, якщо не потрібно. - Хостовий фаєрвол блокує вхідний IPv6 незалежно (захист у глибину).
- Конфігурація хмарної мережі не призначає глобальну IPv6 інстансу, якщо не потрібно.
- Моніторинг продовжує працювати (деякі агенти можуть віддавати перевагу IPv6).
Питання й відповіді (FAQ)
1) Чи це баг Docker?
Зазвичай ні. Це Docker робить те, для чого його спроєктували: зробити мережу простою з мінімальною участю оператора. «Баг» — це припущення, що мінімальний ввід означає мінімальну експозицію.
2) Якщо мій контейнер слухає лише IPv4, чи може він бути доступним через IPv6?
Так, залежно від того, як хост публікує порт і чи проксі на хості приймає на v6 і форвардить на v4 локально. Підтверджуйте за допомогою ss на хості і реальних тестів підключення.
3) Чи достатньо привʼязки до 127.0.0.1?
Для захисту від IPv4 — так. Для IPv6 потрібно також переконатися, що ви не опублікували на [::]. У Compose будьте явними; не покладайтеся на дефолти. Перевіряйте через docker ps і docker port.
4) Чи варто вимкнути IPv6 скрізь, щоб бути в безпеці?
Якщо вам він не потрібен, відключення може зменшити ризик. Але загальне відключення на хості може зламати мережеві припущення хмари і ускладнити налагодження. Більш стійкий підхід: тримайте IPv6 ввімкненим, default-deny inbound і публікуйте порти явно.
5) Чому мій IPv6-фаєрвол виглядає порожнім, хоча я додав правила?
Можливо, ви дивитесь ip6tables, а система насправді використовує nftables, або навпаки. Завжди перевіряйте слухачі (ss) і інспектуйте ruleset nftables, якщо у вас iptables-nft.
6) Де найкраще впроваджувати політику входу контейнерів?
Ланцюг DOCKER-USER (або еквівалент у nftables) — найкращий гачок «перед тим, як Docker робить своє». Він призначений для політики оператора.
7) Чи є така ж проблема у Kubernetes?
Механіки інші, але той самий клас помилок. NodePorts, pod у hostNetwork і підтримка CNI для IPv6 можуть відкрити сервіси через IPv6, якщо фаєрвол вузла і правила хмари не симетричні.
8) Як довести собі, що я виправив проблему?
Три докази: (1) ss -lntp не показує несподіваних [::]-слухачів, (2) політика фаєрвола для IPv6 явна (default-deny або строгі allowlist-и), і (3) зовнішній тест підключення по IPv6 не проходить для портів, які мають бути приватними.
9) Якщо в Docker вказано “ipv6”: true — чи це автоматично робить сервіси публічними?
Не автоматично. Увімкнення IPv6 дає контейнерам v6-адреси на конфігурованих мережах і може встановлювати правила форвардингу. Публічна експозиція залежить від маршрутизації та фільтрації. Але це збільшує кількість способів, якими можна бути здивованим, тому поєднуйте з явною політикою.
Висновок: наступні кроки, які ви можете виконати
Витоки Docker через IPv6 не містичні. Це прогнозований результат двостекових хостів, неявних публікацій і фаєрвол-політик, які говорять лише IPv4. Ви не виправите це надією. Виправляєте явними привʼязками і правилами default-deny, що охоплюють обидва сімейства протоколів.
Зробіть наступне, у цьому порядку:
- Запустіть
ip -6 addr,ss -lntpіdocker ps, щоб виявити фактичну експозицію. - Визначте, чи потрібен IPv6 на цьому хості і в цих контейнерах.
- Зробіть публікацію явною (адреси привʼязки) і централізуйте публічний інгрес, коли можливо.
- Застосуйте політику в
DOCKER-USER(IPv4 і IPv6) або nftables inet із default-deny. - Перевірте зовні та додайте автоматизований чек, щоб це не повернулося тихо наступного кварталу.