SSD Nodes Learn Hosting plans →
Rehberler Matt ConnorYazan Matt Connor

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 kendi DOCKER zincirine bir DNAT (hedef ağ adresi çevirisi) kuralı yazar; PREROUTING zinciri de bu zincire atlama yapar. 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 hiçbir zaman girmez. Bu nedenle ufw status varsayılan reddetme kuralını gösterse ve sudo ufw deny 8080 başarılı olduğunu bildirse 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 düzeltmeyi ele alır: portları 127.0.0.1 üzerinde yayınlamak ve DOCKER-USER zincirinde filtreleme yapmak. Eğer UFW sizin için yeniyse, önce UFW güvenlik duvarı temelleri kılavuzu ile kurulumu yapın; çünkü varsayılan olarak reddeden bir güvenlik duvarı, sunucudaki diğer her şey için hala 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-alpine

ufw 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 OK

Container yanıt verir. Açık bir reddetme kuralı ekleyin ve tekrar test edin:

sudo ufw deny 8080/tcp

Port 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ıra içinde yaşanır.

  1. PREROUTING ilk sırada çalışır. Buradan ulaşılan kurallar paketin hedef adresini yeniden yazabilir; Docker'ın yayınlanan bir port için PREROUTING zincirinin atladığı DOCKER zincirindeki kuralı tam olarak bunu yapar.
  2. Yönlendirme kararı bir sonraki aşamadır. Doğrudan sunucuya adreslenen bir paket INPUT zincirine gider. Başka bir makineye adreslenen paket ise FORWARD zincirine yönlendirilir.
  3. UFW kuralları INPUT içinde bulunur. Docker kuralları ise FORWARD içinde yer alır.

Yeni başlattığınız container için Docker'ın kuralını inceleyin:

sudo iptables -t nat -L DOCKER -n
Chain 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:80

DNAT satırı tüm hikayeyi özetler. 8080 numaralı porta gelen herhangi bir paketin hedefi, Docker'ın özel köprü ağındaki container adresi olan 172.17.0.2:80 olarak yeniden yazılır. Yeniden yazma işleminden sonra paket artık sunucuya adreslenmiş durumda değildir; bu nedenle yönlendirme kararı paketi, Docker'ın kendi ağlarına trafiği kabul eden kuralları önceden eklediği FORWARD yoluna gönderir. Sizin deny 8080/tcp kuralınız ise INPUT içinde asla gelmeyecek bir paketi 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 baypas edilir; 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 aslında dış dünyaya açık olması gerekmez. Bir veritabanı, reverse proxy arkasındaki bir uygulama sunucusu, 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-alpine

Veya 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 yönlendirilen paketlerle eşleşir. Docker Engine 28.0.0 ve sonraki sürümlerde Docker, raw tablosunun PREROUTING zincirine, loopback dışındaki bir arayüzden gelen ve bu loopback portuna yönelik tüm paketleri düşüren bir kural ekler. Bu sürümlerde port yalnızca ana bilgisayardan erişilebilirdir, başka hiçbir yerden erişilemez. 28.0.0 sürümünden eski sürümlerde Docker, aynı L2 segmentindeki ana bilgisayarların (örneğin aynı ağ anahtarına bağlı makinelerin) localhost üzerinde yayınlanan portlara erişebileceğini belirtir; bu nedenle buna güvenmeden önce docker version dosyasını kontrol edin ve yükseltme yapın. Bağlamayı 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örmelisiniz. Ardından başka bir makineden curl http://your-vps-ip:8080/ adresinin yanıt vermediğini onaylayın. Genel IP adresinize gönderilen bir paket artık Docker tarafından yeniden yazılmaz; bu nedenle paket INPUT noktasına ulaşır ve burada UFW'nin varsayılan reddetme politikası ile daha önce eklenen deny 8080/tcp kuralı paketi sessizce düşürür. Bu yüzden curl komutu "connection refused" hatası yerine bağlantı zaman aşımına uğrayana kadar bekler.

