Docker UFW kurallarını neden atlıyor ve nasıl çözülür?
Docker container portları iptables üzerinden doğrudan yönlendirdiği için UFW kısıtlamaları geçersiz kalır. Güvenlik açığını kapatmak için DOCKER-USER zinciri yapılandırmasını öğrenin.
Docker neden UFW'yi atlar
Docker, yayınlanan container portları UFW tarafından yönetilen güvenlik duvarı kurallarından asla geçmediği için UFW'yi atlar. docker run -p 8080:80 komutunu çalıştırdığınızda, Docker çekirdeğin nat tablosundaki PREROUTING zincirine bir DNAT (hedef ağ adresi çevirisi) kuralı yazar. Bu kural, çekirdek paketin nereye gideceğine karar vermeden önce paketin hedef adresini container'ın özel adresine yeniden yazar. Yeniden yazılan paket, Docker'ın kontrol ettiği FORWARD zinciri üzerinden container'a iletilir. UFW'nin kuralları INPUT zincirinde yer alır ve paket bu zincire asla girmez. Bu nedenle ufw status varsayılan reddetme (deny) durumunu gösterse ve sudo ufw deny 8080 başarılı bir işlem rapor etse bile, 8080 numaralı port 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'ın kuralları, paketin yolunda daha erken bir noktada devreye girdiği için UFW'ye hiçbir zaman danışılmaz. Bu kılavuz, atlama durumunu gösterir, mekanizmayı açıklar ve ardından işe yarayan iki çözümü ele alır: portları 127.0.0.1 üzerinde yayınlamak ve DOCKER-USER zincirinde filtreleme yapmak. UFW sizin için yeniyse, önce UFW güvenlik duvarı temelleri kılavuzu ile kurulumu yapın; çünkü varsayılan olarak reddeden (default-deny) bir güvenlik duvarı, sunucudaki diğer her şey için hala en doğru temeldir.
Kendi sunucunuzda atlatmayı gözlemleyin
UFW'nin etkin olduğu ve gelen trafik için varsayılan olarak reddetme (deny) politikasının uygulandığı bir VPS ile başlayın. Yayınlanmış bir porta sahip 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 komutu Default: deny (incoming), allow (outgoing) çıktısını verir ve 8080 numaralı port için herhangi bir kural görünmez. 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 verir. Açık bir reddetme kuralı ekleyin ve tekrar test edin:
sudo ufw deny 8080/tcpPort hala yanıt vermektedir, çünkü reddetme kuralı paketin asla uğramadığı bir zincirde yer almaktadır. UFW başarısız olmamıştır. Kurala hiçbir zaman başvurulmamıştır. Sorunun bu kadar iyi gizlenmesinin nedeni de budur: hiçbir yerde hata mesajı yazmaz, dağıtım çalışır ve güvenlik duvarı durum çıktısı, tamamen güvenli ve kilitli bir sunucu gibi görünür.
Mekanizma: PREROUTING, INPUT'tan önce çalışır
Çekirdek, gelen bir paketi sabit bir sırada işler ve tüm sorun bu sıradan kaynaklanır.
PREROUTINGilk çalışır. Buradaki kurallar paketin hedef adresini yeniden yazabilir; Docker'ın yayınlanan bir port için oluşturduğu kural tam olarak bunu yapar.- Ardından yönlendirme kararı gelir. Doğrudan sunucuya adreslenen bir paket
INPUTzincirine gider. Başka bir makineye adreslenen bir paket iseFORWARDzincirine gider. - UFW kuralları
INPUTiçinde yer alır. Docker kuralları iseFORWARDiçinde bulunur.
Yeni başlattığınız container 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:80DNAT satırı olayın tamamını açıklar. 8080 numaralı porta gelen herhangi bir paketin hedefi, Docker'ın özel bridge ağı üzerindeki container adresi olan 172.17.0.2:80 olarak yeniden yazılır. Yeniden yazma işleminden sonra paket artık sunucuya adreslenmiş değildir; bu nedenle yönlendirme kararı paketi, Docker'ın kendi ağlarına gelen trafiği kabul eden kuralları eklediği FORWARD yoluna gönderir. Sizin deny 8080/tcp kuralınız, asla gelmeyecek bir paket için INPUT içinde bekler.
Ubuntu 24.04 üzerinde iptables komutu, nftables üzerinde bir arayüz görevi görür ancak zincir sırası ve sonuç aynıdır. UFW ve Docker, aynı çekirdek paket hattına yazım yapar ve Docker'ın giriş noktası daha öncedir. Bunların hiçbiri sadece UFW'ye özgü değildir: Rocky veya AlmaLinux VPS üzerinde firewalld, aynı hat üzerinde filtreleme yapar ve aynı DNAT kuralı tarafından devre dışı bırakılır; bu nedenle aşağıdaki çözümler orada da geçerlidir.
Günlük çözüm: portları 127.0.0.1 üzerinde yayınlamak
Çoğu container'ın en başından beri dış dünyaya açık olması gerekmez. Bir veritabanı, reverse proxy arkasındaki bir uygulama sunucusu, bir yönetim paneli veya metrik uç noktası; bunların hiçbiri doğrudan internete 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 işe yarar çünkü Docker'ın DNAT kuralı artık yalnızca 127.0.0.1 adresine gönderilen paketlerle eşleşir ve internetten gelen bir paket hiçbir zaman meşru olarak bu hedef adresini taşıyamaz; bu nedenle çekirdek, herhangi bir güvenlik duvarı kuralı çalışmadan önce paketi düşürür. Port, yalnızca ana makineden erişilebilir durumdadır, başka hiçbir yerden erişilemez. Bağlantıyı 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örmeniz gerekir. Ardından başka bir makineden curl http://your-vps-ip:8080/ adresine erişimin reddedildiğini teyit edin.
İnternete açık olması gereken servisler için 80 ve 443 numaralı portları yöneten ve ana bilgisayar adına göre yönlendirme yapan tek bir reverse proxy çalıştırın ve başka hiçbir şeyi dışarıya açmayın. Traefik reverse proxy rehberi tarafından oluşturulan yapı bu modeldir ve VPS üzerinde Nextcloud gibi self-hosted bir uygulamanın proxy dışında erişilemez kalması bu şekilde sağlanır. ports: girdilerinin nasıl tanımlandığı ve Compose iş akışının geri kalanı Docker Compose temel rehberi içerisinde ele alınmıştır.
Tüm dahili container'lar loopback üzerinde olduğunda, UFW tekrar kendi normal işini yapmaya başlar: ana makinenin bizzat sunduğu portları korumak. Kural setini burada oluşturun, ardından komutları sırasıyla çalıştırın:
Gerçek filtreleme: DOCKER-USER zinciri
Bazen bir container portunun ağa açık kalması ancak kısıtlanması gerekir; örneğin sadece tek bir ofis adresinin erişebileceği bir veritabanı replika portu gibi. Docker bunun için DOCKER-USER zincirini sağlar. Herhangi bir container'a giden her paket, Docker'ın kendi kabul kurallarından önce DOCKER-USER içinden geçer ve Docker bu zincire asla kural yazmaz. Bu zincir size özeldir ve Docker daemon yeniden başlatıldığında içeriğine dokunmaz.
Komuttan önce bir tuzak: Bir paket DOCKER-USER noktasına ulaştığında, DNAT yeniden yazma işlemi çoktan gerçekleşmiştir. Paketin hedef portu, yayınlanan port (8080) değil, container portudur (örneğimizde 80). Bu nedenle --dport 8080 ile eşleşen bir kural hiçbir şeyle eşleşmez. Güvenilir yöntem, çekirdeğin bağlantı izleyicisinin hatırladığı, istemcinin orijinal olarak bağlandığı portu eşleştirmektir:
sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROPBunu şu şekilde okuyun: eth0 üzerinden gelen ve orijinal hedef portu 8080 olan bir bağlantıya ait paketler için, 10.0.0.10 adresinden gönderilmeyen her şeyi düşür (drop). --ctdir ORIGINAL eşleşmesi, kuralı istemciden container'a doğru olan yönle sınırlar; böylece yanıt paketleri yanlışlıkla yakalanmaz. eth0 kısmını genel ağ arayüzünüzle değiştirin; ip route | grep default bunu adlandırır. Testi daha önce olduğu gibi yapın: İzin verilen adresten curl başarılı olur, başka herhangi bir yerden ise bağlantı zaman aşımına uğrar. Bu takılma, arkasında hiçbir şey olmayan bir porttan ziyade, görevini yapan bir DROP kuralının imzasıdır ve reddedilen bir bağlantı ile zaman aşımına uğrayan bağlantı arasındaki fark, filtrelenmiş bir portu, dinleme yapmayan bir servisten ayırmanın en hızlı yoludur.
iptables komutuyla eklenen kurallar yeniden başlatmada kaybolur. UFW bu güvenlik duvarını zaten yönettiğinden, kuralları kalıcı hale getirmek için en temiz yer /etc/ufw/after.rules dosyasıdır. 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 işler; böylece container filtrelemeniz artık güvenlik duvarınızın geri kalanıyla aynı yerde bulunur ve hem yeniden başlatmalardan hem de Docker güncellemelerinden etkilenmez.
Docker'ın iptables entegrasyonunu neden devre dışı bırakmamalısınız
Bu soruna yönelik eski yanıtlar { "iptables": false } ayarının /etc/docker/daemon.json içinde yapılmasını önermektedir. Bunu yapmayın. Docker'ın güvenlik duvarı kuralları, yalnızca portları dışarı açmaktan çok daha fazlasını yapar. Masquerade kuralı, container'ların ana makinenin adresi üzerinden internete çıkış yapmasını sağlar; dolayısıyla bu entegrasyon kapalıyken container'lar imaj indiremez, paket depolarına erişemez veya herhangi bir harici API (uygulama programlama arayüzü) çağrısı yapamaz. DNAT kuralları, -p mekanizmasının çalışmasını sağlayan temel unsurdur; bu kurallar olmadan yayınlanan portlar tamamen işlevsiz hale gelir. Ayrıca, farklı Compose ağlarını birbirinden ayıran izolasyon kuralları da devre dışı kalır. Bu bypass işlemini düzeltmek için container ağ yapısını bozmuş olursunuz ve tüm bu kuralları manuel olarak yazıp yönetmek zorunda kalırsınız. Docker'ın kendi dokümantasyonu, bu ayarı tam olarak bunu yapmayı amaçlayan kişiler için tanımlamaktadır. DOCKER-USER zinciri, tam olarak kimsenin bu anahtara ihtiyaç duymaması için var olmuştur.
Aynı sorunun IPv6 tarafı
Önce yayınlanan portun IPv6 üzerinde nasıl göründüğünü kontrol edin:
sudo ss -tlnp | grep 8080Docker Engine 27 sürümünden itibaren Docker, ip6tables yönetimini varsayılan olarak üstlenir. IPv6 etkinleştirilmiş bir Docker ağında, yayınlanan bir port IPv6 tablolarında da aynı DNAT işlemine tabi tutulur; bu nedenle aynı atlatma durumu orada da mevcuttur ve aynı çözüm geçerlidir: DOCKER-USER zinciri ip6tables içinde de bulunur, bu yüzden kuralınızı sudo ip6tables -I DOCKER-USER ... ile yansıtın ve sunucunuzun genel IPv6 adresiyle dışarıdan curl kullanarak test edin, örneğin curl -6 http://[2001:db8:2a::1]:8080/.
IPv6 olmayan bir ağda, IPv6 istemcileri bunun yerine [::]:8080 üzerinde dinleme yapan ve trafiği IPv4 üzerinden container içine ileten bir kullanıcı alanı süreci olan docker-proxy tarafından işlenir. Bir ana makine sürecine giden trafik INPUT üzerinden geçer, bu nedenle UFW bu yolu filtreleyebilir ancak yalnızca UFW IPv6'yı yönetiyorsa bu mümkündür. UFW'nin bunu yapıp yapmadığı ve bir VPS üzerinde IPv6 açığının oluşmasına neden olan diğer yollar UFW ve IPv6 rehberi konusudur.
Loopback üzerinde yayın yapmak tüm bu sorunu devre dışı bırakır: -p 127.0.0.1:8080:80 yalnızca IPv4 loopback adresine bağlanır, bu nedenle IPv6 dinleyicisi yoktur ve her iki yığın üzerinden de dışarıdan ulaşılabilecek bir nokta bulunmaz.
Sürdürülebilir yapı
- Tüm dahili portları
127.0.0.1üzerinde yayımlayın; böylece hiçbir port dış dünyaya doğrudan açılmaz. - Genel ağ tarafını, 80 ve 443 numaralı portları yöneten tek bir reverse proxy'ye atayın.
- UFW üzerinde varsayılan engelleme kuralını koruyun; yalnızca SSH ve proxy portlarına izin verin.
- Genel erişime açık container portlarını
DOCKER-USERiçerisinde, orijinal hedef port ile eşleştirerek filtreleyin ve/etc/ufw/after.rulesüzerinde kalıcı hale getirin. - Docker'ın iptables entegrasyonunu açık bırakın.
Bu yapı bir kez kurulduğunda belirsizlikleri ortadan kaldırır: ufw status ana makineyi, DOCKER-USER ise container'ları tanımlar. Hiçbir port yanlışlıkla dışarı açılmaz ve yazdığınız bir sonraki docker run -p komutu tam olarak amaçladığınız portu dış dünyaya açar.
FAQ
UFW portu engellediği halde Docker container'ına neden erişebiliyorum?
Çünkü Docker, portu PREROUTING zincirinde bir DNAT kuralı ile yayınlar. Bu kural, paket henüz herhangi bir filtreleme işlemine tabi tutulmadan önce paketin hedef adresini container'ın adresine yeniden yazar. Paket daha sonra FORWARD yolunu izler; UFW kuralları ise paketin hiçbir zaman girmediği INPUT zincirinde yer alır. Güvenlik duvarına hiçbir zaman danışılmaz, bu nedenle UFW'nin reddetme kuralları yayınlanan container portları üzerinde etkisiz kalır.
Docker'ın yayınladığı portları UFW ile nasıl engelleyebilirim?
UFW'nin kuralları yanlış zincirde olduğu için bunu tek başına yapamaz. Portu dış dünyaya açmaktan vazgeçin ve sadece ana makinenin erişebilmesi için 127.0.0.1:8080:80 olarak yayınlayın 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şlatmalardan ve ufw reload işlemlerinden sonra da kalıcı olması için /etc/ufw/after.rules dosyasına ekleyin.
Docker'ın daemon.json dosyasında "iptables": false ayarını yapmalı mıyım?
Hayır. Bu ayar, Docker'ın tüm güvenlik duvarı ve NAT kurallarını kaldırır; bu da sadece bypass sorunundan çok daha fazlasına yol açar. Masquerade kuralı silindiği için container'lar dış dünyaya erişimini kaybeder ve DNAT kuralları silindiği için yayınlanan portlar çalışmaz hale gelir. Bunun yerine loopback üzerinden yayınlama yöntemini ve DOCKER-USER zincirini kullanın; bu yöntemler container ağını bozmadan erişim 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 özellikli bir Docker ağında yayınlanan bir port, tıpkı IPv4'te olduğu gibi UFW'yi devre dışı bırakacak şekilde yeniden yazılır ve aynı DOCKER-USER kuralının ip6tables ile yansıtılması gerekir. IPv6 olmayan ağlarda ise docker-proxy süreci [::] üzerinde dinleme yapar ve bu trafik INPUT üzerinden geçer; UFW, IPv6'yı yönetiyorsa bu trafiği filtreleyebilir. Portu 127.0.0.1 üzerinde yayınlamak her iki durumu da engeller, çünkü bu durumda IPv6 üzerinde hiçbir şey dinleme yapmaz.