Deploy-nya selesai tanpa satu pun langkah yang keluar dengan status bukan nol, dan tujuh menit berikutnya situs produksi yang saya pegang balas 500 di setiap URL. Yang membuat tujuh menit itu terasa jauh lebih lama adalah /wp-login.php ikut 500. Tidak ada dashboard yang bisa dibuka, tidak ada plugin yang bisa dimatikan dari admin, tidak ada tombol apa pun yang bisa saya pencet dari dalam. Pintu masuknya ikut terkunci bersama seluruh rumahnya.
Deploy-nya sendiri baru saja selesai dan melaporkan sukses. Skripnya menyiapkan berkas di folder staging, membungkusnya jadi arsip, mengirimnya lewat SSH, lalu mengekstraknya di server. Tidak ada satu pun langkah yang keluar dengan status bukan nol.
Semua pemeriksaan yang saya percaya justru lolos
Yang bikin ini lambat sekali didiagnosis bukan kurangnya informasi, tapi informasi yang semuanya bilang "aman".
php -l saya jalankan di server untuk berkas yang barusan diganti. Bersih, tidak ada syntax error. Saya naik satu tingkat dan menulis skrip kecil yang me-load WordPress dari CLI lalu mencetak satu baris kalau berhasil:
WP LOADED OKWordPress bisa dimuat penuh dari command line. Berarti bukan fatal di functions.php, pikir saya waktu itu. Lalu saya bandingkan md5 berkas lokal lawan berkas di server, satu per satu, dan hasilnya identik semua. Isi berkasnya sampai persis seperti yang saya kirim. Saya juga memeriksa berkas konfigurasi PHP tingkat direktori yang di server itu sudah beberapa kali jadi tersangka, dan berkas itu tidak tersentuh sama sekali oleh deploy ini.
Jadi posisinya begini: sintaksnya benar, isinya benar, WordPress bisa dimuat, konfigurasinya tidak berubah, dan situsnya tetap 500 di setiap URL. Yang rusak cuma satu jalur, yaitu jalur web. Dan pada titik itu saya masih menghabiskan waktu di jalur yang salah, karena semua alat ukur yang saya pakai berjalan di sisi yang sehat.
Satu kolom yang tidak saya baca
Selama beberapa menit pertama saya memverifikasi keberadaan berkas dengan ls. Berkasnya ada, namanya benar, tanggalnya baru. Cukup, pikir saya, lanjut cari di tempat lain.
Yang akhirnya membuka semuanya adalah menambahkan satu huruf:
ls -l wp-content/themes/tema/inc/-rw------- 1 deploy www-data 4821 Aug 20 10:11 helpers.php-rw-------. Mode 600. Pemiliknya deploy, grupnya www-data, dan grup itu tidak punya hak baca sama sekali.
PHP-FPM di server itu berjalan sebagai www-data. Salah satu berkas mode 600 tadi di-require dari functions.php. Setiap request web masuk sebagai www-data, mencoba membaca berkas yang secara eksplisit hanya boleh dibaca pemiliknya, gagal di situ, dan berakhir jadi fatal sebelum apa pun sempat dirender. Termasuk halaman login, karena halaman login juga memuat tema.
Sekarang semua pemeriksaan tadi masuk akal. php -l, skrip pemuat WordPress, dan md5sum semuanya saya jalankan lewat SSH sebagai deploy, yaitu pemilik berkasnya. Sebagai pemilik, saya bisa membaca berkas mode 600 tanpa hambatan apa pun. Saya sedang membuktikan berulang kali bahwa berkas itu terbaca oleh user yang memang berhak, sementara yang bermasalah adalah user yang lain.
Dari mana mode 600 itu datang
Sumbernya ada di baris paling awal skrip deploy, jauh dari berkas yang rusak.
umask 077Baris itu saya tulis dengan alasan yang benar-benar masuk akal. Skrip deploy membuat berkas askpass sementara yang berisi password SSH, dan berkas semacam itu tidak boleh terbaca user lain di mesin yang sama. umask 077 menjamin apa pun yang lahir setelahnya hanya bisa dibaca pemiliknya.
Masalahnya, shell yang sama juga menyiapkan berkas deploy. Sebelum dibungkus, tiap berkas dinormalisasi akhir barisnya:
tr -d '\r' < "$src" > "stage/$f"Redirection > membuat berkas baru, dan berkas baru itu lahir di bawah umask 077, jadi modenya 600. Bukan sebagian, tapi semuanya. tar kemudian merekam mode 600 itu apa adanya ke dalam arsip.
Di sisi server saya sudah merasa aman karena ekstraksinya memakai flag yang saya kira menjinakkan izin berkas:
tar xzf deploy.tar.gz --no-same-permissions -C /var/www/htmlDi sinilah asumsi saya meleset. --no-same-permissions tidak melebarkan mode apa pun. Yang dia lakukan adalah membuang bit setuid, setgid, dan sticky, lalu menerapkan umask ke mode yang tercatat di arsip. Umask hanya bisa mencabut hak, tidak pernah menambahkannya. Mode 600 yang masuk tetap keluar sebagai 600, dan flag itu justru terdengar seperti pengaman padahal fungsinya hampir kebalikan dari yang saya bayangkan.
Rantainya utuh dari ujung ke ujung: satu baris umask yang dipasang untuk melindungi berkas password, mengalir diam-diam ke seluruh berkas staging, ikut terbungkus ke dalam arsip, lolos dari flag yang saya kira menetralkannya, lalu mendarat di produksi sebagai berkas yang tidak bisa dibaca web server.
Perbaikannya
Pemadaman langsung selesai dengan melebarkan izin di seluruh pohon tema, lalu memastikan hasilnya benar-benar berubah. Server itu setgid ke www-data dan user deploy ada di grup yang sama, jadi 664 sudah cukup:
find /var/www/html/wp-content/themes/tema -type f -exec chmod 664 {} +
ls -l wp-content/themes/tema/inc/Situsnya kembali hidup di request berikutnya, tanpa restart PHP-FPM, tanpa clear cache, tanpa apa pun. Memang tidak pernah ada yang salah dengan kodenya.
Sesudah itu saya benahi penyebabnya supaya tidak bisa terulang. umask 077 saya buang dari shell yang menyiapkan deploy, dan berkas askpass tetap diamankan, tapi secara eksplisit di berkasnya sendiri:
: > "$askpass"
chmod 600 "$askpass"Cara ini lebih baik bukan cuma karena efek sampingnya hilang, tapi karena niatnya jadi terbaca. chmod 600 "$askpass" mengatakan dengan jelas berkas mana yang dilindungi. umask 077 mengatakan "semua yang lahir setelah baris ini", dan "semua" itu ternyata jauh lebih luas dari yang saya maksud.
Lalu dua pemeriksaan yang sekarang selalu ikut di akhir deploy. Yang pertama menyapu pohon untuk mencari berkas yang tidak terbaca grup:
find /var/www/html -type f ! -perm -g+rKalau perintah itu mencetak sesuatu, deploy-nya belum selesai. Kalau tidak mencetak apa pun, tidak ada berkas yang tersembunyi dari web server.
Yang kedua, dan ini yang paling murah, adalah memukul situsnya sendiri lewat HTTP sebelum menyentuh urusan cache atau apa pun:
for u in / /wp-login.php /kontak/; do
printf '%s ' "$u"
curl -s -o /dev/null -w '%{http_code}\n' "https://origin.example$u"
doneBeberapa detik, tiga angka, dan seluruh kelas kegagalan ini ketahuan seketika. Kalau saya menjalankan ini di menit pertama, tujuh menit tadi akan jadi satu menit.
Yang saya bawa pulang
Pemeriksaan lewat CLI tidak bisa membuktikan sebuah deploy terbaca oleh web server, karena keduanya berjalan sebagai user yang berbeda. Selama tujuh menit itu saya bukan kekurangan data, saya justru punya banyak data yang semuanya benar dan semuanya tidak relevan, karena tidak ada satu pun yang diambil dari sisi yang sedang rusak. Alat ukur yang berjalan sebagai pemilik berkas akan selamanya bilang bahwa berkas itu terbaca.
Kalau seluruh situs 500 sementara CLI terlihat sehat, urutan curiga yang benar adalah izin berkas atau auto_prepend_file lebih dulu, baru kodenya. Kode yang rusak biasanya merusak sebagian, dan kerusakan yang rata di setiap URL tanpa kecuali itu lebih sering berarti ada sesuatu yang mengganjal sebelum kode kamu sempat jalan.
Dan yang paling saya ingat dari kejadian ini: umask itu bukan pengaturan lokal. Dia melekat ke seluruh sisa hidup shell tersebut dan ke setiap berkas yang lahir di sana. Kalau sebuah shell punya dua pekerjaan, menjaga rahasia dan mengirim berkas ke produksi, jangan pernah menyetel kebijakan izin di tingkat shell. Setel di berkas yang memang perlu dijaga, dan biarkan sisanya lahir normal.