SSD Nodes Learn 🎉 VPS $5.50/aydan başlayan
Rehberler Matt ConnorYazan Matt Connor · Güncellendi 2026-08-13

Nginx 403 hatası ve SELinux izin sorunları çözümü

Nginx dosya izinleri doğru olmasına rağmen 403 hatası veriyorsa sorun SELinux kaynaklıdır. Deny loglarını inceleyip semanage ve restorecon komutlarıyla etiketi düzeltin.

Nginx, izinleri doğru olan bir dosya için neden 403 hatası döndürür

Nginx'in izin bitleri doğru olan bir dosya için 403 hatası döndürmesi, neredeyse her zaman SELinux'un (Security-Enhanced Linux) okuma işlemini reddetmesinden kaynaklanır. SELinux, normal izinler doğrulandıktan sonra ikinci bir kural kümesini denetler ve web sunucusunun yalnızca web içeriği etiketi taşıyan dosyaları okumasına izin verilir. Dosyanız farklı bir etiket taşıdığı için açma işlemi başarısız olur ve Nginx'in gönderebileceği bir veri kalmaz.

Yalnızca mod değerine değil, etikete de bakın:

ls -ldZ /data/www /data/www/index.html
drwxr-xr-x. root root unconfined_u:object_r:default_t:s0 /data/www
-rw-r--r--. root root unconfined_u:object_r:default_t:s0 /data/www/index.html

drwxr-xr-x komutundan sonra yazdırılan nokta, dosyanın bir SELinux etiketi taşıdığını gösterir. default_t, politika tarafından tanımlanmamış bir yola atanan etikettir ve web sunucusu kurallarında bu türün okunmasına izin veren bir kural yoktur. Hata günlüğü sıradan bir Unix hatası gösterir; bu nedenle sorun bir izin hatası gibi görünür:

2026/08/11 09:14:02 [error] 1183#1183: *1 open() "/data/www/index.html" failed (13: Permission denied), client: 203.0.113.9, server: _, request: "GET / HTTP/1.1"

Çekirdek, hem sıradan izin reddi hem de SELinux reddi için 13: Permission denied döndürür. Bu nedenle ilk iş, hangi katmanın reddettiğini bulmaktır. İşe setenforce 0 ile başlamayın.

Modelin ihtiyaç duyduğunuz kısmı

SELinux, genellikle MAC olarak kısaltılan zorunlu erişim denetimidir. Her süreç, web sunucusu için httpd_t gibi bir etki alanında (domain) çalışır. Her dosya ve her ağ portu, httpd_sys_content_t gibi bir tür (type) taşır. Politika; etki alanı, tür ve eylem kombinasyonlarına izin verilen bir listedir ve bu listede yer almayan her şey reddedilir. SELinux, klasik Unix denetiminden sonra çalışır; bu nedenle drwxr-xr-x içindeki izin bitlerinin erişime öncelikle izin vermesi gerekir. Her iki katman da onay vermelidir.

Tam bir bağlam (context), system_u:system_r:httpd_t:s0 gibi iki nokta üst üste ile ayrılmış dört alandan oluşur: SELinux kullanıcısı, rol, tür ve seviye. Bir sunucuda zamanınızın neredeyse tamamını üçüncü alan olan tür ile geçirirsiniz. İki komut, canlı değerleri gösterir:

ps -eZ | grep nginx
id -Z

Nginx çalışanları, httpd_t ile biten bir bağlam gösterir. Oturum açtığınız kabuk (shell) ise unconfined_u:unconfined_r:unconfined_t:s0 değerini gösterir; çünkü varsayılan targeted politikası servisleri kısıtlar ancak etkileşimli kullanıcıları serbest bırakır. Bunu bilmek önemlidir, çünkü SELinux servisleri en düşük yetkili kullanıcılarla çalıştırmanın yerini almaz. Birisi sisteme sızdığında, servisin nelere erişebileceğini sınırlar.

