TEKNİK YAZI / Backend Sistemleri

UFW (Uncomplicated Firewall): Linux Güvenlik Duvarı Rehberi

UFW ile Linux güvenlik duvarı kurallarını SSH erişimini kaybetmeden yönetin: kural sırası, Docker port sorunu, IPv6, günlük izleme ve kuralları yedekleme.

UFW (Uncomplicated Firewall), Linux güvenlik duvarı (firewall) kurallarını okunabilir komutlarla yönetmek için kullanılan bir ön yüzdür. Uzak bir sunucuda kural değiştirirken önce yönetim erişimini açık tutacak SSH kuralını doğrulamak ve olası kilitlenmeye karşı ikinci bir oturum bulundurmak gerekir.

UFW nedir?

UFW, kullanımı kolay bir güvenlik duvarı yönetim aracıdır. Kuralları kendisi uygulamaz; yazdığınız komutları iptables kurallarına çevirir. Güncel Ubuntu sürümlerinde iptables komutu iptables-nft arka ucuyla çalışır ve kuralları çekirdekteki nftables altyapısına yazar; bunu sudo iptables --version çıktısındaki (nf_tables) ibaresinden görebilirsiniz. UFW, Ubuntu’nun varsayılan güvenlik duvarı yapılandırma aracıdır ve başlangıçta devre dışıdır. Debian’da ise sudo apt install ufw ile ayrıca kurulur.

Durum ve etkinleştirme komutları

# Durum
sudo ufw status

# Ayrıntılı durum: varsayılan politikalar ve günlük düzeyi
sudo ufw status verbose

# Numaralı liste (kural silme için)
sudo ufw status numbered

# Etkinleştirme: uzak sunucuda önce aşağıdaki SSH adımlarını izleyin
sudo ufw enable

# Devre dışı bırakma
sudo ufw disable

Önce: uzak sunucuda erişimi kaybetmemek

ufw enable komutu, kuralları yazmadan önce çalıştırıldığında yönetim erişimini kesebilir. Uzak bir sunucuda bu, konsol erişimi olmadan geri dönülemeyen bir durumdur. Kural değiştirmeye başlamadan önce aşağıdaki sıra izlenmelidir.

1. Gerçek SSH portunu doğrulayın. ufw allow ssh komutu servis adını /etc/services üzerinden çözer ve 22 numaralı portu açar. SSH başka bir porta taşınmışsa bu kural işe yaramaz ve ufw enable bağlantıyı keser. Dinlenen portu ve şu an bağlı olduğunuz oturumun sunucu tarafındaki portunu kontrol edin:

# sshd'nin dinlediği portlar
sudo ss -tlnp | grep sshd

# Mevcut SSH oturumunun sunucu portu (SSH_CONNECTION'ın dördüncü alanı)
echo "$SSH_CONNECTION" | awk '{print $4}'

Ubuntu 22.10 ve sonrasında SSH soket etkinleştirmeyle başlar ve portu systemd tutar; ilk komut bu yüzden boş dönebilir. Böyle bir durumda systemctl status ssh.socket çıktısındaki Listen: satırına bakın. İkinci komut her durumda o an bağlı olduğunuz portu gösterir.

2. Kuralı UFW’yi etkinleştirmeden önce ekleyin. Çıkan portu açıkça yazın:

sudo ufw allow 22/tcp

3. Kuralları uygulamadan önce sonucu görün. --dry-run, oluşacak kural setini hiçbir değişiklik yapmadan yazdırır:

sudo ufw --dry-run enable

4. İkinci bir oturum açık tutun. Etkinleştirme sırasında mevcut SSH oturumunuzu kapatmayın. UFW enable komutu bu nedenle onay sorar.

5. Etkinleştirin ve üçüncü bir oturumla doğrulayın. Yeni bir bağlantı kurulabildiğini görmeden mevcut oturumları kapatmayın:

sudo ufw enable
sudo ufw status verbose

Sağlayıcınızın web konsolu (KVM, seri konsol veya kurtarma modu) varsa bunu önceden denemiş olmak, kilitlenme durumunda tek geri dönüş yoludur.

Temel UFW komutları

1. Port yönetimi

# Belirli bir portu aç
sudo ufw allow 22        # SSH
sudo ufw allow 80        # HTTP
sudo ufw allow 443       # HTTPS

# Port aralığı (aralıklarda protokol belirtmek gerekir)
sudo ufw allow 6000:6007/tcp

# Protokol belirterek
sudo ufw allow 22/tcp
sudo ufw allow 53/udp

# Portu kapat
sudo ufw deny 23         # Telnet'i engelle

