Dlaczego Docker omija UFW i jak naprawić ten problem
Docker nadpisuje reguły iptables, przez co porty kontenerów są otwarte mimo ustawień UFW. Dowiedz się, jak wymusić filtrowanie ruchu i zabezpieczyć wystawione usługi.
Dlaczego Docker omija UFW
Docker omija UFW, ponieważ opublikowane porty kontenerów nigdy nie przechodzą przez reguły zapory zarządzane przez UFW. Po uruchomieniu 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 nadpisuje adres docelowy każdego pakietu na prywatny adres kontenera, zanim jądro podejmie decyzję o dalszej trasie pakietu. Tak zmodyfikowany pakiet jest następnie przekazywany do kontenera przez łańcuch FORWARD, którym zarządza Docker. Reguły UFW znajdują się w łańcuchu INPUT, do którego pakiet nigdy nie trafia. W rezultacie ufw status wskazuje domyślne odrzucanie ruchu, sudo ufw deny 8080 zgłasza sukces, a port 8080 nadal odpowiada na zapytania z całego Internetu.
Nie jest to błąd Dockera ani awaria UFW. Oba narzędzia konfigurują tę samą zaporę jądra. Reguły Dockera działają po prostu na wcześniejszym etapie ścieżki pakietu, więc UFW nie otrzymuje zapytania o przepuszczenie ruchu. Niniejszy przewodnik demonstruje to zjawisko, wyjaśnia mechanizm jego działania, a następnie omawia dwa skuteczne rozwiązania: publikowanie portów na 127.0.0.1 oraz filtrowanie w łańcuchu DOCKER-USER. Jeśli UFW jest nowym narzędziem, należy najpierw skonfigurować je zgodnie z przewodnikiem po podstawach zapory UFW, ponieważ domyślnie odrzucająca zapora stanowi właściwą podstawę dla wszystkich pozostałych usług na serwerze.
Weryfikacja obejścia zabezpieczeń na własnym serwerze
Należy rozpocząć od serwera VPS, na którym aktywny jest UFW z domyślną polityką odrzucania ruchu przychodzącego (deny). Następnie należy uruchomić kontener WWW z opublikowanym portem:
sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpineufw status verbose wskazuje Default: deny (incoming), allow (outgoing) oraz brak reguły dla portu 8080. Według raportu zapory sieciowej port jest zamknięty. Teraz należy przeprowadzić test z innej maszyny, nie z poziomu samego serwera:
curl -I http://your-vps-ip:8080/HTTP/1.1 200 OKKontener odpowiada na żądania. Należy dodać jawne regułę odrzucania (deny) i przetestować ponownie:
sudo ufw deny 8080/tcpPort nadal odpowiada, ponieważ reguła odrzucania znajduje się w łańcuchu, do którego pakiet nigdy nie trafia. UFW nie zawiódł. Zapora nie została nawet odpytana. Jest to również powód, dla którego problem jest trudny do wykrycia: nigdzie nie pojawia się komunikat o błędzie, wdrożenie przebiega poprawnie, a status zapory wygląda dokładnie tak, jak w przypadku poprawnie zabezpieczonego serwera.
Mechanizm: PREROUTING działa przed INPUT
Jądro przetwarza przychodzący pakiet w ustalonej kolejności i to właśnie w niej tkwi cały problem.
PREROUTINGdziała jako pierwszy. Reguły w tym miejscu mogą nadpisać adres docelowy pakietu, co dokładnie robi reguła Docker dla opublikowanego portu.- Następnie podejmowana jest decyzja o routingu. Pakiet zaadresowany do samego hosta trafia do łańcucha
INPUT. Pakiet zaadresowany do dowolnej 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 kontenera, który właśnie uruchomiono:
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 swój adres docelowy nadpisany na 172.17.0.2:80, czyli adres kontenera w prywatnej sieci bridge Docker. Po nadpisaniu pakiet nie jest już zaadresowany do hosta, więc decyzja o routingu kieruje go ścieżką FORWARD, gdzie Docker dodał już reguły akceptujące ruch w swoich sieciach. Twoja reguła deny 8080/tcp czeka w INPUT na pakiet, który nigdy nie dotrze.
W systemie Ubuntu 24.04 polecenie iptables stanowi interfejs dla nftables, jednak kolejność łańcuchów i wynik są identyczne. Zarówno UFW, jak i Docker zapisują reguły w tym samym potoku przetwarzania pakietów jądra, a punkt wejścia Docker znajduje się wcześniej. Nie jest to specyficzne tylko dla UFW: firewalld na serwerze VPS z systemem Rocky lub AlmaLinux filtruje ruch w tym samym punkcie potoku i jest omijany przez tę samą regułę DNAT, dlatego poniższe rozwiązania mają zastosowanie również w tym przypadku.
Codzienna naprawa: publikowanie portów na 127.0.0.1
Większość kontenerów w ogóle nie musi być publicznie dostępna. Baza danych, serwer aplikacji za odwrotnym proxy, panel administracyjny czy punkt końcowy metryk – żaden z tych elementów nie powinien odpowiadać bezpośrednio na zapytania z Internetu. Należy publikować je na adresie pętli zwrotnej (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, ponieważ reguła DNAT w Dockerze dopasowuje teraz tylko pakiety zaadresowane do 127.0.0.1, a pakiet z Internetu nigdy nie może legalnie posiadać takiego adresu docelowego, więc jądro odrzuca go, zanim uruchomiona zostanie jakakolwiek reguła firewalla. Port jest osiągalny z poziomu hosta, ale nie z żadnego innego miejsca. Zweryfikuj powiązanie:
sudo ss -tlnp | grep 8080W danych wyjściowych oczekiwany jest wynik 127.0.0.1:8080, a nie 0.0.0.0:8080 lub [::]:8080. Następnie potwierdź z innej maszyny, że połączenie na curl http://your-vps-ip:8080/ jest odrzucane.
Dla usług, które powinny być wystawione do Internetu, należy uruchomić jedno odwrotne proxy, które obsługuje porty 80 oraz 443 i kieruje ruch na podstawie nazwy hosta, nie publikując niczego innego. Jest to wzorzec, na którym opiera się przewodnik po odwrotnym proxy Traefik i w ten sposób aplikacja self-hosted, taka jak Nextcloud na VPS, pozostaje nieosiągalna inaczej niż przez swoje proxy. Sposób deklarowania wpisów ports: oraz reszta przepływu pracy w Compose zostały opisane w przewodniku po podstawach Docker Compose.
Gdy każdy wewnętrzny kontener korzysta z adresu loopback, UFW wraca do pełnienia swojej standardowej roli: ochrony portów, które udostępnia sam host. Skonfiguruj ten zestaw reguł, a następnie wykonaj polecenia w podanej kolejności:
Filtrowanie rzeczywiste: łańcuch DOCKER-USER
Czasami port kontenera musi pozostać opublikowany w sieci, ale z ograniczeniami, na przykład w przypadku portu repliki bazy danych, do którego dostęp może mieć 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 regułami akceptacji Dockera, a Docker nigdy nie zapisuje w nim własnych reguł. Łańcuch ten jest przeznaczony dla użytkownika, a Docker nie ingeruje w jego zawartość podczas restartów demona.
Jedna uwaga przed wykonaniem polecenia: w momencie, gdy pakiet dociera do DOCKER-USER, przepisanie DNAT już nastąpiło. Portem docelowym pakietu jest port kontenera (80 w naszym przykładzie), a nie port opublikowany (8080). Reguła dopasowująca --dport 8080 zatem niczego nie wykryje. Niezawodnym sposobem jest dopasowanie portu, z którym klient pierwotnie się łączył, co zapamiętuje mechanizm śledzenia połączeń jądra:
sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROPNależy to odczytywać następująco: dla pakietów, które weszły przez eth0 i należą do połączenia, którego pierwotnym portem docelowym był 8080, odrzuć wszystko, co nie zostało wysłane z 10.0.0.10. Dopasowanie --ctdir ORIGINAL ogranicza regułę do kierunku klient-kontener, dzięki czemu pakiety odpowiedzi nie są omyłkowo przechwytywane. Zastąp eth0 swoim interfejsem publicznym; ip route | grep default oznacza jego nazwę. Przetestuj to w ten sam sposób co poprzednio: curl z dozwolonego adresu kończy się sukcesem, a z każdego innego połączenie przekracza limit czasu. To zawieszenie jest sygnaturą reguły DROP wykonującej swoją pracę, w przeciwieństwie do portu, za którym nic nie stoi, a różnica między odrzuconym połączeniem a takim, którego limit czasu został przekroczony jest najszybszym sposobem na odróżnienie przefiltrowanego portu od usługi, która po prostu nie nasłuchuje.
Reguły dodane za pomocą polecenia iptables znikają po restarcie. Ponieważ UFW zarządza już tym firewallem, czystym miejscem na ich utrwalenie jest /etc/ufw/after.rules. Dołącz 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 i każdym uruchomieniu systemu, więc filtrowanie kontenerów znajduje się teraz w tym samym miejscu co reszta firewalla i przetrwa zarówno restart, jak i aktualizację Dockera.
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 firewalla Docker wykonują znacznie więcej zadań niż tylko publikowanie portów. Reguła maskarady (masquerade) zapewnia kontenerom dostęp do Internetu poprzez adres hosta, więc po wyłączeniu tej integracji kontenery nie będą mogły pobierać obrazów, łączyć się z repozytoriami pakietów ani wywoływać zewnętrznych API (application programming interface). Reguły DNAT odpowiadają za poprawne działanie -p, więc po ich wyłączeniu opublikowane porty przestaną działać całkowicie. Znikną również reguły izolacji, które utrzymują oddzielne sieci Compose w separacji. Naprawa tego obejścia poprzez zepsucie sieci kontenerów zmusi administratora do ręcznego tworzenia i utrzymywania każdej z tych reguł. Dokumentacja Docker opisuje to ustawienie jako przeznaczone wyłącznie dla osób, które zamierzają wykonać taką pracę samodzielnie. Łańcuch DOCKER-USER istnieje właśnie po to, aby nikt nie musiał korzystać z tego przełącznika.
Obsługa IPv6 w tym samym problemie
Najpierw należy sprawdzić, jak wygląda opublikowany port w protokole IPv6:
sudo ss -tlnp | grep 8080Począwszy od Docker Engine 27, Docker domyślnie zarządza ip6tables. W sieci Docker z włączoną obsługą IPv6, opublikowany port podlega takiemu samemu mechanizmowi DNAT w tablicach IPv6, co oznacza, że występuje tam to samo obejście zabezpieczeń i stosuje się to samo rozwiązanie: łańcuch DOCKER-USER istnieje również w ip6tables. Należy zatem odzwierciedlić regułę za pomocą sudo ip6tables -I DOCKER-USER ... i przetestować połączenie z zewnątrz, używając polecenia curl względem publicznego adresu IPv6 serwera, na przykład curl -6 http://[2001:db8:2a::1]:8080/.
W sieci bez obsługi IPv6, klienci IPv6 są obsługiwani przez docker-proxy, czyli standardowy proces działający w przestrzeni użytkownika, który nasłuchuje na [::]:8080 i przekazuje ruch do kontenera za pośrednictwem IPv4. Ruch do procesu hosta przechodzi przez INPUT, więc UFW może filtrować tę ścieżkę, ale tylko wtedy, gdy UFW w ogóle zarządza obsługą IPv6. Kwestia tego, czy tak jest, oraz inne sposoby, w jakie luka IPv6 może pojawić się na VPS, zostały opisane w przewodniku po UFW i IPv6.
Publikowanie na interfejsie zwrotnym (loopback) pozwala całkowicie ominąć ten problem: -p 127.0.0.1:8080:80 wiąże się wyłącznie z adresem zwrotnym IPv4, więc nie istnieje żaden proces nasłuchujący w IPv6 i nie ma możliwości dotarcia do usługi z zewnątrz w żadnym z tych stosów protokołów.
Wzorzec zapewniający bezpieczeństwo
- Publikuj każdy port wewnętrzny na
127.0.0.1, aby nigdy nie był on domyślnie wystawiony na zewnątrz. - Przekaż obsługę ruchu publicznego jednemu reverse proxy, które zajmuje porty 80 oraz 443.
- Utrzymuj domyślną politykę deny w UFW dla hosta, zezwalając jedynie na SSH oraz porty proxy.
- Filtruj faktycznie publiczne porty kontenerów w
DOCKER-USER, dopasowując je do oryginalnego portu docelowego i utrwalając w/etc/ufw/after.rules. - Pozostaw włączoną integrację Docker z iptables.
Skonfigurowanie tego raz eliminuje ryzyko niespodzianek: ufw status opisuje hosta, a DOCKER-USER opisuje kontenery. Żaden port nie zostanie opublikowany przypadkowo, a każde kolejne docker run -p, które wpiszesz, wystawi dokładnie to, co zamierzałeś.
FAQ
Dlaczego kontener Docker jest dostępny, mimo że UFW blokuje port?
Ponieważ Docker publikuje port za pomocą reguły DNAT w łańcuchu PREROUTING, która nadpisuje adres docelowy pakietu na adres kontenera, zanim nastąpi jakiekolwiek filtrowanie. Pakiet przemieszcza się następnie ścieżką FORWARD, a reguły UFW znajdują się w łańcuchu INPUT, do którego pakiet nigdy nie trafia. Firewall nie jest w ogóle sprawdzany, więc reguły blokujące nie mają wpływu na opublikowane porty kontenerów.
Jak sprawić, aby UFW blokował opublikowane porty Dockera?
Samo UFW nie może tego zrobić, 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, aby tylko host miał do niego dostęp, albo filtrować ruch w łańcuchu DOCKER-USER za pomocą reguły iptables, która dopasowuje oryginalny port docelowy poprzez conntrack. Regułę tę należy utrwalić w /etc/ufw/after.rules, aby przetrwała restarty oraz ufw reload.
Czy należy ustawić "iptables": false w pliku daemon.json Dockera?
Nie. To ustawienie usuwa wszystkie reguły firewalla i NAT Dockera, co powoduje znacznie więcej problemów niż samo obejście zabezpieczeń. Kontenery tracą dostęp do Internetu, ponieważ znika reguła maskarady, a opublikowane porty przestają działać, ponieważ usuwane są reguły DNAT. Należy stosować publikację na interfejsie zwrotnym (loopback) oraz łańcuch DOCKER-USER; rozwiązuje to problem wystawienia usług bez naruszania sieci kontenerów.
Czy Docker omija UFW również w przypadku IPv6?
W Docker Engine w wersji 27 i nowszych zarządzanie ip6tables jest domyślnie włączone, więc port opublikowany w sieci Docker z obsługą IPv6 jest przekierowywany z pominięciem UFW dokładnie tak samo jak w IPv4 i wymaga analogicznej reguły DOCKER-USER z użyciem ip6tables. W sieciach bez IPv6 proces docker-proxy nasłuchuje na [::] i ten ruch przechodzi przez INPUT, gdzie UFW może go filtrować, o ile UFW zarządza obsługą IPv6. Publikacja na 127.0.0.1 pozwala uniknąć obu tych przypadków, ponieważ na IPv6 nie nasłuchuje wówczas żadna usługa.