Üç mod ve hangi imajların SELinux içerdiği

sestatus
getenforce

Enforcing modu, erişimi engeller ve günlüğe kaydeder. Permissive modu, her şeye izin verir ancak engellenecek işlemleri günlüğe kaydeder. Disabled modu ise hiçbir politika yüklemez. getenforce komutu mevcut modu yazdırır. sestatus komutu ise yeniden başlatma sonrasında geçerli olacak modu /etc/selinux/config dosyasından okuyarak görüntüler.

Rocky Linux, AlmaLinux, Fedora ve RHEL, SELinux'u targeted politikası ile enforcing modunda sunar. Ubuntu ve Debian ise aynı işi farklı bir mekanizmayla yapan AppArmor'u kullanır (son bölüm bu konuyu ele almaktadır). Bu nedenle aynı uygulama, sunucularınızdan birinde sorunsuz kurulurken diğerinde 403 hatası verebilir.

İhtiyaç duymadan önce araçları kurun

sudo dnf install -y policycoreutils-python-utils setroubleshoot-server

Minimal bir imaj üzerinde semanage: command not found kullanımı, policycoreutils-python-utils paketinin eksik olduğu anlamına gelir; bu paket semanage ve audit2allow bileşenlerini içerir. setroubleshoot-server, sealert ekler ve her bir erişim reddini sade bir dille özetleyerek günlük kayıtlarına yazar. Her iki aracı da yeni kurulmuş bir sunucuya yükleyin; çünkü bu araçlara ihtiyaç duyduğunuz an, sistemin halihazırda bozulmuş olduğu andır.

SELinux reddetme kayıtlarını audit günlüğünde okuma

Her reddetme işlemi, audit daemon tarafından bir AVC (erişim vektörü önbelleği) mesajı olarak kaydedilir:

sudo ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recent
type=AVC msg=audit(1754896442.881:412): avc:  denied  { read } for  pid=1183 comm="nginx" name="index.html" dev="vda1" ino=17203 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0

Dört alan hikayenin tamamını anlatır. comm engellenen programdır. scontext kaynak bağlamıdır; sürecin içinde çalıştığı etki alanıdır. tcontext hedef bağlamıdır; erişilmeye çalışılan nesnenin etiketidir. tclass nesne türüdür, burada bir dosyadır. Birlikte okunduğunda: httpd_t içindeki süreç, default_t etiketli bir dosyayı okumaya çalışmıştır ve permissive=0, isteğin sadece günlüğe kaydedilmekle kalmayıp gerçekten engellendiğini belirtir.

Eğer ausearch hiçbir çıktı vermiyorsa, audit daemon çalışmıyor olabilir. Bu durumda reddetme kayıtları çekirdek halka arabelleğine (kernel ring buffer) düşer:

sudo journalctl -k | grep -i avc

Şimdi kaydı anlaşılır bir cümleye dönüştürün:

sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.log

audit2why aynı kayıtları okur ve tanıdığı nedenleri isimlendirir: kapalı bir boolean, politikayla eşleşmeyen bir etiket veya hiç kural olmaması. sealert tüm günlüğü tarar ve her reddetme için önerilen bir komut yazdırır. Öneriyi bir ipucu olarak değerlendirin. İfadeler sürümler arasında değişiklik gösterir ve sealert bazen tek satırlık bir etiket düzeltmesi yeterliyken özel bir politika modülü önerebilir.

Bilmeniz gereken bir şey daha var. Politika, zararsız kabul edilen reddetmeleri gizleyen dontaudit kuralları içerir; bu nedenle bir program hatalı çalışsa bile günlük boş kalabilir. Bir test süresince bunları görünür hale getirin:

sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -B

semanage fcontext ve restorecon ile hatalı etiketlenmiş bir yolu düzeltme

