SSD Nodes Learn Hosting plans →
Panduan Matt ConnorOleh Matt Connor · Diperbarui 2026-09-04

Mengatasi Error 403 Nginx Akibat SELinux

Nginx mengembalikan error 403 padahal izin file sudah benar? Pelajari cara membaca log SELinux serta menggunakan perintah semanage dan restorecon untuk memperbaikinya.

Mengapa nginx mengembalikan error 403 pada file yang izin aksesnya sudah benar

nginx mengembalikan error 403 pada file yang bit izin aksesnya sudah benar hampir selalu disebabkan oleh SELinux (Security-Enhanced Linux) yang menolak akses baca. SELinux memeriksa sekumpulan aturan kedua setelah izin akses standar terpenuhi, dan server web hanya diizinkan membaca file yang memiliki label konten web. File Anda memiliki label yang berbeda, sehingga operasi buka file gagal dan nginx tidak memiliki data untuk dikirim.

Periksa labelnya, jangan hanya mode izin aksesnya:

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

Titik yang dicetak setelah drwxr-xr-x menandakan bahwa file tersebut memiliki label SELinux. default_t adalah label yang didapatkan sebuah path ketika kebijakan tidak mengenalinya, dan tidak ada aturan dalam server web yang mengizinkan pembacaan tipe tersebut. Log error menampilkan error Unix biasa, itulah sebabnya masalah ini terlihat seperti bug izin akses:

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 tersebut, baik yang biasa maupun yang berasal dari SELinux. Jadi, tugas pertama adalah mencari tahu lapisan mana yang melakukan penolakan. Jangan mulai dengan setenforce 0.

Bagian model yang Anda perlukan

SELinux adalah kontrol akses wajib, yang biasanya ditulis sebagai MAC. Setiap proses berjalan dalam sebuah domain, seperti httpd_t untuk web server. Setiap file dan setiap port jaringan membawa sebuah tipe, seperti httpd_sys_content_t. Kebijakan ini adalah daftar kombinasi domain, tipe, dan tindakan yang diizinkan, dan segala sesuatu yang tidak ada dalam daftar tersebut akan ditolak. SELinux berjalan setelah pemeriksaan Unix klasik, sehingga bit izin dalam drwxr-xr-x tetap harus mengizinkan akses tersebut terlebih dahulu. Kedua lapisan harus memberikan izin.

Konteks lengkap memiliki empat kolom yang dipisahkan oleh titik dua, seperti system_u:system_r:httpd_t:s0: pengguna SELinux, peran, tipe, dan level. Pada server, Anda akan menghabiskan hampir seluruh waktu Anda pada kolom ketiga, yaitu tipe. Dua perintah berikut menampilkan nilai yang sedang aktif:

ps -eZ | grep nginx
id -Z

Worker nginx menunjukkan konteks yang diakhiri dengan httpd_t. Shell login Anda menunjukkan unconfined_u:unconfined_r:unconfined_t:s0, karena kebijakan default targeted membatasi layanan dan membiarkan pengguna interaktif tetap bebas. Hal ini penting untuk diketahui, karena SELinux tidak menggantikan menjalankan layanan di bawah pengguna dengan hak akses minimum. SELinux membatasi apa yang dapat dijangkau oleh suatu layanan setelah seseorang berhasil membobolnya.

Tiga mode, dan distribusi mana yang memiliki SELinux

sestatus
getenforce

Enforcing memblokir dan mencatat aktivitas. Permissive mengizinkan segalanya dan mencatat apa yang seharusnya diblokir. Disabled tidak memuat kebijakan apa pun. getenforce mencetak mode saat ini. sestatus juga mencetak mode dari /etc/selinux/config, yaitu mode yang akan aktif kembali setelah reboot.