Kaynak belirtilmeden yazılan bir allow kuralı portu tüm adreslere açar. Bu, herkese açık olması gereken web servisleri için doğrudur; geliştirme sunucuları, veritabanları ve yönetim arayüzleri için değildir. Bu tür servisler için bir sonraki başlıktaki kaynak kısıtlı biçimi kullanın.

2. Servis adları ve uygulama profilleri

# Servis adıyla (/etc/services üzerinden porta çözülür)
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https

# Uygulama profillerini listele (/etc/ufw/applications.d)
sudo ufw app list

# Uygulama profiliyle
sudo ufw allow 'Apache Full'
sudo ufw allow 'Nginx Full'
sudo ufw allow 'OpenSSH'

Servis adları /etc/services üzerinden sabit port numaralarına çözülür: ssh her zaman 22, http 80, https 443 anlamına gelir. Servisi standart dışı bir portta çalıştırıyorsanız servis adı yerine port numarasını yazın. Uygulama profilleri ise ilgili paketlerin /etc/ufw/applications.d altına bıraktığı dosyalardır; Apache Full ya da Nginx Full yalnız o web sunucusu kuruluysa listede görünür.

ufw limit komutu da burada anılmalıdır, çünkü davranışı adından anlaşılmıyor:

sudo ufw limit 22/tcp

Bu kural, aynı kaynak IP adresinden 30 saniye içinde altıncı ve sonraki bağlantı girişimlerini reddeder. Kaba kuvvet denemelerini yavaşlatır, fakat tek başına bir kimlik doğrulama önlemi değildir; anahtar tabanlı SSH girişi ve parola girişinin kapatılmasıyla birlikte düşünülmelidir. Anahtar kurulumu ve SSH üzerinden dosya aktarımı için scp yazısına bakabilirsiniz.

3. IP adresine göre kurallar

# Belirli bir IP'den gelen bağlantılara izin ver
sudo ufw allow from 192.168.1.100

# IP aralığı
sudo ufw allow from 192.168.1.0/24

# Belirli bir IP'yi engelle
sudo ufw deny from 192.168.1.50

# Belirli bir IP'den belirli bir porta
sudo ufw allow from 192.168.1.100 to any port 22 proto tcp

Geliştirme sunucusu için örnek kural seti

Komutları tek bir betiğe koyup topluca çalıştırmak yerine sırayla uygulamak ve her adımda ufw status ile sonucu görmek daha güvenlidir; kural hatası ancak erişim kaybedilmeden fark edilirse ucuza kapanır.

ufw reset mevcut tüm kuralları siler ve güvenlik duvarını devre dışı bırakır. Çalışan bir sunucuda kullanmadan önce mevcut durumu kaydedin:

sudo ufw status numbered > ~/ufw-kurallar-yedek.txt

Temel politika ve herkese açık servisler:

# Varsayılan politika: gelen kapalı, giden açık
sudo ufw default deny incoming
sudo ufw default allow outgoing

# SSH: gerçek portu yukarıdaki bölümde doğruladıktan sonra
sudo ufw limit 22/tcp

# Herkese açık web servisleri
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

Geliştirme sunucuları ve veritabanları kaynak kısıtı olmadan açılmamalıdır. React, Flask ve Django’nun geliştirme sunucuları üretim için tasarlanmamıştır. Flask’ın hata ayıklama konsolu tarayıcı üzerinden kod çalıştırmaya izin verir. Django DEBUG=True iken hata sayfasında ayarları ve traceback’teki yerel değişkenleri, 404 sayfasında URL yapılandırmasını gösterir. SECRET_KEY ile adında PASS, KEY veya TOKEN gibi ifadeler geçen ayarları maskeler; yerel değişkenlerdeki sırlar, dosya yolları ve yapılandırma ayrıntıları yine de sızabilir. İnternete açık bir port bu yüzeyleri doğrudan erişilebilir kılar.

# Geliştirme sunucuları: yalnızca yerel ağdan
sudo ufw allow from 192.168.1.0/24 to any port 3000 proto tcp
sudo ufw allow from 192.168.1.0/24 to any port 5000 proto tcp
sudo ufw allow from 192.168.1.0/24 to any port 8000 proto tcp

# Veritabanları: yalnızca bilinen kaynaklardan
sudo ufw allow from 192.168.1.0/24 to any port 3306 proto tcp
sudo ufw allow from 192.168.1.0/24 to any port 5432 proto tcp
sudo ufw allow from 192.168.1.0/24 to any port 27017 proto tcp