İki komut kullanılır ve sıra önemlidir. semanage fcontext -a, bir yolun etiketinin ne olması gerektiğini kaydeder. restorecon ise bu kayıtlı varsayılanı diskteki dosyalara uygular.

sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.html

Yol bir düzenli ifade (regular expression) şeklindedir. (/.*)?, dizinin kendisini ve altındaki her şeyi kapsar; bir belge kök dizininin (document root) ihtiyaç duyduğu şey budur. Değişiklik yapmadan önce nelerin değişeceğini görün: sudo restorecon -Rvn /data/www, planlanan yeniden etiketlemeleri yazdırır çünkü -n herhangi bir işlem yapılmayacağı anlamına gelir. Gerçek bir restorecon işleminden sonra etiket httpd_sys_content_t olarak okunur ve servis yeniden başlatmaya gerek kalmadan 403 hatası ortadan kalkar.

chcon komutunu yalnızca test amaçlı kullanın. chcon -t httpd_sys_content_t index.html etiketi doğrudan ayarlar; ancak bir sonraki restorecon, paket güncellemesi veya tam sistem yeniden etiketleme işleminde bu ayar sıfırlanır, çünkü politika hala yolun farklı bir etikete sahip olması gerektiğini belirtir. semanage fcontext, kalıcı olan sürümdür. Kayıtlı olanları listelemek için sudo semanage fcontext -l | grep '^/data' kullanın.

Servisin yazması gereken içerik farklı bir tür gerektirir. Yükleme dizini veya önbellek için httpd_sys_rw_content_t kullanın ve bunu yalnızca ilgili yollarla sınırlı tutun: salt okunur bir siteyi yazılabilir bir tür altında tutmak, uygulamanın ihtiyaç duyduğundan daha fazla erişim yetkisi verir.

Etiket neden hatalıydı? Neredeyse her zaman dosyaların oraya nasıl geldiğiyle ilgilidir. mv, bir dosyanın mevcut etiketini korur; bu nedenle /root dışına taşınan bir site admin_home_t etiketiyle gelir ve öyle kalır. Basit bir cp, yeni dosyaya hedef dizinin varsayılan etiketini verir ki genellikle istenen budur; cp -a ve rsync -X ise kaynak etiketlerini dosyayla birlikte kopyalar. Yeni bir üst düzey dizine yapılan git clone, default_t sonucunu doğurur. Bir sayfa /usr/share/nginx/html üzerinden düzgün yüklenirken kendi dizininizden yüklenemiyorsa, bunun nedeni budur.

Bir davranış sınıfını boolean ile düzeltme

Bazı hatalar etiket sorunu değildir. Yeni kurulmuş bir Rocky veya AlmaLinux sunucusundaki reverse proxy 502 hatası döndürür ve hata günlüğünde şu ifade yer alır:

2026/08/11 10:02:55 [crit] 1183#1183: *3 connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstream

Upstream servisiniz düzgün çalışıyor. httpd_t etki alanı, varsayılan olarak giden ağ bağlantıları açmaya izin vermez; bu nedenle connect() çağrısı, loopback arayüzüne ulaşmadan reddedilir. Bu davranışın tamamını tek bir anahtar kontrol eder:

getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on

-P, önemli olan bayraktır: değeri diske yazar. -P kullanılmadığında, yapılan değişiklik bir sonraki yeniden başlatmada kaybolur; bu da makine yeniden başlatılana kadar çalışan bir servis elde etmenize neden olur. Çalışan değeri kayıtlı olanın yanında yazdıran semanage boolean -l | grep httpd_can_network_connect ile durumu doğrulayın.

Mevcut olduğunda, el ile yazılmış bir kural yerine her zaman bir boolean tercih edin. Boolean değerleri dağıtım politikasıyla birlikte gelir; bu nedenle bakımları yapılır, belgelenir ve bir sonraki kişi tarafından kolayca bulunabilirler. getsebool -a, sistemdeki tüm boolean değerlerini listeler.

Bir servisin standart olmayan bir portu dinlemesini sağlama