İnternete açık olması gereken servisler için 80 ve 443 numaralı portları yöneten tek bir reverse proxy çalıştırın ve başka hiçbir portu dışarıya açmayın. Traefik reverse proxy rehberi bu modeli temel alır; VPS üzerinde Nextcloud gibi self-hosted uygulamalar da ancak proxy üzerinden erişilebilir kalarak bu şekilde korunur. 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 görevine döner: ana bilgisayarın bizzat sunduğu portları korumak. Kural setini burada oluşturun ve ardından komutları sırasıyla çalıştırın:

ToolUFW rule generator

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. Bu zincir sizin kurallarınız içindir; Docker, daemon yeniden başlatılsa dahi bu kuralları silmez veya değiştirmez.

Bu zincir, Docker'ın varsayılan iptables güvenlik duvarı altyapısına aittir. Docker Engine 29, firewall-backend daemon seçeneği ile etkinleştirilmesi gereken deneysel bir nftables altyapısı eklemiştir ve bu yapıda DOCKER-USER zinciri bulunmaz; özel filtreleme, forward hook üzerinde bir temel zincir ile kendi oluşturduğunuz ayrı bir nftables tablosunda yapılır. Eğer bu seçeneği değiştirmediyseniz iptables kullanıyorsunuz demektir ve aşağıdaki her şey geçerlidir. İki araç arasındaki fark iptables ve nftables rehberi içerisinde ele alınmıştır.

Komuttan önce bir uyarı: 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 başlangıçta 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 DROP

Bunu ş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 isimlendirir. Testi daha önceki gibi yapın: curl ile izin verilen adresten yapılan bağlantı başarılı olur, başka herhangi bir yerden yapılan bağlantı ise 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 işaretidir 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ırt etmenin en hızlı yoludur.

iptables komutuyla eklenen kurallar yeniden başlatma sonrasında kaybolur. UFW bu güvenlik duvarını zaten yönettiği için, kuralları kalıcı hale getirmenin temiz yolu /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
COMMIT

Ardı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 iptables entegrasyonunu neden devre dışı bırakmamalısınız

Bu soruna yönelik eski yanıtlar, /etc/docker/daemon.json dosyasında { "iptables": false } ayarının 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 dış internete erişmesini sağlar; dolayısıyla entegrasyon kapalı olduğunda container'lar paket depolarına ulaşamaz veya herhangi bir harici API (application programming interface) çağrısı yapamaz. DNAT ve izolasyon kuralları da devre dışı kalır ve Docker belgeleri, herhangi bir alternatif kural tanımlanmadığı takdirde container portlarının yerel ağdaki diğer makineler tarafından erişilebilir hale geleceği konusunda uyarır. Bu bypass sorununu çözmeye çalışırken 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 belgeleri, bu ayarın çoğu kullanıcı için uygun olmadığını belirtir ve kuralların sonradan manuel olarak oluşturulmasını son derece karmaşık bir süreç olarak tanımlar. DOCKER-USER zinciri, tam olarak kimsenin bu anahtara ihtiyaç duymaması için mevcuttur.

Aynı sorunun IPv6 tarafı

Öncelikle yayınlanan portun IPv6 üzerinde nasıl göründüğünü kontrol edin:

sudo ss -tlnp | grep 8080

Docker 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 aynı DNAT işlemine tabi tutulur. Dolayısıyla aynı atlatma açığı burada da mevcuttur ve aynı çözüm uygulanır: 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 test edin; örneğin curl -6 http://[2001:db8:2a::1]:8080/. Bu durum Docker'ın varsayılan iptables altyapısı için geçerlidir; deneysel nftables altyapısında her iki yığında da DOCKER-USER zinciri bulunmaz ve IPv6 filtreleme işlemleri kendi ip6 nftables tablonuzda gerçekleştirilir.