Daha güvenli bir yaklaşım, geliştirme sunucusunu yalnızca geri döngü arayüzüne bağlamak ve uzaktan erişimi SSH tüneliyle sağlamaktır. Bu durumda güvenlik duvarında hiçbir ek port açmanız gerekmez:

# Sunucuda: yalnızca 127.0.0.1 dinlenir
python manage.py runserver 127.0.0.1:8000

# İstemcide: tünel açılır
ssh -L 8000:127.0.0.1:8000 kullanici@sunucu

Kural setini uyguladıktan sonra etkin politikayı ve gerçek dinleme durumunu ayrı ayrı doğrulayın:

sudo ufw status verbose
sudo ss -tlnp

Docker kullanıyorsanız UFW kurallarınız geçersiz olabilir

Bu sorun, UFW kullanılan Docker sunucularında kolayca gözden kaçar.

Docker, -p ile yayımlanan portlara gelen trafiği iptables’ın nat tablosunda, PREROUTING aşamasında konteyner adresine yönlendirir (DNAT). Bu trafik ana makinenin INPUT zinciri yerine FORWARD zincirinden geçer; UFW’nin allow ve deny kuralları ise INPUT ve OUTPUT tarafındadır. Docker’ın kendi kuralları DOCKER ve ilişkili zincirlerde durur. Sonuç olarak bir konteyneri -p ile yayımladığınızda port, UFW’de ne yazdığından bağımsız olarak dışarıdan erişilebilir hâle gelir:

docker run -d -p 3306:3306 mysql   # port dışarıya açıldı
sudo ufw deny 3306                 # bu kuralın etkisi yok
sudo ufw status                    # kural yine de "aktif" görünür

Pratik çözüm, portu yalnızca geri döngü arayüzüne yayımlamaktır:

docker run -d -p 127.0.0.1:3306:3306 mysql

Konteyner portlarının gerçekten dışarıya açılması gerekiyorsa filtreleme DOCKER-USER zincirine yazılmalıdır. Docker bu zinciri kullanıcı kuralları için boş bırakır ve kendi kurallarından önce işler; UFW bu zinciri yönetmez. Zincirdeki kuralları görmek için:

sudo iptables -L DOCKER-USER -n -v

Docker 29.0 ile deneysel olarak gelen nftables arka ucunda ("firewall-backend": "nftables") DOCKER-USER zinciri yoktur. Bu arka uca geçerseniz kurallarınızı, Docker’ın belgesinde anlatıldığı gibi ayrı bir nftables tablosuna taşımanız gerekir.

Sıkı politika: giden trafiği de kısıtlamak

Giden trafiği varsayılan olarak kapatmak, ele geçirilmiş bir sistemden veri sızdırılmasını ve zararlı yazılımın dışarıya bağlanmasını zorlaştırır. Bedeli, sunucunun kurduğu her dış bağlantının açıkça izinlendirilmesi gerekmesidir.

Bu politikaya geçmeden önce hangi işlerin kesileceğini bilmek gerekir. Kesilen bağlantılar çoğunlukla zaman aşımı olarak görünür ve sebebinin güvenlik duvarı olduğu hemen anlaşılmaz. Bu yüzden önce sunucunun kurduğu dış bağlantıları bir süre gözlemleyin. ss, kurulu TCP bağlantılarını ve onları açan süreçleri gösterir; yedekleme ve güncelleme gibi zamanlanmış işlerin çalıştığı saatler dahil birkaç kez alınan çıktı bir envanter oluşturur:

sudo ss -tnp state established

Karşı taraf (Peer Address:Port) portu 443, 22 veya 587 gibi bilinen bir servis portu olan satırlar genellikle giden bağlantılardır. Envanter hazır olunca önce izin kurallarını ekleyin, varsayılan politikayı en son değiştirin:

sudo ufw allow out 53          # DNS
sudo ufw allow out 80/tcp      # HTTP
sudo ufw allow out 443/tcp     # HTTPS
sudo ufw allow out 123/udp     # NTP

# İzinler eklendikten sonra
sudo ufw default deny outgoing

Yukarıdaki izinler yalnızca ad çözümleme, paket deposu erişimi ve saat eşitlemesi için yeterlidir. Şunlar açıkça izinlendirilmedikçe çalışmaz:

IPv6 kuralları ayrıdır

UFW varsayılan olarak IPv6 kurallarını da üretir; bu davranış /etc/default/ufw dosyasındaki IPV6=yes satırıyla belirlenir ve ufw status çıktısında (v6) ekli satırlar olarak görünür.

Dikkat edilmesi gereken nokta, adres tabanlı kuralların adres ailesine özgü olmasıdır. Bir IPv4 adresini engellemek aynı sunucunun IPv6 adresini engellemez:

