← blog · 16 Ekim 2026

Docker'da Yayınlanan Port Güvenlik Duvarını Neden Atlar: UFW, DOCKER-USER Zinciri ve Doğru Bağlama Adresi

UFW her gelen bağlantıyı reddediyor görünürken Docker'ın yayınladığı port internete açık kalabilir. Paketin gerçekte izlediği yol, portu bağlama adresiyle kapatmak, DOCKER-USER zincirinde süzmek, IPv6 ve testin neden dışarıdan yapılması gerektiği.

Tek sunuculu kurulumlarda sık görülen bir düzen var. UFW açılır, varsayılan politika gelen bağlantıları reddedecek şekilde ayarlanır, yalnızca SSH ve HTTPS'e izin verilir. Sonra bir veritabanı konteyneri -p 5432:5432 ile ayağa kaldırılır, çünkü yerel bir aracın ona bağlanması gerekmiştir. ufw status hâlâ yalnızca iki kural gösterir, ama veritabanı portu internetin her yerinden erişilebilir durumdadır. Bunu çoğu zaman bir tarama raporu ya da veritabanı günlüğünde beklenmeyen bir giriş denemesi ortaya çıkarır. UFW bozuk değildir, Docker da kazara bir açık bırakmamıştır; ikisi paketin yolunun farklı noktalarında çalışır.

sudo ufw default deny incoming
sudo ufw allow 22/tcp
sudo ufw allow 443/tcp
sudo ufw enable
docker run -d --name db -e POSTGRES_PASSWORD=ornek -p 5432:5432 postgres:17

Bu beş komuttan sonra 5432 dışarıdan açıktır.

Paket hangi yoldan geçiyor

UFW kurallarını iptables'ın INPUT zincirine yazar. Bu zincir, hedefi makinenin kendisi olan paketleri görür. Docker ise bir portu yayınladığınızda nat tablosuna bir hedef adres dönüşümü (DNAT) kuralı ekler. Paket henüz yönlendirme kararı verilmeden, PREROUTING aşamasında, hedefi konteynerin adresi olacak şekilde yeniden yazılır. Hedef artık makinenin kendisi değil, köprü ağındaki bir konteynerdir. Paket INPUT zincirine hiç uğramaz, FORWARD zincirinden geçer ve orada Docker'ın kendi kuralları yayınlanan porta gelen trafiği kabul eder. UFW'nin reddetme kuralına söz hakkı kalmaz.

Docker'ın belgeleri bunu açıkça yazar: Docker ile UFW güvenlik duvarı kurallarını birbiriyle uyumsuz biçimlerde kullanır ve yayınlanan porta giden trafik UFW ayarlarına ulaşmadan yönlendirilir. Kubernetes'teki NodePort servisleri de düğümün güvenlik duvarını benzer şekilde atlayabilir. Mekanizma aynı, araç farklı.

İlk savunma: hiç yayınlamamak

En güvenilir çözüm portu yayınlamamaktır. Aynı Compose dosyasındaki servisler ortak bir ağı paylaşır ve birbirine servis adıyla ulaşır. Uygulama veritabanına db:5432 adresiyle bağlanır ve bunun için hiçbir ports satırı gerekmez. Yalnızca diğer konteynerlerin konuştuğu bir veritabanında, önbellekte ya da kuyrukta ports satırı görmek başlı başına bir uyarı işaretidir.

Yönetim için ara sıra kendi bilgisayarınızdan bağlanmanız gerekiyorsa, portu yayınlamak yerine bir SSH tüneli çoğu zaman yeterlidir.

İkinci savunma: bağlama adresi

Bir portu yayınlamak zorundaysanız ve onu yalnızca aynı makinedeki bir ters vekil (reverse proxy) kullanıyorsa, portu yalnızca geri döngü adresine bağlayın:

services:
  web:
    image: nginx:1.27
    ports:
      - "127.0.0.1:8080:80"
  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./db_password.txt

Bu dosyada web yalnızca makinenin içinden erişilebilir, db ise hiç yayınlanmamıştır. Adres yazmadan 8080:80 yazmak, Docker'ın varsayılanı olan bütün arayüzlere bağlanmak demektir; docker ps çıktısında bu 0.0.0.0:8080->80/tcp olarak, çoğu zaman yanında [::]:8080->80/tcp ile görünür. Bu varsayılanı /etc/docker/daemon.json içindeki ip ayarı değiştirir: adres belirtilmeden yayınlanan portlar o adrese bağlanır. Unutulan bir ports satırına karşı iyi bir emniyet kemeridir, ama kendisi de sunucu yeniden kurulurken kaybolabilecek bir yapılandırma dosyasında duran bir varsayımdır. Adresi Compose dosyasına açıkça yazmak, niyeti dosyayı sonra okuyacak kişiye de gösterir.

Üçüncü savunma: DOCKER-USER zinciri

Port dışarıya açık olmalı ama yalnızca belirli kaynaklardan erişilmeliyse, kuralın yeri DOCKER-USER zinciridir. Docker bu zinciri oluşturur ve FORWARD içinde kendi kurallarından önce değerlendirir. Buraya yazılan kural yayınlanan bütün portlara uygulanır.