IPv6 bulunmayan bir ağda, IPv6 istemcileri bunun yerine [::]:8080 üzerinde dinleme yapan ve trafiği IPv4 üzerinden container içine ileten normal bir kullanıcı alanı süreci olan docker-proxy tarafından karşılanır. Bir ana makine sürecine giden trafik INPUT üzerinden geçer, bu nedenle UFW bu yolu filtreleyebilir; ancak bu yalnızca UFW IPv6'yı yönettiğinde mümkündür. UFW'nin IPv6'yı yönetip yönetmediği ve bir VPS üzerinde IPv6 açığının oluşabileceği diğer durumlar 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 oluşmaz ve Docker Engine 28.0.0 ve sonraki sürümlerde her iki yığında da dışarıdan erişilebilecek bir nokta kalmaz.

Sürdürülebilir yapı

  • Docker Engine 28.0.0 veya sonraki sürümlerde, hiçbir portun dışarıya sızmaması için tüm dahili portları 127.0.0.1 üzerinde yayımlayın (daha eski sürümler, aynı L2 segmentindeki ana bilgisayarların bu portlara erişmesine izin verir).
  • Dış dünyaya bakan tarafı, 80 ve 443 numaralı portları yöneten tek bir reverse proxy'ye atayın.
  • Ana bilgisayar için UFW varsayılan reddetme politikasını koruyun; yalnızca SSH ve proxy portlarına izin verin.
  • Gerçekten herkese açık olması gereken container portlarını, Docker'ın varsayılan iptables altyapısı üzerinde, orijinal hedef port ile eşleşecek şekilde DOCKER-USER içinde filtreleyin ve bu ayarları /etc/ufw/after.rules içinde 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 bilgisayarı, DOCKER-USER ise container'ları tanımlar. Hiçbir şey yanlışlıkla yayımlanmaz ve yazdığınız bir sonraki docker run -p komutu, tam olarak hedeflediğiniz portu dışarıya açar.

FAQ

UFW portu engellediği halde Docker container'ıma neden erişebiliyorum?

Çünkü Docker, portu nat tablosunun DOCKER zincirinde bir DNAT kuralı ile yayınlar ve bu zincire PREROUTING üzerinden ulaşılır. Bu işlem, paket herhangi bir filtrelemeye uğramadan önce paketin hedef adresini container'ın adresine göre 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ışılmadığı için, UFW'nin engelleme kuralları yayınlanan container portları üzerinde hiçbir etkiye sahip değildir.

UFW'nin Docker tarafından yayınlanan portları engellemesini nasıl sağlarım?

UFW'nin kuralları yanlış zincirde olduğu için bunu tek başına yapamaz. Portu dışarıya açmaktan vazgeçin ve Docker Engine 28.0.0 ve sonraki sürümlerde 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 geçerli kalması için /etc/ufw/after.rules içerisinde kalıcı hale getirin.

Docker'ın daemon.json dosyasında "iptables": false ayarını yapmalı mıyım?

Hayır. Bu ayar, Docker'ın güvenlik duvarı ve NAT kurallarının çoğunu kaldırır ve bu durum, bypass sorunundan çok daha fazla şeyi bozar. Masquerade kuralı silindiği için container'lar dış dünyaya erişimini kaybeder ve Docker, yerine başka kurallar konulmadığı sürece container portlarının yerel ağdaki diğer ana makineler tarafından erişilebilir hale geleceği konusunda uyarır. Bunun yerine loopback üzerinden yayınlama 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'nin etrafından dolaşacak şekilde yeniden yazılır. Docker'ın varsayılan iptables altyapısında, aynı DOCKER-USER kuralının ip6tables ile yansıtılması gerekir. Engine 29 ile gelen deneysel nftables altyapısında DOCKER-USER zinciri bulunmaz, bu nedenle filtreleme işlemi kendi nftables tablonuzda yapılmalıdır. IPv6 olmayan ağlarda docker-proxy süreci [::] üzerinde dinleme yapar ve bu trafik INPUT üzerinden geçer; UFW IPv6'yı yönetiyorsa bu trafiği filtreleyebilir. 127.0.0.1 üzerinde yayın yapmak her iki durumu da engeller, çünkü IPv6 üzerinde hiçbir şey dinlenmez.