Rocky Linux, AlmaLinux, Fedora, dan RHEL merilis SELinux dalam mode enforcing dengan kebijakan targeted. Default yang sama tersebut merupakan warisan, bukan kebetulan, karena keempatnya berasal dari garis keturunan Red Hat yang sama yang melalui CentOS sebelum Rocky Linux dan AlmaLinux muncul. Anda menjalankan salah satu dari keduanya tidak membuat perbedaan pada apa pun di halaman ini, karena keduanya merilis kebijakan dan alat yang sama, sehingga memilih antara Rocky Linux dan AlmaLinux bergantung pada janji kompatibilitas dan dukungan CPU yang lebih lama, bukan pada default keamanan. Ubuntu dan Debian menggunakan AppArmor sebagai gantinya, yang melakukan tugas serupa dengan mekanisme berbeda (bagian terakhir membahas hal ini). Jadi, aplikasi yang sama dapat terinstal dengan lancar di salah satu server Anda namun menghasilkan error 403 di server lainnya.

Instal alat sebelum Anda membutuhkannya

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

semanage: command not found pada image minimal berarti policycoreutils-python-utils tidak tersedia: paket tersebut memuat semanage dan audit2allow. setroubleshoot-server menambahkan sealert dan menulis ringkasan dalam bahasa Inggris sederhana untuk setiap penolakan ke dalam jurnal. Instal keduanya pada server baru, karena saat Anda membutuhkannya adalah saat di mana sesuatu sudah rusak.

Cara membaca penolakan SELinux di log audit

Setiap penolakan dicatat oleh audit daemon sebagai pesan AVC (access vector cache):

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

Empat field menjelaskan seluruh kejadian. comm adalah program yang diblokir. scontext adalah konteks sumber, yaitu domain tempat proses berjalan. tcontext adalah konteks target, yaitu label pada objek yang coba diakses. tclass adalah jenis objek, dalam hal ini sebuah file. Jika dibaca bersamaan: proses di httpd_t mencoba membaca file berlabel default_t, dan permissive=0 menyatakan bahwa permintaan tersebut benar-benar diblokir, bukan sekadar dicatat.

Jika ausearch tidak menampilkan apa pun, kemungkinan audit daemon tidak berjalan. Penolakan kemudian akan masuk ke kernel ring buffer sebagai gantinya:

sudo journalctl -k | grep -i avc

Sekarang ubah catatan tersebut menjadi kalimat yang mudah dipahami:

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

audit2why membaca catatan yang sama dan menyebutkan penyebab yang dikenali: boolean yang dimatikan, label yang tidak sesuai dengan kebijakan, atau ketiadaan aturan sama sekali. sealert menelusuri seluruh log dan mencetak saran perintah untuk setiap penolakan. Anggap saran tersebut sebagai petunjuk. Redaksi pesan dapat berubah antar rilis, dan sealert terkadang mengusulkan modul kebijakan kustom padahal perbaikan label satu baris adalah solusi yang tepat.

Satu hal lagi yang perlu diketahui. Kebijakan berisi aturan dontaudit yang menyembunyikan penolakan yang dianggap tidak berbahaya, sehingga program bisa saja mengalami kegagalan sementara log tetap kosong. Tampilkan kembali penolakan tersebut selama durasi pengujian:

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

Memperbaiki path yang salah label dengan semanage fcontext dan restorecon

Dua perintah diperlukan, dan urutannya penting. semanage fcontext -a mencatat label yang seharusnya dimiliki oleh sebuah path. restorecon menerapkan label default yang tercatat 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.html

Path tersebut berupa ekspresi reguler. (/.*)? mencakup direktori itu sendiri dan semua isinya, yang merupakan kebutuhan untuk document root. Lihat apa yang akan berubah sebelum menerapkannya: sudo restorecon -Rvn /data/www mencetak rencana pelabelan ulang, karena -n berarti tidak ada tindakan yang dilakukan. Setelah restorecon yang sebenarnya, label akan terbaca httpd_sys_content_t dan error 403 hilang tanpa perlu restart service.

Gunakan chcon hanya sebagai pengujian. chcon -t httpd_sys_content_t index.html menetapkan label secara langsung, dan restorecon berikutnya, pembaruan paket, atau pelabelan ulang penuh akan meresetnya, karena kebijakan sistem tetap menyatakan bahwa path tersebut harus memiliki label yang berbeda. Pada mesin di mana dnf-automatic menerapkan pembaruan keamanan sesuai jadwal, reset tersebut terjadi secara otomatis, bukan saat Anda sedang berada di depan mesin, sehingga situs akan rusak beberapa jam setelah hal terakhir yang Anda ubah. semanage fcontext adalah versi yang bersifat permanen. Tampilkan apa yang telah Anda catat dengan sudo semanage fcontext -l | grep '^/data'.