Burada sık düşülen bir tuzak var: paket bu zincire ulaştığında DNAT çoktan uygulanmıştır. Hedef adres konteynerin adresi, hedef port da konteynerin içindeki porttur. --dport 8080 yazarsanız ve konteyner içeride 80'i dinliyorsa, kural hiçbir pakete uymaz. Asıl hedefe göre eşleştirmek için conntrack eklentisi kullanılır:

sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 ! --ctorigsrc 203.0.113.0/24 -j DROP

Bu kural, dış arayüzden (eth0) gelen, asıl hedef portu 8080 olan ve kaynağı belirtilen bloğun dışında kalan bağlantıları düşürür. -i eth0 koşulu önemlidir: konteynerlerin kendi açtığı bağlantıların dönüş trafiği ve konteynerler arası trafik bu arayüzden gelmez, dolayısıyla kuraldan etkilenmez.

İkinci tuzak kalıcılıktır. iptables komutuyla eklenen kural yeniden başlatmada kaybolur. iptables-save çıktısının tamamını saklamak pratik görünür, ama o çıktıda Docker'ın konteyner adreslerine bağlı dinamik kuralları da vardır; açılışta geri yüklenen eski kurallar, Docker'ın yeniden kurduklarıyla karışır. Yalnızca kendi DOCKER-USER satırlarınızı kalıcı hale getirin. UFW kullanıyorsanız bu satırların doğal yeri UFW'nin after.rules dosyasıdır. ufw-docker gibi topluluk betikleri tam olarak bunu yapar ve DOCKER-USER zincirini UFW'nin yönlendirme kurallarına bağlar; böylece erişimi ufw route komutlarıyla yönetmeye devam edebilirsiniz.

Yapılmaması gereken: Docker'ın iptables yönetimini kapatmak

daemon.json içine "iptables": false yazıp Docker'ı güvenlik duvarından uzak tutmak ilk bakışta temiz bir çözüm gibi görünür. Docker'ın belgeleri bu ayarın konteyner ağını büyük olasılıkla bozacağı konusunda uyarır. Konteynerlerin dışarıya çıkması için gereken adres dönüşümü kurulmaz, yayınlanan portlar çalışmaz, ağlar arasındaki yalıtım kuralları da ortadan kalkar. Bunları elle yeniden kurmak, Docker'ın herhangi bir sürümünde değişebilecek bir iç ayrıntının sorumluluğunu üstlenmek demektir. Önerimiz net: bu ayara dokunmayın, süzmeyi DOCKER-USER zincirinde yapın.

IPv6'yı ayrıca ölçün

IPv4 için yazılmış bir iptables kuralı IPv6 trafiğine hiçbir şey yapmaz. Docker IPv6 ile çalışıyorsa aynı zincir ip6tables tarafında da vardır ve kuralın orada ayrıca yazılması gerekir. UFW de iki protokol için ayrı kural tutar. Bir portu IPv4'te kapatıp IPv6'da açık bırakmak en sık atlanan durumlardan biridir, özellikle sunucunun bir IPv6 adresi olduğunu kimse hatırlamıyorsa.

Dışarıdan ölçmek şart

Güvenlik duvarını sunucunun kendisinden test etmek yanıltıcıdır. Makineden kendi genel adresine açılan bağlantı farklı zincirlerden geçer ve dışarıdan gelen bir paketin karşılaşacağı kuralları görmez. Gerçek ölçüm başka bir ağdaki bir makineden yapılır:

nmap -Pn -p 22,80,443,5432,6379,8080 203.0.113.10

Sunucunun üzerinde de iki şeyi düzenli kontrol edin. docker ps çıktısında 0.0.0.0 ya da [::] ile başlayan her port bilinçli bir karar olmalı; ss -ltnp çıktısında da beklemediğiniz bir dinleyici bulunmamalı. Bu kontrolü bir kez yapmak yetmez. Yeni bir Compose dosyası eklendiğinde ya da bir imaj güncellendiğinde tekrarlanmalıdır, çünkü açık çoğunlukla tam o anda doğar. Daha iyisi, dış taramayı zamanlanmış bir iş olarak çalıştırıp sonucu beklenen port listesiyle karşılaştırmaktır.

Ne zaman bu kadarına gerek yok

Sunucu, sağlayıcının ağ düzeyindeki bir güvenlik duvarının (bulut güvenlik grupları gibi) arkasındaysa ve o duvar yalnızca gereken portlara izin veriyorsa, yayınlanan port zaten internete açılamaz. Bu durumda ana savunma o katmandır, sunucudaki kurallar ikinci katman olur. Yine de bağlama adresini doğru yazmak bedava bir önlemdir: dış duvar bir gün değişirse, yanlışlıkla açılan tek bir kural bütün veritabanlarınızı açmaz.

network_mode: host ile çalışan konteynerlerde durum farklıdır. Konteyner doğrudan makinenin ağını kullanır, DNAT yoktur ve trafik INPUT zincirinden geçer; UFW kuralları bu konteynerlere beklendiği gibi uygulanır. Bu, host ağ kipini seçmek için tek başına bir gerekçe değildir, ama iki kipin güvenlik duvarıyla ilişkisinin neden bu kadar farklı olduğunu açıklar.

[ ← burak.sh'a dön ]