Docker neden UFW kurallarını bypass eder?
Docker, iptables üzerinden DNAT kuralları yazarak UFW kurallarını atlar. Engellenen portların neden hala açık kaldığını ve kesin çözüm yollarını inceleyin.
Docker neden UFW'yi devre dışı bırakır
Docker, yayınlanan konteyner portları UFW tarafından yönetilen güvenlik duvarı kurallarından geçmediği için UFW'yi devre dışı bırakır. docker run -p 8080:80 çalıştırıldığında, Docker, çekirdeğin nat tablosundaki PREROUTING zincirine bir DNAT (destination network address translation) kuralı yazar. Bu kural, çekirdek paketin nereye gideceğine karar vermeden önce her paketin hedef adresini konteynerin özel adresiyle değiştirir. Yeniden yazılan paket, Docker tarafından kontrol edilen FORWARD zinciri üzerinden konteynere iletilir. UFW kuralları INPUT zincirinde bulunur ve paket bu zincire asla girmez. Bu nedenle ufw status varsayılan olarak engelleme gösterir, sudo ufw deny 8080 başarılı olduğunu raporlar ve 8080 portu tüm internete yanıt vermeye devam eder.
Bu bir Docker hatası değildir ve UFW bozuk değildir. Her iki araç da aynı çekirdek güvenlik duvarını programlar. Docker kuralları, paketin yolunun daha erken bir noktasında işlem yapar, bu yüzden UFW'ye hiçbir zaman danışılmaz. Bu kılavuz, bu durumu gösterir, mekanizmayı açıklar ve ardından çalışan iki çözümü ele alır: portları 127.0.0.1 üzerinde yayınlamak ve DOCKER-USER zincirinde filtreleme yapmak. Eğer UFW konusunda yeniyseniz, önce UFW güvenlik duvarı temelleri kılavuzu ile kurulum yapın; çünkü varsayılan olarak engelleme yapan bir güvenlik duvarı, sunucudaki diğer tüm işlemler için hala doğru temeldir.
Bypass işlemini kendi sunucunuzda gözlemleyin
Gelen trafik için varsayılan olarak engelleme (deny) politikası uygulayan ve UFW'nin aktif olduğu bir VPS üzerinden başlayın. Yayınlanmış bir port ile bir web container çalıştırın:
sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpineufw status verbose çıktısı Default: deny (incoming), allow (outgoing) değerini gösterir ve 8080 portu için herhangi bir kural bulunmaz. Güvenlik duvarının kendi raporuna göre port kapalıdır. Şimdi, sunucunun kendisinden değil, farklı bir makineden test edin:
curl -I http://your-vps-ip:8080/HTTP/1.1 200 OKContainer yanıt vermektedir. Açık bir engelleme (deny) kuralı ekleyin ve tekrar test edin:
sudo ufw deny 8080/tcpPort hâlâ yanıt vermektedir; çünkü engelleme kuralı, paketin hiçbir zaman uğramadığı bir zincirde (chain) yer almaktadır. UFW hata yapmamıştır. UFW'ye hiçbir zaman danışılmamıştır. Sorunun bu kadar iyi gizlenmesinin sebebi de budur: hiçbir yerde hata mesajı yazılmaz, dağıtım (deploy) başarılı olur ve güvenlik duvarı durumu çıktısı, sağlıklı ve kısıtlanmış bir sunucuyla tamamen aynı görünür.
Mekanizma: PREROUTING, INPUT'tan önce çalışır
Çekirdek, gelen bir paketi belirli bir sırayla işler; sorun bu sıralamadan kaynaklanmaktadır.
- Önce
PREROUTINGçalışır. Buradaki kurallar paketin hedef adresini yeniden yazabilir; Docker'ın yayınlanmış bir port için kullandığı kural tam olarak bunu yapar. - Sonra yönlendirme kararı gelir. Doğrudan ana makineye yöneltilen paketler
INPUTzincirine gider. Başka bir makineye yöneltilen paketler iseFORWARDzincirine gider. - UFW kuralları
INPUTiçinde yer alır. Docker kuralları iseFORWARDiçinde yer alır.
Yeni başlattığınız konteyner için Docker'ın kuralına bakın:
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:80Tüm süreç DNAT satırında gerçekleşir. 8080 portuna gelen her paketin hedef adresi, Docker'ın özel köprü ağındaki (bridge network) konteyner adresi olan 172.17.0.2:80 olarak yeniden yazılır. Yeniden yazma işleminden sonra paket artık ana makineye yöneltilmemiştir; bu nedenle yönlendirme kararı paketi FORWARD yoluna gönderir. Docker, kendi ağlarına trafik kabul etmek için bu yola zaten kurallar eklemiştir. deny 8080/tcp kuralınız ise INPUT içinde hiç gelmeyecek bir paketi beklemektedir.
Ubuntu 24.04 üzerinde iptables komutu nftables için bir arayüzdür, ancak zincir sırası ve sonuç aynıdır. Hem UFW hem de Docker aynı çekirdek paket hattına veri yazar ve Docker'ın giriş noktası daha erkendir.
Günlük çözüm: portları 127.0.0.1 üzerinde yayınlamak
Çoğu konteynerin başlangıçta halka açık olmasına gerek yoktur. Bir veritabanı, ters proxy arkasındaki bir uygulama sunucusu, bir yönetim paneli veya bir metrik uç noktası: bunların hiçbiri internete doğrudan yanıt vermemelidir. Bunları loopback adresi üzerinde yayınlayın:
docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpineVeya bir Compose dosyasında:
services:
web:
image: nginx:1.29-alpine
ports:
- "127.0.0.1:8080:80"Bu yöntem, Docker'ın DNAT kuralının artık yalnızca 127.0.0.1 adresine yönlendirilen paketlerle eşleşmesi sayesinde çalışır. İnternetten gelen bir paket asla bu hedef adresi taşıyamaz, bu nedenle çekirdek (kernel) herhangi bir güvenlik duvarı kuralı çalışmadan paketi düşürür. Port, yalnızca host üzerinden erişilebilirdir; başka hiçbir yerden erişilemez. Bağlamayı (binding) doğrulayın:
sudo ss -tlnp | grep 8080Çıktıda 0.0.0.0:8080 veya [::]:8080 değil, 127.0.0.1:8080 görülmelidir. Ardından, başka bir makineden curl http://your-vps-ip:8080/ bağlantısının reddedildiğini onaylayın.
İnternete açık olması gereken servisler için, 80 ve 443 portlarını kullanan ve hostname üzerinden yönlendirme yapan tek bir ters proxy çalıştırın ve başka hiçbir şeyi yayınlamayın. Bu, Traefik reverse proxy kılavuzu tarafından oluşturulan desendir; VPS üzerinde Nextcloud gibi kendi sunucunuzda barındırılan bir uygulamanın proxy dışında nasıl erişilemez kaldığının yöntemidir. ports: girişlerinin nasıl tanımlandığı ve Compose iş akışının geri kalanı Docker Compose temelleri kılavuzunda ele alınmaktadır.
Tüm dahili konteynerler loopback üzerinde olduğunda, UFW normal görevine döner: hostun kendisinin sunduğu portları korumak. Kural setini burada oluşturun ve ardından komutları sırasıyla çalıştırın:
Gerçek filtreleme: DOCKER-USER zinciri
Bazen bir konteyner portunun ağ üzerinde açık kalması ancak kısıtlanması gerekir; örneğin, yalnızca belirli bir ofis adresinin erişebilmesi gereken bir veritabanı replikasyon portu gibi. Bunun için Docker, DOCKER-USER zincirini sağlar. Herhangi bir konteynere giden her paket, Docker'ın kendi accept kurallarından önce DOCKER-USER üzerinden geçer ve Docker bu zincire asla kural yazmaz. Bu zincir kullanıcılar içindir ve Docker daemon yeniden başlatıldığında içeriği korunur.
Komuttan önce dikkat edilmesi gereken bir nokta: Bir paket DOCKER-USER noktasına ulaştığında, DNAT yeniden yazma işlemi zaten gerçekleşmiş olur. Paketin hedef portu, yayınlanan port (örneğimizde 8080) değil, konteyner portudur (örneğimizde 80). Bu nedenle --dport 8080 ile eşleşen bir kural hiçbir şeyi yakalamaz. Güvenilir yöntem, çekirdeğin (kernel) connection tracker mekanizmasının hatırladığı, istemcinin orijinal olarak bağlandığı port ile eşleşme yapmaktır:
sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROPBu kural şu anlama gelir: eth0 üzerinden gelen ve orijinal hedef portu 8080 olan bir bağlantıya ait paketler için, 10.0.0.10 dışından gönderilmeyen her şeyi düşür (drop). --ctdir ORIGINAL eşleşmesi, kuralı istemciden konteynere yönüyle sınırlandırır; böylece yanıt paketleri yanlışlıkla yakalanmaz. eth0 yerine kendi genel (public) arayüzünüzü yazın; ip route | grep default bunu tanımlar. Önceki gibi test edin: İzin verilen adresten yapılan curl başarılı olur, diğer tüm adreslerden ise bağlantı zaman aşımına uğrar.
iptables komutu ile eklenen kurallar yeniden başlatmada silinir. UFW bu güvenlik duvarını zaten yönettiği için, kuralları kalıcı hale getirmenin en uygun yeri /etc/ufw/after.rules konumudur. Dosyanın sonuna bir blok ekleyin:
*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
COMMITArdından sudo ufw reload komutunu çalıştırın. UFW, her yeniden yüklemede ve her açılışta bu dosyayı tekrar uygular; böylece konteyner filtrelemeniz artık güvenlik duvarınızın geri kalanıyla aynı yerde bulunur, yeniden başlatmalardan ve Docker güncellemelerinden etkilenmez.
Docker'ın iptables entegrasyonu neden devre dışı bırakılmamalıdır
Bu soruna yönelik eski yanıtlar, /etc/docker/daemon.json içinde { "iptables": false } ayarının yapılmasını önermektedir. Bunu yapmayın. Docker'ın güvenlik duvarı kuralları, port yayınlamaktan çok daha fazlasını yapar. Masquerade kuralı, konteynerlerin ana makine adresi üzerinden internete çıkış yapmasını sağlar; bu entegrasyon kapatıldığında konteynerler imaj çekemez, paket depolarına erişemez veya herhangi bir harici API'ye (application programming interface) bağlanamaz. DNAT kuralları -p işlevini yerine getirmesini sağlar, bu nedenle yayınlanan portlar tamamen çalışmaz hale gelir. Ayrı Compose ağlarını birbirinden ayıran izolasyon kuralları da devre dışı kalır. Bu bypass yöntemini uygulamak, konteyner ağ yapısını bozmak anlamına gelir ve tüm bu kuralları manuel olarak yazmak ve yönetmek zorunda kalırsınız. Docker dokümantasyonu, bu ayarın tam olarak bunu yapmak isteyen kişiler için olduğunu belirtir. DOCKER-USER zinciri, kimsenin bu anahtara ihtiyaç duymaması için mevcuttur.
Aynı problemin IPv6 tarafı
Öncelikle, yayınlanan portun IPv6 üzerindeki görünümünü kontrol edin:
sudo ss -tlnp | grep 8080Docker Engine 27 sürümünden itibaren Docker, ip6tables yapılandırmasını varsayılan olarak yönetmektedir. IPv6 etkinleştirilmiş bir Docker ağında, yayınlanan bir port IPv6 tablolarında aynı DNAT işlemine tabi tutulur. Bu nedenle aynı atlatma durumu burada da mevcuttur ve aynı çözüm geçerlidir: DOCKER-USER zinciri ip6tables içerisinde de bulunur. Kuralınızı sudo ip6tables -I DOCKER-USER ... ile aynısını yaparak kuralınızı yansıtın ve dışarıdan, örneğin sunucunuzun genel IPv6 adresi olan curl -6 http://[2001:db8:2a::1]:8080/ adresine curl ile test edin.
IPv6 bulunmayan bir ağda, IPv6 istemcileri bunun yerine [::]:8080 üzerinde dinleme yapan ve trafiği IPv4 üzerinden konteyner içine ileten docker-proxy adlı normal bir kullanıcı alanı süreci tarafından işlenir. Bir ana makine sürecine gelen trafik INPUT üzerinden geçer; bu nedenle UFW bu yolu filtreleyebilir, ancak bu yalnızca UFW'nin IPv6'yı yönetiyor olması durumunda mümkündür. Bu durumun geçerli olup olmadığı ve bir VPS üzerinde IPv6 boşluğunun oluşabileceği diğer yöntemler UFW ve IPv6 kılavuzu konusudur.
Loopback üzerinden yayın yapmak tüm sorunu devre dışı bırakır: -p 127.0.0.1:8080:80 yalnızca IPv4 loopback adresine bağlanır, bu nedenle bir IPv6 dinleyicisi bulunmaz ve her iki yığın üzerinden de dışarıdan erişilecek bir unsur yoktur.
Yapıyı ayakta tutan desen
- Tüm dahili portları
127.0.0.1üzerinde yayınlayın; böylece portlar en başta dış dünyaya açık olmaz. - 80 ve 443 portlarını kullanan tek bir reverse proxy'ye genel tarafı atayın.
- Host için UFW varsayılan olarak engelleme (deny) modunda kalsın; sadece SSH ve proxy portlarına izin verin.
- Gerçekten genel olan container portlarını, orijinal hedef portuna göre eşleşen ve
/etc/ufw/after.rulesiçinde saklananDOCKER-USERüzerinde filtreleyin. - Docker'ın iptables entegrasyonunu açık bırakın.
Bir kez kurulduğunda, bu yöntem sürprizleri ortadan kaldırır: ufw status host yapısını, DOCKER-USER ise container yapılarını tanımlar. Hiçbir şey kazara yayınlanmaz ve yazdığınız bir sonraki docker run -p komutu tam olarak amaçladığınız şeyi dışarı açar.
FAQ
UFW portu engellemesine rağmen Docker konteynerine neden erişebiliyorum?
Çünkü Docker, portu PREROUTING zincirinde bir DNAT kuralı ile yayınlar. Bu kural, herhangi bir filtreleme işlemi gerçekleşmeden önce paketin hedef adresini konteyner adresiyle değiştirir. Paket daha sonra FORWARD yolunu izler; UFW kuralları ise paketin hiçbir zaman girmediği INPUT zincirinde yer alır. Güvenlik duvarına danışılmadığı için, "deny" kuralları yayınlanmış konteyner portları üzerinde etkili olmaz.
UFW'nin Docker tarafından yayınlanan portları engellemesini nasıl sağlarım?
UFW bunu doğrudan yapamaz çünkü kuralları yanlış zincirdedir. Ya portu sadece hostun erişebileceği şekilde 127.0.0.1:8080:80 olarak yayınlayarak dışa açılmasını durdurun ya da conntrack aracılığıyla orijinal hedef portu eşleştiren bir iptables kuralı ile DOCKER-USER zincirinde filtreleme yapın. Bu kuralın yeniden başlatmalarda ve ufw reload durumlarında kalıcı olması için /etc/ufw/after.rules içinde saklanması gerekir.
Docker'ın daemon.json dosyasında "iptables": false ayarını yapmalı miyim?
Hayır. Bu ayar Docker'ın tüm firewall ve NAT kurallarını kaldırır; bu durum bypass durumundan çok daha büyük sorunlara yol açar. Masquerade kuralı kalktığı için konteynerler internete çıkış erişimini kaybeder ve DNAT kuralları silindiği için yayınlanmış portlar çalışmayı durdurur. Bunun yerine loopback yayınlama ve DOCKER-USER zincirini kullanın; bu yöntem konteyner ağını bozmadan maruz kalma sorununu çözer.
Docker, IPv6 üzerinde de UFW'yi bypass ediyor mu?
Docker Engine 27 ve sonraki sürümlerde, ip6tables yönetimi varsayılan olarak açıktır. Bu nedenle, IPv6 destekli bir Docker ağında yayınlanan bir port, IPv4'te olduğu gibi UFW'yi devre dışı bırakacak şekilde yeniden yazılır ve DOCKER-USER kuralının ip6tables ile aynen yansıtılması gerekir. IPv6 bulunmayan ağlarda ise docker-proxy süreci [::] üzerinde dinleme yapar ve bu trafik, UFW'nin IPv6'yı yönetmesi durumunda filtreleme yapabileceği INPUT üzerinden geçer. 127.0.0.1 üzerinde yayınlama yapmak her iki durumu da engeller, çünkü IPv6 üzerinde hiçbir şey dinleme yapmaz.