Konten yang harus ditulis oleh service memerlukan tipe yang berbeda. Gunakan httpd_sys_rw_content_t untuk direktori unggahan atau cache, dan batasi hanya pada path tersebut: situs read-only yang berada di bawah tipe writable memberikan akses lebih luas daripada yang dibutuhkan aplikasi.

Mengapa label tersebut salah sejak awal? Hampir selalu karena cara file tersebut dipindahkan. mv mempertahankan label file yang ada, sehingga situs yang dipindahkan dari /root akan tiba dengan label admin_home_t dan tetap seperti itu. cp biasa memberikan label default direktori tujuan ke file baru, yang biasanya merupakan hasil yang diinginkan, sementara cp -a dan rsync -X menyalin label sumber bersama dengan file tersebut. git clone ke direktori tingkat atas yang baru akan menghasilkan default_t. Ketika sebuah halaman dimuat dengan baik dari /usr/share/nginx/html namun gagal dari direktori Anda sendiri, inilah penyebabnya.

Memperbaiki jenis perilaku dengan boolean

Beberapa kegagalan bukan disebabkan oleh masalah label. Reverse proxy pada server Rocky atau AlmaLinux yang baru diinstal mengembalikan error 502, dan log error menampilkan:

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 Anda berjalan dengan baik. Domain httpd_t secara default tidak diizinkan untuk membuka koneksi jaringan keluar, sehingga panggilan connect() ditolak sebelum mencapai antarmuka loopback. Satu switch mengontrol seluruh perilaku tersebut:

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

-P adalah flag yang menentukan: flag ini menulis nilai ke disk. Tanpa -P, perubahan akan hilang pada reboot berikutnya, yang menyebabkan layanan Anda berfungsi hanya sampai mesin dimulai ulang. Konfirmasikan dengan semanage boolean -l | grep httpd_can_network_connect, yang mencetak nilai yang sedang berjalan di samping nilai yang tersimpan.

Pilihlah boolean daripada aturan yang ditulis manual jika tersedia. Boolean disertakan dalam kebijakan distribusi, sehingga terpelihara, terdokumentasi, dan mudah ditemukan oleh orang berikutnya. getsebool -a mencantumkan setiap boolean yang ada pada sistem.

Mengizinkan service mendengarkan pada port non-standar

Port juga memiliki label. Pindahkan nginx ke 8081 dan service tersebut menolak untuk start:

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

httpd_t hanya dapat melakukan bind pada port yang 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 8081

Periksa daftarnya terlebih dahulu. Beberapa port 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 termasuk dalam tipe yang berbeda, ubah dengan semanage port -m -t http_port_t -p tcp 8081 alih-alih menambahkannya.

Perintah yang sama adalah kunci agar port SSH yang telah dipindahkan dapat berfungsi. Bind to port 2222 on 0.0.0.0 failed: Permission denied di dalam journalctl -u sshd berarti 2222 tidak ada di dalam ssh_port_t, jadi jalankan sudo semanage port -a -t ssh_port_t -p tcp 2222 sebelum Anda me-restart daemon dan menutup sesi Anda. Itu adalah langkah yang sering dilewatkan orang saat mengikuti panduan umum untuk pengerasan SSH pada VPS di image keluarga Red Hat. SELinux bukanlah 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. Flag --permanent tersebut membawa jebakan reboot yang sama seperti -P pada boolean, dan zona yang menentukan interface mana yang menerapkan aturan tersebut layak dibaca sekali di dasar-dasar firewalld untuk VPS Rocky atau AlmaLinux.

Saat tidak ada boolean dan label yang dapat diubah

Hal ini jarang terjadi pada server normal, dan di sinilah orang sering menyebabkan kerusakan. audit2allow dapat membuat modul kebijakan dari penolakan (denial) yang ada di log:

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

