Адміністратор вмикає UFW, забороняє всі вхідні з'єднання, а порт контейнера все одно відкритий з інтернету. Це не помилка конфігурації UFW: Docker керує iptables сам, і трафік до опублікованих портів не доходить до правил, які пише UFW. Нижче — як побачити проблему та як закрити порти правильно.
Чому UFW не захищає опублікований порт
UFW додає правила в ланцюжок INPUT: він обробляє пакети, адресовані самому серверу. Коли ви публікуєте порт ключем -p, Docker створює правило DNAT у таблиці nat і перенаправляє пакети контейнеру. Такий трафік іде не на сам сервер, а транзитом через ланцюжок FORWARD, де першими стоять правила Docker. Правила UFW з INPUT до нього не застосовуються.
| Ланцюжок | Хто пише | Що обробляє |
|---|---|---|
| INPUT | UFW | Трафік до самого сервера |
| DOCKER-USER | Адміністратор | Транзитний трафік до контейнерів, перевіряється першим |
| DOCKER | Docker | Дозволи для опублікованих портів |
Як перевірити, що порт видно ззовні
Запустіть тестовий контейнер з опублікованим портом і подивіться на стовпець PORTS: запис 0.0.0.0:8080 означає, що порт слухає на всіх інтерфейсах. Потім перевірте доступність з іншого комп'ютера. Адресу в прикладі взято з діапазону для документації, підставте адресу свого сервера.
docker run -d --name web -p 8080:80 nginx
docker ps
ufw status
nc -zv 203.0.113.10 8080
Якщо з'єднання пройшло, а ufw status показує заборону, ви зіткнулися саме з цією поведінкою. Переглянути правила Docker можна так:
iptables -L DOCKER-USER -n -v --line-numbers
iptables -t nat -L DOCKER -n
iptables -L FORWARD -n -v
ss -tlnp
Спосіб перший: публікація лише на 127.0.0.1
Найпростіший і найнадійніший шлях — не відкривати порт у зовнішню мережу, якщо він потрібен лише локально, наприклад зворотному проксі-серверу на цьому ж хості. Вкажіть адресу прив'язки перед портом. Тоді зовнішній доступ до порту неможливий, а Nginx на хості, як і раніше, звертається до контейнера через 127.0.0.1.
docker run -d --name web -p 127.0.0.1:8080:80 nginx
services:
web:
image: nginx
ports:
- "127.0.0.1:8080:80"
Адресу прив'язки за замовчуванням можна задати й для всіх контейнерів параметром ip у файлі /etc/docker/daemon.json, після чого знадобиться перезапуск Docker. Параметр iptables зі значенням false вимикає керування правилами, але тоді ламається вихід контейнерів у мережу, тому як постійне рішення його не рекомендують.
Спосіб другий: правила в DOCKER-USER
Якщо порт має бути доступним лише з певних адрес, додавайте правила в ланцюжок DOCKER-USER: Docker не затирає його й перевіряє раніше за власні правила. На момент перевірки пакет уже пройшов DNAT, тому порт призначення змінився. Щоб відфільтрувати за початковим портом, використовуйте модуль conntrack із параметром --ctorigdstport.
iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL -j DROP
iptables -I DOCKER-USER -i eth0 -s 198.51.100.7 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL -j ACCEPT
iptables -L DOCKER-USER -n -v --line-numbers
Правило ACCEPT вставлено пізніше за DROP, тому воно опинилося вище й спрацює першим. Ці правила самі по собі не переживають перезавантаження: збережіть їх, наприклад пакетом iptables-persistent, і перевірте результат після перезапуску Docker і сервера. Докладніше про ланцюжки — у статті iptables: налаштування файрвола Linux з нуля.
Спосіб третій: ізольовані мережі Docker
Контейнерам, яким не потрібен зовнішній доступ, порти публікувати не треба. Бази даних і кеші досить підключити до спільної користувацької мережі: контейнери звертаються один до одного за іменем, а ззовні їх не видно. Мережа з ключем --internal додатково позбавлена виходу в інтернет.
docker network create --internal backend
docker run -d --name db --network backend mariadb
У Docker Compose те саме робиться блоком networks і параметром internal: true, подробиці — у статті Docker Compose: запуск стека застосунків. Базове налаштування файрвола описано в матеріалі UFW Firewall на VPS, а встановлення Docker — у статті Docker на VPS: встановлення та запуск контейнерів.
Часті питання
Чому Docker відкриває порти в обхід UFW?
Docker сам додає правила в iptables: DNAT у таблиці nat і дозволи в ланцюжку FORWARD. Трафік до контейнера йде транзитом, а не в ланцюжок INPUT, де працює UFW. Тому заборона в UFW для опублікованого порту не спрацьовує, і потрібні правила DOCKER-USER або прив'язка до 127.0.0.1.
Чи достатньо публікації на 127.0.0.1?
Якщо застосунок потрібен лише процесам на цьому ж сервері, наприклад Nginx у ролі зворотного проксі, прив'язки до 127.0.0.1 достатньо: порт не слухає зовнішні інтерфейси. Якщо доступ потрібен обраним зовнішнім адресам, використовуйте правила DOCKER-USER або закриту мережу, наприклад VPN.
Чи треба публікувати порти для зв'язку контейнерів між собою?
Ні. Контейнери в одній користувацькій мережі Docker звертаються один до одного за іменем служби та внутрішнім портом, публікація не потрібна. Порт публікують лише для доступу ззовні. Бази даних і черги краще тримати в мережі без публікації, тоді вони недоступні з інтернету.
Чи збережуться правила DOCKER-USER після перезавантаження?
Самі по собі ні: правила iptables зберігаються в пам'яті ядра. Збережіть їх штатним способом дистрибутива, наприклад пакетом iptables-persistent, і після перезавантаження перевірте командою iptables -L DOCKER-USER, що правила на місці й порядок не порушено запуском Docker.