Portlar da etiketlenmiştir. Nginx'i 8081 portuna taşıdığınızda başlatmayı reddeder:

nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)

httpd_t, http_port_t olarak etiketlenmiş portlara bağlanabilir; 8081 bunlardan biri değildir. Portu ekleyin:

sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081

Önce listeyi kontrol edin. 8008 ve 8443 dahil olmak üzere birçok yüksek port zaten izinlidir; bir portu iki kez eklemek ValueError: Port tcp/8081 already defined hatasına neden olur. Eğer port zaten farklı bir türe aitse, eklemek yerine semanage port -m -t http_port_t -p tcp 8081 ile değiştirin.

Aynı komut, taşınan bir SSH portunun çalışmasını sağlayan komuttur. journalctl -u sshd içindeki Bind to port 2222 on 0.0.0.0 failed: Permission denied, 2222 portunun ssh_port_t içinde eksik olduğu anlamına gelir; bu nedenle servisi yeniden başlatıp oturumunuzu kapatmadan önce sudo semanage port -a -t ssh_port_t -p tcp 2222 komutunu çalıştırın. Bu, insanların Red Hat tabanlı bir imaj üzerinde VPS üzerinde SSH güvenliğini sağlama konulu genel bir kılavuzu takip ederken atladıkları adımdır. SELinux bir güvenlik duvarı değildir, bu nedenle portun yine de açılması gerekir: burada sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload komutu veya Debian veya Ubuntu imajında ufw kullanılmalıdır.

Değiştirilecek bir boolean veya etiket bulunmadığında

Normal bir sunucuda bu durum nadirdir ve kullanıcıların sisteme zarar verdiği nokta burasıdır. audit2allow, günlük kayıtlarındaki (log) engellemelerden (denial) bir ilke modülü oluşturabilir:

sudo ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local
cat nginx_local.te
sudo semodule -i nginx_local.pp

Yüklemeden önce nginx_local.te dosyasını okuyun. İki alışkanlık bu süreci güvenli tutar. Girdiyi, -c ile düzeltmekte olduğunuz tek bir programla sınırlandırın; çünkü bir haftalık alakasız engelleme kayıtlarını audit2allow aracına yönlendirmek, hepsine aynı anda izin verilmesine neden olur. Ayrıca açıklayamadığınız bir engelleme kaydından oluşturulan modülü asla yüklemeyin: httpd_t aracının sunucudaki her dosyayı okumasına izin veren bir kuralı oluşturmak kolay, aylar sonra fark etmek ise zordur. Bir modülü kaldırmak için sudo semodule -r nginx_local komutunu kullanın.

Permissive modu bir çözüm değil, tanılama modudur

sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1

Permissive modu, erişime izin verir ve bunu günlüğe kaydeder. Bu modun gerçek değeri, sağladığı eksiksizliktir. Enforcing modunda servis ilk engellemede durur; bu nedenle bir sorunu giderip yeniden başlatırsınız ve bir sonrakine takılırsınız. Permissive modunda ise çalışma devam eder ve günlük dosyası tüm engellemeleri tek seferde toplar; böylece sisteme geri dönüp hepsini bir arada düzeltebilirsiniz.

setenforce, /etc/selinux/config dosyasına müdahale etmez; bu nedenle yeniden başlatma işlemi sunucuyu tekrar enforcing moduna döndürür. Bu bir güvenlik önlemidir ve setenforce 0 komutundan ibaret bir "çözümün" en beklenmedik anda tekrar sorun yaratmasının nedeni de budur. Eğer bir servis üzerinde çalışırken ona alan açmanız gerekirse, tüm makineyi değil sadece ilgili domaini işaretleyin: sudo semanage permissive -a httpd_t komutu diğer her şeyi enforcing modunda bırakır, sudo semanage permissive -d httpd_t komutu ise bu işlemi geri alır.