sudo ufw deny from 203.0.113.50     # yalnızca IPv4
sudo ufw deny from 2001:db8::50     # IPv6 için ayrı kural gerekir

Sunucunuz IPv6 üzerinden erişilebilir durumdaysa port kurallarınızın her iki adres ailesinde de beklediğiniz gibi göründüğünü sudo ufw status verbose ile doğrulayın.

İzleme

Engellenen bağlantıları görüntüleme

UFW etkinleştirildiğinde günlük kaydı low düzeyinde açıktır: /etc/ufw/ufw.conf dosyasındaki varsayılan değer LOGLEVEL=low’dur ve sudo ufw status verbose çıktısında Logging: on (low) olarak görünür. low, politikaya uymadığı için engellenen paketleri hız sınırıyla kaydeder. medium ve üstü çok daha fazla kayıt üretir ve yoğun bir sistemde diski hızla doldurabilir. Günlük kapatılmışsa yeniden açmak için:

sudo ufw logging low

Günlüklerin nereye yazıldığı sistemin yapılandırmasına bağlıdır. Rsyslog kuruluysa kayıtlar /var/log/ufw.log dosyasına da düşer. Rsyslog’un bulunmadığı minimal veya konteyner imajlarında kayıtlar yalnızca çekirdek günlüğündedir:

# Rsyslog kuruluysa
sudo grep 'UFW BLOCK' /var/log/ufw.log | tail -20

# Her durumda çalışır
sudo journalctl -k --grep='UFW BLOCK' --since '1 hour ago'

Kaynak adresi ve hedef portu alan numarasına göre değil alan adına göre ayıklamak daha dayanıklıdır; UFW satırlarındaki alan sayısı arayüze ve protokole göre değişir:

# En çok engellenen kaynak adresler
sudo journalctl -k --grep='UFW BLOCK' --since today \
  | grep -oE 'SRC=[^ ]+' | sort | uniq -c | sort -nr | head -10

# En çok hedeflenen portlar
sudo journalctl -k --grep='UFW BLOCK' --since today \
  | grep -oE 'DPT=[0-9]+' | sort | uniq -c | sort -nr | head -10

Düzenli rapor için systemd timer

Sürekli çalışan bir kabuk döngüsü yerine zamanlanmış bir servis kullanmak daha öngörülebilirdir: zamanlama, günlükleme ve hata durumunda davranış sistem tarafından yönetilir, PID dosyası tutmak gerekmez. Servis birimlerinin yapısı systemd servis yazısında ayrıntılı anlatılıyor.

/usr/local/bin/ufw-rapor betiği:

#!/usr/bin/env bash
set -euo pipefail

esik=100
# journalctl --grep eşleşme bulamazsa sıfır dışı kodla çıkar; || true betiğin
# sakin saatlerde hata vermesini önler. -q "-- No entries --" satırını bastırır.
sayi=$({ journalctl -k -q -o cat --grep='UFW BLOCK' --since '1 hour ago' || true; } | wc -l)

echo "Son bir saatte engellenen bağlantı: ${sayi}"

if (( sayi > esik )); then
    echo "UYARI: eşik (${esik}) aşıldı" >&2
    exit 1
fi

journalctl --grep, eşleşme bulamadığında grep gibi sıfır dışı bir kodla çıkar. set -euo pipefail altında bu, engelleme olmayan bir saatte betiğin hata vermesine, servisin başarısız görünmesine ve yalancı alarma yol açardı. -o cat yalnızca ileti metnini yazar, -q ise eşleşme yokken basılan -- No entries -- satırının sayıma girmesini önler.

/etc/systemd/system/ufw-rapor.service:

[Unit]
Description=UFW engelleme raporu

[Service]
Type=oneshot
ExecStart=/usr/local/bin/ufw-rapor

/etc/systemd/system/ufw-rapor.timer:

[Unit]
Description=UFW engelleme raporunu saatlik çalıştır

[Timer]
OnCalendar=hourly
Persistent=true

[Install]
WantedBy=timers.target

Devreye alma ve izleme:

sudo systemctl daemon-reload
sudo systemctl enable --now ufw-rapor.timer
journalctl -u ufw-rapor.service

Betik standart çıktıya yazdığı için ayrı bir günlük dosyası tutmak gerekmez; çıktıyı journald toplar. Eşik aşıldığında servis başarısız duruma geçer ve bu durum OnFailure= ile bir bildirim unit’ine bağlanabilir.

Kuralları yedeklemek ve geri yüklemek

