Docker omija UFW jak naprawić ten błąd
Docker tworzy reguły DNAT w iptables, które omijają reguły UFW. Dowiedz się, dlaczego porty są otwarte mimo blokady i jak użyć flagi --network host.
Dlaczego Docker omija UFW
Docker omija UFW, ponieważ publikowane porty kontenerów nie przechodzą przez reguły zarządzane przez UFW. Podczas uruchamiania docker run -p 8080:80, Docker zapisuje regułę DNAT (destination network address translation) w łańcuchu PREROUTING tabeli nat jądra systemu. Reguła ta zmienia adres docelowy każdego pakietu na prywatny adres kontenera, zanim jądro zdecyduje o kierunku przekazania pakietu. Przekształcony pakiet jest następnie przesyłany do kontenera przez łańcuch FORWARD, który jest kontrolowany przez Docker. Reguły UFW znajdują się w łańcuchu INPUT, a pakiet nigdy do niego nie trafia. W rezultacie ufw status pokazuje domyślne odrzucanie, sudo ufw deny 8080 raportuje sukces, a port 8080 pozostaje dostępny dla całego internetu.
Nie jest to błąd Docker, a UFW działa poprawnie. Oba narzędzia programują ten sam firewall jądra. Reguły Docker działają na wcześniejszym etapie ścieżki pakietu, przez co UFW nie jest wywoływane. Niniejszy poradnik demonstruje to ominięcie, wyjaśnia mechanizm działania, a następnie przedstawia dwa skuteczne rozwiązania: publikowanie portów na 127.0.0.1 oraz filtrowanie w łańcuchu DOCKER-USER. Jeśli UFW jest dla użytkownika nowym narzędziem, należy najpierw zapoznać się z podstawowymi zasadami działania firewalla UFW, ponieważ firewall z domyślnym odrzucaniem jest niezbędną podstawą dla wszystkich pozostałych usług na serwerze.
Sprawdź obejście na własnym serwerze
Rozpocznij od serwera VPS, na którym usługa UFW jest aktywna i posiada domyślną politykę deny dla ruchu przychodzącego. Uruchom kontener webowy z opublikowanym portem:
sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpineufw status verbose pokazuje Default: deny (incoming), allow (outgoing) i brak reguły dla portu 8080. Według raportu firewalla port jest zamknięty. Teraz przeprowadź test z innej maszyny, a nie z samego serwera:
curl -I http://your-vps-ip:8080/HTTP/1.1 200 OKKontener odpowiada. Dodaj jawną regułę deny i przeprowadź test ponownie:
sudo ufw deny 8080/tcpPort nadal odpowiada, ponieważ reguła deny znajduje się w łańcuchu, do którego pakiet nigdy nie dociera. UFW nie uległ awarii. Nie został wywołany. To sprawia, że problem jest trudny do wykrycia: nigdzie nie pojawia się błąd, wdrożenie działa poprawnie, a wyjście statusu firewalla wygląda dokładnie tak, jak w przypadku prawidłowo zabezpieczonego serwera.
Mechanizm: PREROUTING działa przed INPUT
Jądro przetwarza przychodzące pakiety w ściśle określonej kolejności. To właśnie ta kolejność stanowi przyczynę problemu.
- Najpierw działa
PREROUTING. Reguły w tym miejscu mogą zmienić adres docelowy pakietu; reguła Docker dla opublikowanego portu wykonuje właśnie taką operację. - Następnie następuje decyzja o trasowaniu (routing decision). Pakiet adresowany do samego hosta trafia do łańcucha
INPUT. Pakiet adresowany do innej maszyny trafia do łańcuchaFORWARD. - Reguły UFW znajdują się w
INPUT. Reguły Docker znajdują się wFORWARD.
Spójrz na regułę Docker dla nowo uruchomionego kontenera:
sudo iptables -t nat -L DOCKER -nChain DOCKER (2 references)
target prot opt source destination
RETURN 0 -- 0.0.0.0/0 0.0.0.0/0
DNAT 6 -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80Linia DNAT wyjaśnia całą sytuację. Każdy pakiet przychodzący na port 8080 ma adres docelowy zmieniany na 172.17.0.2:80, czyli adres kontenera w prywatnej sieci bridge Docker. Po tej zmianie pakiet nie jest już adresowany do hosta, więc decyzja o trasowaniu kieruje go ścieżką FORWARD. Docker dodał tam już reguły akceptujące ruch wewnątrz własnych sieci. Reguła deny 8080/tcp oczekuje w INPUT na pakiet, który nigdy nie dotrze.
W systemie Ubuntu 24.04 polecenie iptables jest interfejsem dla nftables, jednak kolejność łańcuchów i wynik operacji są identyczne. Zarówno UFW, jak i Docker zapisują reguły w tym samym potoku pakietów jądra, przy czym punkt wejścia Docker występuje wcześniej.
Standardowe rozwiązanie: publikowanie portów na 127.0.0.1
Większość kontenerów nie wymaga publicznego dostępu. Bazy danych, serwery aplikacji za reverse proxy, panele administracyjne lub punkty końcowe metryk nie powinny odpowiadać bezpośrednio z internetu. Należy publikować je na adresie loopback:
docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpineLub w pliku Compose:
services:
web:
image: nginx:1.29-alpine
ports:
- "127.0.0.1:8080:80"Działa to dlatego, że reguła DNAT w Dockerze dopasowuje teraz tylko pakiety adresowane do 127.0.0.1. Pakiet z internetu nie może mieć takiego adresu docelowego, więc jądro systemu odrzuca go przed przetworzeniem przez reguły firewall. Port jest dostępny wyłącznie z hosta. Aby zweryfikować wiązanie (binding):
sudo ss -tlnp | grep 8080W wyniku operacji należy uzyskać 127.0.0.1:8080, a nie 0.0.0.0:8080 lub [::]:8080. Następnie należy potwierdzić z innej maszyny, że połączenie dla curl http://your-vps-ip:8080/ jest odrzucane.
W przypadku usług wymagających dostępu z internetu należy uruchomić jeden reverse proxy obsługujący porty 80 i 443, który kieruje ruch na podstawie nazw hostów. Nie należy publikować żadnych innych portów. Taki wzorzec opisuje przewodnik po reverse proxy Traefik. Tak działa również Nextcloud na VPS, pozostając niedostępnym poza proxy. Sposób deklarowania wpisów ports: oraz pozostałe aspekty pracy z Docker Compose opisano w podstawach Docker Compose.
Gdy kontenery wewnętrzne są przypisane do loopback, UFW może pełnić swoją standardową funkcję: chronić porty obsługiwane przez hosta. Należy skonfigurować zestaw reguł tutaj, a następnie wykonać poniższe polecenia w podanej kolejności:
Real filtering: the DOCKER-USER chain
Czasami port kontenera musi pozostać opublikowany w sieci, ale musi być ograniczony. Przykładem jest port repliki bazy danych, do którego może uzyskać dostęp tylko jeden adres biurowy. W tym celu Docker udostępnia łańcuch DOCKER-USER. Każdy pakiet kierowany do dowolnego kontenera przechodzi przez DOCKER-USER przed własnymi regułami accept Docker. Docker nigdy nie dopisuje reguł do tego łańcucha. Łańcuch ten jest przeznaczony dla użytkownika, a Docker nie modyfikuje jego zawartości podczas restartów demona.
Ważna uwaga przed wykonaniem polecenia: w momencie, gdy pakiet dociera do DOCKER-USER, operacja DNAT rewrite została już wykonana. Port docelowy pakietu to port kontenera (w przykładzie 80), a nie port opublikowany (8080). Reguła dopasowująca do --dport 8080 nie zadziała. Niezawodną metodą jest dopasowanie portu, z którym pierwotnie połączył się klient, co jest zapamiętywane przez kernel connection tracker:
sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROPInterpretacja: dla pakietów, które weszły przez eth0 i należą do połączenia, którego pierwotny port docelowy wynosił 8080, odrzuć wszystko, co nie pochodzi z 10.0.0.10. Dopasowanie --ctdir ORIGINAL ogranicza regułę do kierunku klient-kontener, dzięki czemu pakiety odpowiedzi nie zostaną błędnie przechwycone. Zastąp eth0 swoim publicznym interfejsem; ip route | grep default określa jego nazwę. Przetestuj to w ten sam sposób co wcześniej: curl z dozwolonego adresu zakończy się sukcesem, natomiast z dowolnego innego adresu połączenie wygaśnie (timeout).
Reguły dodane za pomocą polecenia iptables znikają po restarcie systemu. Ponieważ UFW zarządza tym firewallem, właściwym miejscem na ich trwałe zapisanie jest /etc/ufw/after.rules. Dodaj blok na końcu pliku:
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP
COMMITNastępnie uruchom sudo ufw reload. UFW odtwarza ten plik przy każdym przeładowaniu oraz przy każdym uruchomieniu systemu. Dzięki temu filtrowanie kontenerów znajduje się w tym samym miejscu co reszta reguł firewall i jest odporne na restarty oraz aktualizacje Docker.
Dlaczego nie należy wyłączać integracji Docker z iptables
Starsze odpowiedzi na ten problem sugerują ustawienie { "iptables": false } w /etc/docker/daemon.json. Nie należy tego robić. Reguły firewall Docker wykonuje znacznie więcej niż tylko publikowanie portów. Reguła masquerade umożliwia kontenerom dostęp do internetu poprzez adres hosta. Po wyłączeniu integracji kontenery nie mogą pobierać obrazów, łączyć się z serwerami pakietów ani wywoływać zewnętrznych interfejsów API. Reguły DNAT umożliwiają działanie -p, więc po ich wyłączeniu publikowane porty przestaną działać. Znikną również reguły izolacji, które oddzielają od siebie sieci Compose. Rozwiązanie problemu obejścia zabezpieczeń spowoduje uszkodzenie sieci kontenerów, co wymusi ręczne tworzenie i utrzymywanie każdej z tych reguł. Dokumentacja Docker opisuje to ustawienie jako przeznaczone dla osób, które zamierzają wykonywać te czynności ręcznie. Łańcuch DOCKER-USER istnieje właśnie po to, aby nikt nie musiał używać tego przełącznika.
Problem po stronie IPv6
Najpierw sprawdź, jak wygląda opublikowany port w IPv6:
sudo ss -tlnp | grep 8080Od wersji Docker Engine 27 silnik Docker domyślnie zarządza ip6tables. W sieci Docker z włączonym IPv6 opublikowany port otrzymuje takie samo traktowanie DNAT w tabelach IPv6. Występuje tam ta sama luka, co wymaga tej samej poprawki: łańcuch DOCKER-USER również istnieje w ip6tables. Należy odzwierciedlić regułę za pomocą sudo ip6tables -I DOCKER-USER ... i przeprowadzić test zewnętrzny przy użyciu curl pod publiczny adres IPv6 serwera, na przykład curl -6 http://[2001:db8:2a::1]:8080/.
W sieciach bez IPv6 klienci IPv6 są obsługiwani przez docker-proxy. Jest to standardowy proces w przestrzeni użytkownika, który nasłuchuje na [::]:8080 i przekazuje ruch do kontenera przez IPv4. Ruch do procesu hosta przechodzi przez INPUT, więc UFW może filtrować tę ścieżkę, ale tylko wtedy, gdy UFW zarządza protokołem IPv6. Szczegóły dotyczące tego, czy tak jest, oraz inne sposoby powstawania luk IPv6 na VPS, opisano w przewodniku po UFW i IPv6.
Publikowanie na loopback rozwiązuje problem: -p 127.0.0.1:8080:80 wiąże tylko IPv4 loopback, więc nie powstaje nasłuchujący proces IPv6 i z zewnątrz nie można połączyć się z żadną ze stert.
Wzorzec zapewniający stabilność
- Publikować każdy port wewnętrzny na
127.0.0.1, aby uniknąć jego przypadkowego wystawienia. - Przypisać stronę publiczną jednemu reverse proxy obsługującemu porty 80 oraz 443.
- Ustawić domyślną blokadę (deny) w UFW dla hosta, zezwalając jedynie na porty SSH oraz proxy.
- Filtrować publiczne porty kontenerów w
DOCKER-USER, dopasowując je do oryginalnego portu docelowego zapisanego w/etc/ufw/after.rules. - Pozostawić włączoną integrację Docker z iptables.
Po jednorazowej konfiguracji eliminuje to nieprzewidziane skutki: ufw status opisuje hosta, a DOCKER-USER opisuje kontenery. Żaden port nie zostanie opublikowany przez przypadek, a każda kolejna docker run -p wystawi dokładnie te usługi, które zostały zamierzone.
FAQ
Dlaczego mogę uzyskać dostęp do kontenera Docker, mimo że UFW blokuje port?
Dzieje się tak, ponieważ Docker publikuje port za pomocą reguły DNAT w łańcuchu PREROUTING. Reguła ta zmienia adres docelowy pakietu na adres kontenera przed rozpoczęciem filtrowania. Pakiet przesyłany jest ścieżką FORWARD, natomiast reguły UFW znajdują się w łańcuchu INPUT, do którego pakiet nie trafia. Firewall nie jest sprawdzany, więc reguły typu deny nie mają wpływu na publikowane porty kontenerów.
Jak sprawić, aby UFW blokował publikowane porty Docker?
UFW nie może tego zrobić samodzielnie, ponieważ jego reguły znajdują się w niewłaściwym łańcuchu. Należy albo przestać wystawiać port, publikując go jako 127.0.0.1:8080:80 (dostępny tylko dla hosta), albo zastosować filtrowanie w łańcuchu DOCKER-USER za pomocą reguły iptables, która dopasowuje oryginalny port docelowy poprzez conntrack. Należy zapisać tę regułę w /etc/ufw/after.rules, aby zachowała się po ponownym uruchomieniu i ufw reload.
Czy należy ustawić "iptables": false w pliku Docker's daemon.json?
Nie. To ustawienie usuwa wszystkie reguły firewall i NAT generowane przez Docker, co powoduje więcej błędów niż samo obejście zabezpieczeń. Kontenery tracą dostęp do internetu, ponieważ reguła masquerade zostaje usunięta, a publikowane porty przestają działać z powodu braku reguł DNAT. Zamiast tego należy używać publikacji na loopback oraz łańcucha DOCKER-USER; rozwiązuje to problem ekspozycji bez przerywania pracy sieci kontenerów.
Czy Docker również omija UFW w przypadku IPv6?
W Docker Engine w wersji 27 i nowszych zarządzanie ip6tables jest włączone domyślnie. Port opublikowany w sieci Docker obsługującej IPv6 jest przekierowywany z pominięciem UFW dokładnie tak samo jak w IPv4 i wymaga lustrzanego odbicia reguły DOCKER-USER za pomocą ip6tables. W sieciach bez IPv6 proces docker-proxy nasłuchuje na [::], a ruch ten przechodzi przez INPUT, gdzie UFW może go filtrować, jeśli zarządza IPv6. Publikacja na 127.0.0.1 pozwala uniknąć obu przypadków, ponieważ w IPv6 nic nie nasłuchuje.