SELinux'u devre dışı bırakmanın etiket düzeltmekten daha maliyetli olmasının nedeni

SELINUX=disabled değerini /etc/selinux/config içinde ayarlamak, tek satırlık bir etiket düzeltmesi yerine kalıcı olarak daha zayıf bir sunucuyu tercih etmektir. Aradaki fark, bir web uygulamasının güvenliği ihlal edildiğinde ortaya çıkar. Enforcing modunda, saldırganın kodu httpd_t içinde çalışır; bu nedenle web içeriğini okuyabilir, ancak Unix kullanıcısının izin verip vermediğine bakılmaksızın politika gereği /etc/shadow dosyasını okuması veya bir systemd birimi yazması reddedilir. Hiçbir politika yüklü olmadığında ise aynı kod, servis hesabının sahip olduğu her şeye erişebilir.

Devre dışı bırakmanın daha sonra ödeyeceğiniz bir bedeli de vardır. Hiçbir politika yüklü değilken, yeni dosyalar etiketsiz oluşturulur; bu nedenle dosya sistemi politika ile uyumsuz hale gelir. SELinux'u tekrar açmak, tam bir yeniden etiketleme (relabel) gerektirir veya birçok servis aynı anda hata verir:

sudo fixfiles -F onboot
sudo reboot

Bu komut /.autorelabel dosyasını yazar ve bir sonraki önyükleme sırasında her dosya sistemini yeniden etiketler. Büyük bir diskte bu işlem uzun sürer ve konsol donmuş gibi görünebilir, bu yüzden bekleyebileceğiniz bir zamanda başlatın. Rocky Linux ve AlmaLinux 9 sürümlerinde yapılandırma dosyası artık çekirdek kısmını tek başına kapatmamaktadır ve SELinux'u tamamen devre dışı bırakmanın belgelenmiş yolu bir çekirdek argümanıdır (sudo grubby --update-kernel ALL --args selinux=0). Bu komutu bilmek, başkasından devraldığınız bir sunucuda işinize yarar. Bu, 403 hatası için bir çözüm değildir.

Konteynerlere ek bir etiket ekleme

Red Hat tabanlı bir ana makinede, konteyner süreçleri container_t içinde çalışır ve yalnızca container_file_t etiketli dosyaları okuyabilir. Ana makineden yapılan bir bind mount işlemi, konteyner içinde Permission denied hatasıyla başarısız olurken, ana makinedeki ls -l durumu tamamen normal görünür. :Z soneki, çalışma zamanına mount işlemini yeniden etiketlemesi talimatını verir:

docker run -d -v /data/appdata:/var/lib/app:Z myimage

:Z, dizini yalnızca bu konteyner için etiketler. :z ise dizini konteynerler arasında paylaşım için etiketler. :Z parametresini diğer servislerin kullandığı bir dizine yönlendirirseniz, bu dizini özyinelemeli (recursive) olarak yeniden etiketler ve bu durum söz konusu servislerin çalışmasını engeller; bu nedenle konteynerlere kendi yollarını tanımlayın. Kurulumla ilgili diğer tüm detaylar, VPS üzerinde Docker çalıştırma bölümünde ele alınan diğer tüm imajlarla aynıdır.

Ubuntu ve Debian size AppArmor sunar

Aynı iş, farklı tasarım. AppArmor, disk üzerindeki dosyaları etiketlemek yerine /etc/apparmor.d/ altındaki bir profili kullanarak bir programı çalıştırılabilir dosyasının yolu üzerinden kısıtlar. Yeniden etiketlenecek bir şey yoktur ve restorecon bulunmaz. Buradan başlayın:

sudo aa-status
sudo journalctl -k | grep -i apparmor