Baca nginx_local.te sebelum Anda menginstalnya. Dua kebiasaan ini menjaga keamanan proses tersebut. Saring input ke satu program yang sedang Anda perbaiki dengan -c, karena menyalurkan (pipe) penolakan yang tidak relevan selama seminggu ke audit2allow akan memberikan izin untuk semuanya sekaligus. Jangan pernah menginstal modul yang dibuat dari penolakan yang tidak dapat Anda jelaskan: aturan yang mengizinkan httpd_t untuk membaca setiap file di server sangat mudah dibuat dan sulit dideteksi berbulan-bulan kemudian. Hapus modul dengan sudo semodule -r nginx_local.

Permissive adalah mode diagnostik, bukan perbaikan

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

Mode permissive mengizinkan akses dan mencatatnya ke dalam log. Nilai utamanya adalah kelengkapan data. Dalam mode enforcing, layanan akan berhenti pada penolakan pertama, sehingga Anda harus memperbaikinya, melakukan restart, lalu menghadapi penolakan berikutnya. Dalam mode permissive, eksekusi tetap berjalan dan log akan mengumpulkan setiap penolakan dalam satu kali proses, sehingga Anda dapat kembali ke mode enforcing dan memperbaikinya sekaligus.

setenforce tidak mengubah /etc/selinux/config, sehingga proses reboot akan mengembalikan sistem ke mode enforcing. Ini adalah fitur pengaman, dan inilah alasan mengapa "perbaikan" yang hanya mengandalkan setenforce 0 akan muncul kembali pada saat yang paling tidak tepat. Jika sebuah layanan memerlukan ruang saat Anda sedang mengerjakannya, tandai domain tersebut alih-alih seluruh mesin: sudo semanage permissive -a httpd_t membiarkan semua hal lain tetap dalam mode enforcing, dan sudo semanage permissive -d httpd_t akan membatalkan pengaturan tersebut.

Mengapa menonaktifkan SELinux lebih merugikan daripada memperbaiki label

Mengatur SELINUX=disabled di dalam /etc/selinux/config menukar perbaikan label satu baris dengan server yang secara permanen lebih lemah. Perbedaannya terlihat pada hari aplikasi web disusupi. Dalam mode enforcing, kode penyerang berjalan di dalam httpd_t, sehingga mungkin dapat membaca konten web, sementara membaca /etc/shadow atau menulis unit systemd ditolak oleh kebijakan terlepas dari apa yang diizinkan oleh pengguna Unix. Tanpa kebijakan yang dimuat, kode yang sama mendapatkan semua akses yang dimiliki akun layanan tersebut.

Menonaktifkan SELinux juga memiliki konsekuensi di kemudian hari. Saat tidak ada kebijakan yang dimuat, file baru dibuat tanpa label, sehingga sistem file tidak lagi sinkron dengan kebijakan. Mengaktifkan kembali SELinux kemudian memerlukan pelabelan ulang penuh, atau sekumpulan layanan akan gagal secara bersamaan:

sudo fixfiles -F onboot
sudo reboot

Perintah tersebut menulis /.autorelabel dan melabeli ulang setiap sistem file selama proses boot berikutnya. Pada disk yang besar, proses ini memakan waktu lama dan konsol terlihat macet, jadi jalankan saat Anda memiliki waktu luang. Karena mesin akan dimatikan, ada baiknya melihat apa saja yang telah diantrekan untuk dimulai ulang terlebih dahulu, yaitu apa yang dilaporkan needs-restarting setelah pembaruan dnf menyisakan kernel dan pustaka lama di memori. Pada Rocky Linux dan AlmaLinux 9, file konfigurasi tidak lagi mematikan bagian kernel secara mandiri, dan cara yang didokumentasikan untuk menonaktifkan SELinux sepenuhnya adalah melalui argumen kernel (sudo grubby --update-kernel ALL --args selinux=0). Mengetahui perintah tersebut membantu saat Anda mengambil alih server milik orang lain. Itu bukanlah perbaikan untuk error 403.

Menambahkan label pada container

Pada host keluarga Red Hat, proses container berjalan di dalam container_t dan hanya diizinkan membaca file yang berlabel container_file_t. Bind mount dari host akan gagal dengan pesan Permission denied di dalam container, meskipun ls -l pada host terlihat normal. Akhiran :Z memberi tahu runtime untuk melakukan pelabelan ulang pada mount tersebut:

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