UFW kurallarını /etc/ufw/user.rules (IPv4) ve /etc/ufw/user6.rules (IPv6) dosyalarında tutar. Yedek alırken ikisini birden almak gerekir; yalnızca IPv4 dosyasını geri yüklemek IPv6 kurallarının eski hâlde kalmasına yol açar.

Okunabilir bir anlık görüntü için:

sudo ufw status numbered > ~/ufw-durum-$(date +%F).txt

Bu dosya doğrudan geri yükleme için kullanılamaz, fakat neyin değiştiğini karşılaştırmak ve kuralları elle yeniden kurmak için en güvenilir kayıttır.

Yapılandırmanın tamamını yedeklemek:

sudo tar czf ~/ufw-yedek-$(date +%F).tar.gz -C / etc/ufw etc/default/ufw

Geri yükleme güvenlik duvarını durdurmayı gerektirir. Uzak bir sunucuda bu işlem erişimi kesme riski taşır; öncesinde konsol erişiminizin olduğundan emin olun:

sudo ufw disable
sudo tar xzf ~/ufw-yedek-2026-09-06.tar.gz -C /
sudo ufw enable
sudo ufw status verbose

Dosyaları elle düzenlediyseniz etkinleştirmeden önce oluşacak kural setini sudo ufw --dry-run enable ile gözden geçirin.

Sık karşılaşılan sorunlar

SSH erişimini kaybettim

Bağlantı koptuktan sonra sunucuya SSH ile ulaşamazsınız; geriye yalnızca sağlayıcınızın web konsolu, seri konsol veya fiziksel erişim kalır. Konsola düştükten sonra:

sudo ufw status numbered      # hangi kuralın eksik olduğunu görün
sudo ss -tlnp | grep sshd     # SSH'nin gerçek portunu doğrulayın
sudo ufw allow 22/tcp         # çıkan portu açın
sudo ufw reload

Bu bölümün asıl değeri sonradan düzeltmekte değil, duruma hiç düşmemektedir. Yazının başındaki sıra bu yüzden vardır: gerçek portu doğrulayın, kuralı enable öncesinde ekleyin, ikinci bir oturum açık tutun, üçüncü bir oturumla doğrulayın.

Port erişilemiyor

Sorunun güvenlik duvarında mı uygulamada mı olduğunu ayırmak ilk adımdır. Uygulama yalnızca 127.0.0.1 üzerinde dinliyorsa hiçbir UFW kuralı onu dışarıya açmaz:

# Hangi süreç hangi adres ve portu dinliyor
sudo ss -tlnp | grep :3000

# Güvenlik duvarı tarafındaki kural
sudo ufw status | grep 3000

ss çıktısındaki yerel adres 127.0.0.1:3000 ise sorun güvenlik duvarında değildir; uygulamanın 0.0.0.0 veya ilgili arayüz üzerinde dinlemesi gerekir.

Süreci sonlandırmanız gerekiyorsa önce SIGTERM gönderin; SIGKILL ancak yanıt alınamadığında kullanılır. PID’yi doğrulama ve sinyal sırası kill ve PID yazısında anlatılıyor:

sudo kill -s TERM <PID>

Kural çakışmaları ve tekrarlar

UFW kuralları sırayla değerlendirir ve eşleşen ilk kuralı uygular. Bu nedenle geniş bir allow kuralı, altında yer alan daha dar bir deny kuralını etkisiz bırakabilir. Sıra numaralarını görüntüleyip gereksiz kuralları tek tek silmek, kural setini sıfırlamaktan güvenlidir:

sudo ufw status numbered
sudo ufw delete 5

Kuralın belirli bir konuma eklenmesi gerekiyorsa:

sudo ufw insert 1 deny from 203.0.113.50

ufw reset yalnızca kural setini baştan kurmaya hazır olduğunuzda ve yedek aldıktan sonra kullanılmalıdır; komut güvenlik duvarını devre dışı bırakır ve tüm kuralları siler.

Sonuç

UFW, tek bir sunucunun güvenlik duvarı politikasını okunabilir komutlarla yönetmek için yeterli ve pratiktir: gelen trafiği varsayılan olarak kapatın, yalnız gereken portları mümkünse kaynak kısıtıyla açın, SSH için limit kullanın ve kuralları yedekleyin. Yanlış kural sırası veya eksik envanter yine de erişimi kesebileceği için değişiklikten önce mevcut servisleri ve kaynak ağları çıkarın, değişiklikten sonra etkin politika ile gerçek bağlantıyı ayrı ayrı doğrulayın. Docker’ın yayımladığı portlar veya karmaşık yönlendirme kuralları söz konusuysa UFW tek başına yetmez; DOCKER-USER zinciri ya da doğrudan nftables gerekir.

Birincil kaynaklar