SELinux Server: Penyebab nginx 403 Meski Izin Benar
nginx mengembalikan 403 meski izin file benar? Baca denial SELinux, perbaiki label dengan semanage dan restorecon, lalu tetap gunakan enforcing.
Mengapa nginx mengembalikan 403 pada file yang izinnya sudah benar
Jika nginx mengembalikan 403 pada file yang bit izinnya sudah benar, penyebabnya hampir selalu SELinux (security-enhanced Linux) yang menolak operasi baca. SELinux memeriksa sekumpulan aturan kedua setelah pemeriksaan izin normal berhasil. Web server hanya boleh membaca file yang memiliki label konten web. File Anda memiliki label yang berbeda, sehingga operasi open gagal dan nginx tidak memiliki data untuk dikirim.
Periksa labelnya, bukan hanya modenya:
ls -ldZ /data/www /data/www/index.htmldrwxr-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.htmlTitik yang dicetak setelah drwxr-xr-x berarti file tersebut memiliki label SELinux. default_t adalah label yang diperoleh path ketika policy belum pernah mengenalinya. Tidak ada aturan web server yang mengizinkan pembacaan tipe tersebut. Error log menampilkan error Unix biasa. Karena itu, masalah ini terlihat seperti masalah izin:
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"Kernel mengembalikan 13: Permission denied untuk kedua jenis penolakan: penolakan biasa dan penolakan SELinux. Jadi, langkah pertama adalah mencari tahu lapisan mana yang menolak akses. Jangan mulai dengan setenforce 0.
Bagian model yang perlu Anda pahami
SELinux adalah mandatory access control, biasanya ditulis MAC. Setiap proses berjalan dalam sebuah domain, misalnya httpd_t untuk web server. Setiap file dan setiap port jaringan memiliki sebuah type, misalnya httpd_sys_content_t. Policy adalah daftar kombinasi domain, type, dan tindakan yang diizinkan. Apa pun yang tidak tercantum dalam daftar tersebut akan ditolak. Pemeriksaan ini berjalan setelah pemeriksaan Unix klasik, sehingga bit permission pada drwxr-xr-x tetap harus mengizinkan akses terlebih dahulu. Kedua lapisan tersebut harus menyatakan akses diizinkan.
Context lengkap memiliki empat field yang dipisahkan oleh titik dua, seperti system_u:system_r:httpd_t:s0: user SELinux, role, type, dan level. Pada server, Anda hampir selalu bekerja dengan field ketiga, yaitu type. Dua perintah berikut menampilkan nilai yang sedang digunakan:
ps -eZ | grep nginx
id -ZWorker nginx menampilkan context yang diakhiri dengan httpd_t. Shell login Anda menampilkan unconfined_u:unconfined_r:unconfined_t:s0 karena policy targeted default membatasi service dan membiarkan user interaktif tanpa pembatasan tersebut. Hal ini penting untuk diketahui karena SELinux tidak menggantikan menjalankan service dengan user berhak istimewa minimum. SELinux membatasi resource yang dapat diakses service setelah seseorang membobolnya.
Tiga mode dan image yang menggunakan SELinux
sestatus
getenforceEnforcing memblokir dan mencatat. Permissive mengizinkan semuanya serta mencatat tindakan yang seharusnya diblokir. Disabled tidak memuat kebijakan apa pun. getenforce menampilkan mode saat ini. sestatus juga menampilkan mode dari /etc/selinux/config, yaitu mode yang akan aktif kembali setelah reboot.
Rocky Linux, AlmaLinux, Fedora, dan RHEL menggunakan SELinux dalam mode enforcing dengan kebijakan targeted. Ubuntu dan Debian menggunakan AppArmor, yang menjalankan fungsi yang sama dengan mekanisme berbeda (bagian terakhir membahasnya). Karena itu, aplikasi yang sama dapat terpasang tanpa masalah pada salah satu server Anda, tetapi mengembalikan 403 pada server lainnya.
Instal alat sebelum Anda membutuhkannya
sudo dnf install -y policycoreutils-python-utils setroubleshoot-serversemanage: command not found pada image minimal berarti policycoreutils-python-utils tidak tersedia: paket tersebut menyediakan semanage dan audit2allow. setroubleshoot-server menambahkan sealert dan menulis ringkasan setiap penolakan dalam bahasa yang mudah dipahami ke journal. Instal keduanya pada server baru, karena saat Anda membutuhkannya biasanya ada sesuatu yang sudah rusak.
Cara membaca penolakan SELinux dalam log audit
Setiap penolakan dicatat oleh audit daemon sebagai pesan AVC (access vector cache):
sudo ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recenttype=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=0Empat field menjelaskan seluruh kejadian. comm adalah program yang diblokir. scontext adalah source context, yaitu domain tempat proses berjalan. tcontext adalah target context, yaitu label pada objek yang hendak diaksesnya. tclass adalah jenis objek, dalam hal ini file. Jika dibaca bersama: proses dalam httpd_t mencoba membaca file berlabel default_t, dan permissive=0 menunjukkan bahwa permintaan tersebut benar-benar diblokir, bukan hanya dicatat.
Jika ausearch tidak menampilkan apa pun, audit daemon mungkin tidak berjalan. Penolakan kemudian masuk ke kernel ring buffer:
sudo journalctl -k | grep -i avcSekarang ubah record tersebut menjadi kalimat:
sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.logaudit2why membaca record yang sama dan menyebutkan penyebab yang dikenali: boolean yang dinonaktifkan, label yang tidak sesuai dengan policy, atau tidak adanya rule sama sekali. sealert menelusuri seluruh log dan menampilkan command yang disarankan untuk setiap penolakan. Perlakukan saran tersebut sebagai petunjuk. Teksnya dapat berubah antar-rilis, dan sealert terkadang menyarankan custom policy module meskipun perbaikan label satu baris adalah jawaban yang tepat.
Ada satu hal lagi yang perlu diketahui. Policy berisi rule dontaudit yang menyembunyikan penolakan yang dianggap tidak berbahaya. Akibatnya, program dapat mengalami masalah sementara log tetap kosong. Tampilkan penolakan tersebut selama satu pengujian:
sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -BMemperbaiki path yang salah label dengan semanage fcontext dan restorecon
Gunakan dua perintah, dan urutannya penting. semanage fcontext -a mencatat label yang seharusnya digunakan untuk suatu path. restorecon menerapkan default yang telah dicatat tersebut ke file di disk.
sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.htmlPath tersebut adalah regular expression. (/.*)? mencakup direktori itu sendiri dan semua isinya, sesuai kebutuhan document root. Lihat perubahan yang akan dilakukan sebelum menerapkannya: sudo restorecon -Rvn /data/www menampilkan relabel yang direncanakan karena -n berarti tidak melakukan tindakan apa pun. Setelah menjalankan restorecon yang sebenarnya, label terbaca sebagai httpd_sys_content_t dan error 403 hilang tanpa restart service.
Gunakan chcon hanya untuk pengujian. chcon -t httpd_sys_content_t index.html menetapkan label secara langsung. Perintah restorecon berikutnya, pembaruan paket, atau relabel penuh akan meresetnya karena policy masih menyatakan bahwa path tersebut seharusnya memiliki label lain. semanage fcontext adalah versi yang tetap bertahan. Tampilkan catatan yang telah Anda simpan dengan sudo semanage fcontext -l | grep '^/data'.
Konten yang harus dapat ditulis oleh service memerlukan type yang berbeda. Gunakan httpd_sys_rw_content_t untuk direktori upload atau cache, dan batasi penerapannya pada path tersebut: situs yang hanya perlu dibaca tetapi menggunakan type yang dapat ditulis akan memberikan akses lebih besar daripada yang diperlukan aplikasi.
Mengapa label tersebut bisa salah? Hampir selalu karena cara file tersebut dipindahkan. mv mempertahankan label file yang sudah ada. Akibatnya, situs yang dipindahkan dari /root tiba dengan label admin_home_t dan tetap menggunakan label tersebut. cp biasa memberikan file baru label default dari direktori tujuan, dan biasanya itulah yang Anda inginkan. Sementara itu, cp -a dan rsync -X menyalin label sumber bersama file. git clone ke direktori tingkat teratas yang baru menghasilkan default_t. Jika halaman dapat dimuat dari /usr/share/nginx/html tetapi gagal dari direktori Anda sendiri, inilah penyebabnya.
Perbaiki jenis perilaku dengan boolean
Tidak semua kegagalan disebabkan oleh masalah label. Reverse proxy pada sistem Rocky atau AlmaLinux yang baru diinstal mengembalikan 502, dan log error berisi:
2026/08/11 10:02:55 [crit] 1183#1183: *3 connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstreamUpstream Anda berfungsi dengan baik. Domain httpd_t secara default tidak diizinkan membuka koneksi jaringan keluar, sehingga panggilan connect() ditolak sebelum mencapai antarmuka loopback. Satu pengaturan mengendalikan seluruh perilaku tersebut:
getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on-P adalah flag yang penting: flag ini menulis nilai ke disk. Tanpa -P, perubahan akan hilang saat reboot berikutnya. Akibatnya, service hanya berfungsi sampai mesin dimulai ulang. Konfirmasikan dengan semanage boolean -l | grep httpd_can_network_connect, yang mencetak nilai yang sedang berjalan di sebelah nilai yang tersimpan.
Gunakan boolean sebagai pengganti rule yang ditulis manual jika tersedia. Boolean disertakan dalam kebijakan distribusi, sehingga dipelihara, didokumentasikan, dan mudah ditemukan oleh orang berikutnya. getsebool -a mencantumkan semua boolean pada sistem.
Mengizinkan service mendengarkan pada port nonstandar
Port juga memiliki label. Pindahkan nginx ke 8081, lalu nginx gagal dijalankan:
nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)httpd_t dapat mengikat port berlabel http_port_t, dan 8081 bukan salah satunya. Tambahkan port tersebut:
sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081Periksa daftarnya terlebih dahulu. Beberapa port bernomor tinggi sudah diizinkan, termasuk 8008 dan 8443, dan menambahkan port yang sama dua kali akan gagal dengan ValueError: Port tcp/8081 already defined. Jika port tersebut sudah dimiliki oleh tipe lain, ubah tipenya dengan semanage port -m -t http_port_t -p tcp 8081, bukan dengan menambahkannya.
Perintah yang sama diperlukan agar port SSH yang dipindahkan dapat digunakan. Bind to port 2222 on 0.0.0.0 failed: Permission denied di journalctl -u sshd berarti 2222 belum ada di ssh_port_t, jadi jalankan sudo semanage port -a -t ssh_port_t -p tcp 2222 sebelum me-restart daemon dan menutup sesi Anda. Langkah ini sering dilewatkan saat mengikuti panduan umum untuk memperkuat keamanan SSH pada VPS di image keluarga Red Hat. SELinux juga bukan firewall, jadi port tersebut tetap harus dibuka: sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload di sini, atau ufw pada image Debian atau Ubuntu.
Jika tidak ada boolean dan label yang dapat diubah
Situasi ini jarang terjadi pada server biasa dan dapat menyebabkan kerusakan. audit2allow dapat membuat modul kebijakan berdasarkan penolakan akses dalam log:
sudo ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local
cat nginx_local.te
sudo semodule -i nginx_local.ppBaca nginx_local.te sebelum memasangnya. Dua kebiasaan berikut membantu menjaga keamanan proses ini. Filter input agar hanya mencakup program yang sedang Anda perbaiki dengan -c, karena meneruskan penolakan akses yang tidak terkait selama seminggu ke audit2allow akan memberikan semuanya sekaligus. Jangan pernah memasang modul yang dibuat dari penolakan akses yang tidak dapat Anda jelaskan. Aturan yang memungkinkan httpd_t membaca semua file di server mudah dibuat dan sulit ditemukan beberapa bulan kemudian. Hapus modul dengan sudo semodule -r nginx_local.
Mode permissive adalah mode diagnosis, bukan solusi
sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1Mode permissive mengizinkan akses dan mencatatnya. Nilai utamanya adalah kelengkapan. Dalam mode enforcing, service berhenti pada penolakan pertama. Anda memperbaiki penolakan tersebut, melakukan restart, lalu menemukan penolakan kedua. Dalam mode permissive, proses terus berjalan dan log mengumpulkan semua penolakan dalam satu kali proses. Setelah itu, Anda kembali ke mode enforcing dan memperbaikinya sekaligus.
setenforce tidak mengubah /etc/selinux/config. Karena itu, reboot mengembalikan server ke mode enforcing. Ini menjadi lapisan pengaman. Hal ini juga menjelaskan mengapa "perbaikan" yang hanya terdiri atas setenforce 0 muncul kembali pada saat yang paling buruk. Jika satu service memerlukan kelonggaran saat Anda mengerjakannya, tandai domain tersebut, bukan seluruh mesin: sudo semanage permissive -a httpd_t membiarkan semua bagian lain tetap enforcing, sedangkan sudo semanage permissive -d httpd_t membatalkannya.
Mengapa menonaktifkan SELinux lebih merugikan daripada memperbaiki label
Menetapkan SELINUX=disabled di /etc/selinux/config menukar perbaikan label satu baris dengan server yang keamanannya lebih lemah secara permanen. Perbedaannya terlihat ketika aplikasi web mengalami breach. Dalam mode enforcing, kode penyerang berjalan di httpd_t. Kode tersebut mungkin dapat membaca konten web, tetapi akses untuk membaca /etc/shadow atau menulis unit systemd ditolak oleh kebijakan, terlepas dari izin yang diberikan oleh pengguna Unix. Tanpa kebijakan yang dimuat, kode yang sama memperoleh semua akses yang dimiliki akun service.
Menonaktifkan SELinux juga menimbulkan biaya yang harus ditanggung kemudian. Selama tidak ada kebijakan yang dimuat, file baru dibuat tanpa label. Akibatnya, kondisi filesystem tidak lagi sesuai dengan kebijakan. Jika SELinux diaktifkan kembali, Anda harus melakukan relabel penuh. Jika tidak, banyak service dapat gagal secara bersamaan:
sudo fixfiles -F onboot
sudo rebootPerintah tersebut menulis /.autorelabel dan melakukan relabel pada setiap filesystem saat boot berikutnya. Pada disk besar, proses ini memerlukan waktu lama dan konsol dapat terlihat seperti berhenti. Jalankan proses tersebut saat Anda dapat menunggunya. Pada Rocky Linux dan AlmaLinux 9, file konfigurasi tidak lagi menonaktifkan komponen kernel secara otomatis. Cara yang didokumentasikan untuk menonaktifkan SELinux sepenuhnya adalah menggunakan argumen kernel (sudo grubby --update-kernel ALL --args selinux=0). Perintah tersebut berguna ketika Anda mengambil alih server milik orang lain. Namun, perintah itu bukan solusi untuk error 403.
Container memerlukan satu label tambahan
Pada host keluarga Red Hat, proses container berjalan dalam container_t dan hanya dapat membaca file berlabel container_file_t. Bind mount dari host gagal dengan Permission denied di dalam container, sedangkan ls -l pada host terlihat normal. Sufiks :Z memberi tahu runtime untuk memberi label ulang pada mount:
docker run -d -v /data/appdata:/var/lib/app:Z myimage:Z memberi label pada direktori hanya untuk container ini. :z memberi label pada direktori agar dapat digunakan bersama oleh beberapa container. Jika :Z diarahkan ke direktori yang digunakan service lain, runtime akan memberi label ulang secara rekursif pada direktori tersebut dan menyebabkan service itu gagal. Karena itu, sediakan path khusus untuk container. Konfigurasi lainnya sama seperti image lain, sebagaimana dijelaskan dalam menjalankan Docker di VPS.
Ubuntu dan Debian menyediakan AppArmor
Tugasnya sama, tetapi desainnya berbeda. AppArmor membatasi program berdasarkan path ke file executable-nya, menggunakan profile di bawah /etc/apparmor.d/, bukan dengan memberi label pada file di disk. Tidak ada file yang perlu diberi label ulang dan tidak ada restorecon. Mulai dari sini:
sudo aa-status
sudo journalctl -k | grep -i apparmorPenolakan akan muncul sebagai apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". Alurnya tetap sama: baca penolakan, temukan profile, lalu ubah rule. sudo apt install apparmor-utils memberi Anda aa-complain (permissive untuk satu profile), sedangkan aa-enforce mengembalikannya. Ubuntu membatasi sekumpulan service yang dipaketkan dan membiarkan service lainnya tanpa pembatasan. Karena itu, baca aa-status untuk melihat apa yang benar-benar aktif, bukan membuat asumsi.
Ada satu kebiasaan yang berlaku pada kedua sistem. Jika sebuah service melaporkan Permission denied pada sesuatu yang tampak benar, baca log keamanan sebelum mengubah permission. Bit permission jarang menjadi masalah untuk kedua kalinya.
FAQ
Mengapa nginx mengembalikan 403 ketika izin file sudah benar?
Karena SELinux menolak operasi baca, bukan karena bit izin. Server web berjalan dalam domain httpd_t dan hanya dapat membaca file yang diberi label untuk konten web. Karena itu, file berlabel default_t atau admin_home_t ditolak dan nginx tidak memiliki konten untuk disajikan. Konfirmasikan dengan sudo ausearch -m AVC -ts recent. Perintah ini menampilkan scontext yang berakhir pada httpd_t dan tcontext yang memiliki tipe yang salah. Kemudian catat label yang benar dan terapkan dengan sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?", dilanjutkan dengan sudo restorecon -Rv /data/www.
Apakah aman menjalankan setenforce 0 agar service dapat berjalan?
setenforce 0 adalah langkah diagnostik, bukan perbaikan. Gunakan perintah tersebut sekali untuk mereproduksi masalah agar log mengumpulkan semua penolakan dalam satu proses. Baca penolakan tersebut dengan sudo ausearch -m AVC -ts recent, lalu jalankan sudo setenforce 1 dan perbaiki penyebabnya. Server yang dibiarkan dalam mode permissive mencatat setiap penolakan tetapi tidak memblokir apa pun. Akibatnya, log tetap berisi banyak pesan dan perlindungan hilang. Jika satu service memerlukan kelonggaran saat Anda melakukan perbaikan, jalankan sudo semanage permissive -a httpd_t agar bagian mesin lainnya tetap berada dalam mode enforcing.
Bagaimana cara menjalankan service pada port nonstandar dengan SELinux enforcing?
Tambahkan port tersebut ke tipe yang diizinkan untuk digunakan oleh service itu. Untuk server web pada port 8081: sudo semanage port -a -t http_port_t -p tcp 8081. Untuk SSH pada port 2222: sudo semanage port -a -t ssh_port_t -p tcp 2222. Periksa daftar saat ini terlebih dahulu dengan sudo semanage port -l | grep -w http_port_t, karena port yang sudah tercantum akan menyebabkan ValueError: Port tcp/8081 already defined. Tanpa langkah ini, daemon berhenti saat startup dengan bind() ... Permission denied meskipun tidak ada proses lain yang menggunakan port tersebut.
Apakah Ubuntu memiliki SELinux?
Tidak. Ubuntu dan Debian menggunakan AppArmor, yang menerapkan profil berdasarkan path executable, bukan label pada file. Periksa dengan sudo aa-status dan cari baris apparmor="DENIED" dalam sudo journalctl -k. Ubuntu membatasi sekumpulan service yang dipaketkan, sehingga banyak program berjalan tanpa pembatasan secara default. Anda akan menemukan SELinux dalam mode enforcing secara default pada Rocky Linux dan AlmaLinux, serta Fedora dan RHEL.