:Z melabeli direktori khusus untuk container ini saja. :z melabelinya agar dapat dibagikan antar-container. Arahkan :Z ke direktori yang digunakan layanan lain dan sistem akan melabeli ulang direktori tersebut secara rekursif, yang akan merusak layanan-layanan tersebut, jadi berikan jalur (path) sendiri untuk setiap container. Jika engine belum terpasang, perhatikan bahwa perintah docker pada distribusi ini sering kali merupakan podman yang menggunakan nama tersebut, sebuah detail teknis yang diselesaikan dalam langkah instalasi Rocky dan AlmaLinux sebelum Anda menemui masalah ini. Segala hal lain mengenai pengaturan ini sama dengan image lainnya, yang dibahas dalam menjalankan Docker di VPS.

Ubuntu dan Debian menyediakan AppArmor

Tugas yang sama, desain yang berbeda. AppArmor membatasi program berdasarkan path ke executable-nya, menggunakan profil di bawah /etc/apparmor.d/, alih-alih melabeli file pada disk. Tidak ada yang perlu dilabeli ulang dan tidak ada restorecon. Mulai dari sini:

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

Penolakan muncul sebagai apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". Alur kerjanya memiliki bentuk yang sama: baca penolakan, temukan profilnya, ubah aturannya. sudo apt install apparmor-utils menyediakan aa-complain (permissive untuk satu profil) dan aa-enforce untuk mengembalikannya. Ubuntu membatasi sekumpulan layanan paket tertentu dan membiarkan sisanya tidak dibatasi, jadi baca aa-status untuk melihat apa yang benar-benar aktif alih-alih berasumsi.

Satu kebiasaan berlaku di kedua sistem. Ketika sebuah layanan melaporkan Permission denied pada sesuatu yang terlihat benar, baca log keamanan sebelum Anda menyentuh izinnya. Bit jarang menjadi masalah untuk kedua kalinya.

FAQ

Mengapa nginx mengembalikan 403 padahal izin file sudah benar?

Karena SELinux menolak akses baca, bukan karena bit izin file. Web server berjalan dalam domain httpd_t dan hanya diizinkan membaca file yang dilabeli untuk konten web, sehingga file yang dilabeli default_t atau admin_home_t akan ditolak dan nginx tidak memiliki konten untuk disajikan. Konfirmasikan hal ini dengan sudo ausearch -m AVC -ts recent, yang akan menampilkan scontext yang berakhiran httpd_t dan tcontext yang memiliki tipe yang salah. Kemudian, catat label yang benar dan terapkan: sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" diikuti dengan sudo restorecon -Rv /data/www.

Apakah aman menjalankan setenforce 0 agar sebuah layanan dapat berjalan?

setenforce 0 adalah langkah diagnostik, bukan perbaikan. Gunakan perintah ini untuk mereproduksi masalah sekali saja agar log mencatat setiap penolakan dalam satu sesi, baca log tersebut dengan sudo ausearch -m AVC -ts recent, kemudian jalankan sudo setenforce 1 dan perbaiki penyebabnya. Server yang dibiarkan dalam mode permissive akan mencatat setiap penolakan tanpa memblokir apa pun, sehingga Anda hanya mendapatkan kebisingan log dan kehilangan perlindungan. Jika satu layanan memerlukan ruang saat Anda bekerja, jalankan sudo semanage permissive -a httpd_t agar bagian sistem lainnya tetap dalam mode enforcing.

Bagaimana cara menjalankan layanan pada port non-standar dengan SELinux dalam mode enforcing?

Tambahkan port tersebut ke tipe yang diizinkan untuk di-bind oleh layanan tersebut. Untuk web server 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 terdaftar akan menyebabkan kegagalan dengan pesan ValueError: Port tcp/8081 already defined. Tanpa langkah ini, daemon akan keluar saat startup dengan pesan 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 file eksekusi, bukan label pada file. Periksa statusnya dengan sudo aa-status dan cari baris apparmor="DENIED" di dalam sudo journalctl -k. Ubuntu membatasi sekumpulan layanan paket tertentu, sehingga banyak program berjalan tanpa batasan (unconfined) secara default. Rocky Linux dan AlmaLinux adalah sistem yang menggunakan SELinux secara default, begitu pula Fedora dan RHEL.