Bir reddetme işlemi apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r" olarak görünür. İş akışı aynı şekildedir: reddetme kaydını okuyun, profili bulun, kuralı değiştirin. sudo apt install apparmor-utils size aa-complain (bir profil için izin verici mod) ve geri almak için aa-enforce komutlarını sağlar. Ubuntu, paketlenmiş servislerin seçili bir kümesini kısıtlar ve geri kalanını kısıtlamasız bırakır; bu nedenle varsayımda bulunmak yerine neyin gerçekten aktif olduğunu görmek için aa-status komutunu okuyun.

Her iki sistemde de geçerli olan bir alışkanlık vardır. Bir servis doğru görünen bir şey üzerinde Permission denied hatası verdiğinde, izinlere dokunmadan önce güvenlik günlüğünü okuyun. Bitler nadiren iki kez sorun çıkarır.

FAQ

Dosya izinleri doğru olmasına rağmen Nginx neden 403 hatası veriyor?

Çünkü erişimi engelleyen şey dosya izinleri değil, SELinux kısıtlamalarıdır. Web sunucusu httpd_t etki alanında çalışır ve yalnızca web içeriği olarak etiketlenmiş dosyaları okuyabilir; bu nedenle default_t veya admin_home_t etiketli bir dosyaya erişim reddedilir ve Nginx sunacak içerik bulamaz. Durumu sudo ausearch -m AVC -ts recent ile doğrulayın; bu komut, httpd_t ile biten ve yanlış türü barındıran tcontext etiketli scontext dosyasını gösterecektir. Ardından doğru etiketi kaydedin ve uygulayın: sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" komutunu takiben sudo restorecon -Rv /data/www komutunu çalıştırın.

Bir servisi çalıştırmak için setenforce 0 kullanmak güvenli midir?

setenforce 0 bir çözüm değil, tanı koyma adımıdır. Sorunu bir kez yeniden oluşturup tüm engelleme kayıtlarının log dosyasına düşmesini sağlamak için kullanın, ardından sudo ausearch -m AVC -ts recent ile kayıtları inceleyin, sudo setenforce 1 komutunu çalıştırın ve temel nedenleri giderin. Permissive modda bırakılan bir sunucu, tüm engellemeleri günlüğe kaydeder ancak hiçbirini durdurmaz; bu durum, sistemin güvenlik korumasını devre dışı bırakırken log kayıtlarını gereksiz yere doldurmanıza neden olur. Çalışma sırasında yalnızca belirli bir servisin esnekliğe ihtiyacı varsa, makinenin geri kalanının enforcing modda kalması için sudo semanage permissive -a httpd_t komutunu kullanın.

SELinux enforcing moddayken bir servisi standart olmayan bir portta nasıl çalıştırırım?

Portu, servisin bağlanmasına izin verilen tür listesine ekleyin. 8081 portunda bir web sunucusu için: sudo semanage port -a -t http_port_t -p tcp 8081. 2222 portunda SSH için: sudo semanage port -a -t ssh_port_t -p tcp 2222. Mevcut listeyi öncelikle sudo semanage port -l | grep -w http_port_t ile kontrol edin, çünkü zaten listelenmiş bir port eklenmeye çalışıldığında ValueError: Port tcp/8081 already defined hatası alınır. Bu adım uygulanmazsa, başka hiçbir işlem portu kullanmıyor olsa bile daemon başlatma sırasında bind() ... Permission denied hatası vererek kapanır.

Ubuntu'da SELinux var mı?

Hayır. Ubuntu ve Debian, dosya etiketleri yerine çalıştırılabilir dosyanın yoluna bağlı profiller uygulayan AppArmor ile gelir. Durumu sudo aa-status ile kontrol edin ve sudo journalctl -k içindeki apparmor="DENIED" satırlarına bakın. Ubuntu yalnızca belirli paketlenmiş servisleri kısıtlar, bu nedenle birçok program varsayılan olarak kısıtlanmamış (unconfined) şekilde çalışır. SELinux'un kutudan çıktığı haliyle enforcing modda olduğu dağıtımlar Fedora, RHEL, Rocky Linux ve AlmaLinux'tur.