- Pada 8 Juli 2023, akun-akun lama menghilang dari instance Mastodon Vivaldi Social, dan pada akhirnya terjadi insiden di mana 198 akun tergabung ke dalam satu akun remote
- Penyebabnya bukan penghapusan langsung atau serangan, melainkan kombinasi antara perilaku penggabungan akun di Mastodon dan konfigurasi replikasi PostgreSQL berbasis Makara di Vivaldi Social, sehingga urutan eksekusi menjadi kacau
- Akun tampak seperti terhapus, tetapi nama pengguna dialokasikan ulang dan gambar avatar serta header juga ikut hilang, sehingga masalahnya dipersempit pada perilaku internal aplikasi Mastodon
- Tim operasi menyiapkan rollback seluruh DB, sambil paralel menjalankan skrip pemulihan selektif untuk mengembalikan akun, postingan, follow, follower, dan data relasi
- Mastodon v4.1.5 mencakup pemblokiran penggunaan Makara oleh worker Sidekiq dan perbaikan urutan penggabungan akun, sehingga operator server yang memakai DB replikasi perlu memeriksa jalur baca worker
Insiden akhir pekan ketika 198 akun menghilang
- Pada Sabtu, 8 Juli 2023 sekitar 17:25 CEST, tab Vivaldi Social kembali meminta login, dan setelah masuk terkonfirmasi bahwa home timeline kosong
- Gejala yang sama muncul di akun administrator sistem lain, dan hasil pemeriksaan basis data menunjukkan bahwa akun-akun yang terdampak telah terhapus lalu dibuat ulang seperti akun baru saat pengguna login kembali
- Vivaldi Social memiliki backup malam hari dari Jumat pukul 23:00 UTC, dan tim operasi mulai menyalin file backup untuk memeriksa kemungkinan pemulihan
- Pada penghapusan akun Mastodon biasa, nama pengguna akan dipesan permanen sehingga tidak bisa dipakai ulang, tetapi dalam insiden ini nama pengguna yang sama dialokasikan kembali, jadi ini bukan penghapusan normal
Penghapusan masih terus berlangsung
- Pada awalnya, akun-akun lama dengan ID di bawah 142 telah hilang, dan pada 19:10 akun hingga ID di bawah 217 juga menghilang, sehingga terungkap bahwa penghapusan masih berlangsung
- Pada 19:18, bantuan diminta dari para pengembang Mastodon, lalu Renaud merespons dan Claire serta Eugen ikut dalam investigasi
- Pada 19:20, setelah instance Docker Mastodon di-restart, penghapusan berhenti, dan ID akun terendah di basis data menjadi 236
- Selama periode insiden, akun yang terhapus atau tergabung akhirnya dipastikan berjumlah 198 akun
Bukan serangan, melainkan dipersempit ke perilaku aplikasi
- Tim operasi dan para pengembang Mastodon memeriksa kemungkinan
UserCleanupSchedulermenghapus akun “unconfirmed”, tetapi pengguna yang terhapus tidak mungkin cocok dengan kondisi kueri itu sehingga hipotesis tersebut dikesampingkan - Karena upgrade ke Mastodon 4.1.3 dilakukan 48 jam sebelum insiden, perubahan antara v4.1.2 dan v4.1.3, termasuk perubahan yang dipublikasikan Vivaldi, ikut ditinjau, tetapi tidak ditemukan penyebab yang relevan
- Avatar dan gambar header akun yang terhapus juga hilang dari filesystem, sehingga dipastikan bahwa yang terjadi bukan sekadar penghapusan langsung di DB, melainkan aplikasi Mastodon sendiri yang menjalankan aksi penghapusan
- Jejak intrusi atau serangan dicari di log dan filesystem, tetapi tidak ada bukti, dan kemungkinan eksploit yang terkait dengan perbaikan keamanan di Mastodon v4.1.3 juga tidak terkonfirmasi
- Pada Sabtu malam, patch untuk menambahkan log atas aksi penghapusan akun diterapkan, dan setelah versi patch dirilis pada 00:29 CEST, tim beristirahat
Petunjuk penentu: postingan yang menumpuk pada satu akun remote
- Pada Minggu 13:56, dilaporkan bahwa halaman profil pakar keamanan Vivaldi, Yngve, menghasilkan error HTTP 500, dan akun tersebut tidak termasuk dalam 198 akun yang terhapus
- Di log, akun yang sama dari instance Mastodon remote yang sama muncul berulang kali, dan dalam artikel ini dianonimkan sebagai akun dari
social.example.com - Kueri yang mengambil status dari akun remote tersebut mengembalikan 17.600 baris
- Pada 14:43, lewat perbandingan dengan backup, terkonfirmasi bahwa semua status dari seluruh akun yang terhapus telah dialokasikan ulang ke satu pengguna di
social.example.com - Setelah 15:00, melalui log
AccountMergingWorker, konsol Rails, dan kueri DB tambahan, dugaan bahwa worker penggabungan akun sedang menggabungkan semua akun ke satu akun remote menjadi semakin kuat
Akar masalah: penggabungan akun dan lag replikasi PostgreSQL
- Vivaldi Social menggunakan konfigurasi replikasi PostgreSQL 2 server, dan proses worker berada dalam kondisi yang memungkinkan pembacaan basis data dari server standby melalui Makara
- Skenario insiden yang diajukan Claire pada 17:28 adalah sebagai berikut
- Vivaldi Social menerima notifikasi perubahan nama akun dari
social.example.com - Saat akun baru dibuat di basis data, field
URImasuk sebagainull - Setelah itu,
URIakun baru diatur ke nilai yang benar dari akun remote - Eksekusi
AccountMergingWorker, yang menggabungkan data dari akun lama ke akun baru melalui Redis, dijadwalkan - Karena lag replikasi basis data, urutan pengaturan
URIdan penjadwalan eksekusi worker menjadi tidak sinkron pada saat pembacaan aktual
- Vivaldi Social menerima notifikasi perubahan nama akun dari
- Karena semua akun lokal di instance Mastodon memiliki nilai
URInull, worker mencocokkan semua akun denganURIyang sama dan menggabungkan semuanya ke akun remote baru - Para pengembang menilai bahwa jika beban basis data meningkat dan lag replikasi menjadi lebih panjang, kejadian seperti ini bisa lebih mudah muncul
- Tim operasi dan para pengembang Mastodon menilai bahwa konfigurasi ini sangat mungkin menjadi akar penyebabnya
Patch dan perubahan konfigurasi
- Setelah penyebabnya dipersempit, tim operasi fokus pada pemulihan data, dan Claire memutuskan menulis patch pencegahan agar kejadian serupa tidak terulang
- Hlini bertugas menerapkan patch dan mengubah konfigurasi replikasi yang sudah tidak lagi direkomendasikan
- Pada 17:58, terjadi masalah saat deployment sehingga muncul satu-satunya downtime total pada akhir pekan itu, lalu pada 18:18 Vivaldi Social kembali online
- Pada 18:44, patch dan perubahan konfigurasi berhasil diterapkan, dan dinilai bahwa insiden yang sama tidak akan terulang
Pemulihan: restorasi selektif alih-alih rollback total
- Awalnya rollback basis data penuh sempat dipertimbangkan, tetapi karena masalah performa yang sudah diketahui, dibutuhkan prosedur rumit berupa mengubah backup
.dumpmenjadi.sqldan memodifikasi file teks 54GB - Tim operasi menjalankan prosedur restorasi penuh dan restorasi selektif secara paralel
- Hlini memodifikasi file
.sql54GB dan menyiapkan restorasi penuh - Thomas menulis skrip untuk memulihkan akun yang terhapus beserta data terkaitnya
- Hlini memodifikasi file
- Saat menulis skrip, ada kesalahan dalam penanganan reference pada parameter binding kueri PDO, dan Ísak yang menemukan masalah itu
- Pada 23:04, bagian pertama yang memperbaiki record user, account, dan identity untuk 198 pengguna terdampak telah selesai
- Pada 23:55, skrip pemulihan selektif untuk mengembalikan status, follows, followers, dan data relasi lainnya ke kondisi sebelum insiden telah selesai
Pemulihan selektif selesai dan perbaikan lanjutan
- Karena adanya constraint relasi basis data, pemulihan dilakukan dalam 2 tahap
- Pertama, memulihkan record user/account/identity untuk seluruh 198 pengguna
- Setelah itu, memulihkan sisa data relasi
- Untuk beberapa pengguna yang login kembali setelah insiden dan membuat follow, muncul error duplicate key, sehingga skrip diubah agar menghapus record lama yang tidak dapat dipulihkan dan mempertahankan record yang lebih baru
- Pada Senin 01:27 CEST, pekerjaan terakhir skrip selesai, dan pada 01:40 reindeks home feed selesai
- Hasilnya, home feed dari 198 akun berhasil dipulihkan, dan rollback total tidak diperlukan
- Pada Senin dan Selasa, masalah lanjutan tambahan ikut diperbaiki
- masalah login pada 6 akun dengan simbol di nama pengguna
- hilangnya data pengaturan web pada 198 akun
- error pada penghitung profil seperti jumlah follower dan jumlah postingan
- 4 akun yang memiliki data tidak benar
Perbaikan resmi dari Mastodon
- Para pengembang Mastodon memberi tahu operator server lain mengenai risiko menjalankan Mastodon pada konfigurasi replikasi berbasis Makara
- Disimpulkan bahwa konfigurasi seperti ini jarang digunakan karena umumnya hanya layak dipertimbangkan pada instance besar seperti Vivaldi Social
- Mastodon v4.1.5 mencakup dua perbaikan terkait insiden ini
Linimasa insiden berdasarkan UTC
- Sabtu 15:15: pesan perubahan nama akun dari instance eksternal dikirim ke Vivaldi Social, dan pekerjaan penggabungan akun yang salah mulai berjalan
- Sabtu 15:25: tanda pertama insiden teramati
- Sabtu 17:20: setelah restart container Docker, pekerjaan penggabungan akun berhenti; total 198 akun terhapus atau tergabung antara 15:15 hingga 17:20
- Minggu 13:00: kemungkinan akar penyebab diidentifikasi
- Minggu 14:25: akar penyebab dikonfirmasi
- Minggu 21:55: pemulihan data dimulai
- Minggu 23:27: pemulihan data selesai
- Senin 10:40: 6 akun dengan simbol di nama pengguna diperbaiki
- Senin 11:05: data pengaturan web yang hilang dipulihkan
- Selasa 15:31: nilai penghitung yang salah diperbaiki
- Selasa 16:01: 4 akun dengan data yang salah diperbaiki
1 komentar
Komentar Hacker News
Ini retrospektif yang bagus, dan khususnya juga menangkap dengan baik seberapa besar dampak biaya manusiawi seperti kurang tidur terhadap penyelesaian insiden yang kompleks
Bagian yang paling mencolok adalah kalimat bahwa “akun baru dibuat di database dengan nilai null pada field URI”
Setiap kali membaca postmortem terkait database, hampir selalu NULL bersembunyi di dekat lokasi kejadian. Sekalipun NULL bukan pelakunya, ia tetap harus selalu masuk daftar yang diinterogasi
Saran saya, jangan bergantung pada NULL sebagai nilai sentinel; kalau bisa, lebih baik tidak mengizinkannya sama sekali di database. Meski tampak ada manfaatnya, beberapa tahun kemudian makna model data berubah, lalu manfaat itu biasanya terhapus oleh bug yang sulit ditemukan: sebuah pernyataan yang tampak tidak berbahaya mengharapkan NULL atau NOT NULL, tetapi menghasilkan hasil tak terduga
Kasus ini memang race condition, tetapi jika akun lokal dan akun remote dibedakan secara jelas dengan tipe, urutan operasi mungkin tidak penting, dan kode penggabungan akun juga bisa dibatasi ke cakupan yang lebih sempit
Null adalah nilai data yang sepenuhnya valid dan harus diperlakukan seperti itu. Nilai default seperti memakai -1 untuk boolean atau nilai kosong untuk string bisa membuat sistem yang seharusnya error saat runtime jika memakai NULL tampak berjalan, tetapi itu tidak berarti sistem berjalan sesuai ekspektasi; ia hanya menjadi diam
Saya mengerti godaan untuk menyembunyikan NULL, tetapi “tidak ada” adalah status data yang sama validnya dengan “ada”, dan sistem pada umumnya harus ditulis untuk menerimanya
Dalam kasus ini, menurut saya masalahnya bukan NULL di database, melainkan NULL di lapisan aplikasi
Jika NULL adalah nilai yang wajib ditangani, semacam monad Maybe, pada akhirnya ia akan ditangani dan dipikirkan. Tidak banyak bedanya apakah itu string kosong, string null dari bahasa yang dipakai, atau nilai penanda khusus buatan sendiri
Dalam banyak kasus, implementor sebaiknya terlebih dahulu memikirkan kekhawatiran dan kebutuhan interaksi seperti yang diminta oleh konflik merge ala Git, lalu dari titik awal itu membuat asumsi penyederhanaan yang sesuai dengan ranah masalahnya
Melihat source Mastodon https://github.com/mastodon/mastodon/blob/main/app/workers/a..., bahkan tampaknya tidak ada daftar eksplisit “akan digabungkan dari ID mana saja” yang diteruskan oleh pihak yang memulai permintaan penggabungan kepada eksekutor penggabungan asinkron, jadi rasanya hanya soal waktu sampai hal seperti ini terjadi
Ini bukan kritik terhadap Mastodon. Saya sendiri pernah menulis logika penggabungan dengan race condition yang jauh lebih buruk dan juga merasakan dampaknya. Sebenarnya, keberadaan fitur seperti ini dalam proyek sukarela seperti https://opencollective.com/mastodon saja sudah mengejutkan. Tetap saja, ini contoh yang patut diwaspadai
Lebih dalam lagi, realitas itu berantakan, dan NULL tidak bisa dihindari karena database tidak bisa menolak memproses hanya karena realitas berantakan. Misalnya, bayangkan memodelkan sapaan kehormatan, gelar awalan, dan gelar akhiran, lalu ingin membuat salam lengkap dari data itu; setidaknya ada orang yang tidak punya gelar akhiran. Sekalipun Anda tidak menyimpan NULL, Anda akan mendapatkan NULL dari hasil JOIN yang dipakai untuk membuat salam
Nilai NULL tertentu bisa dihilangkan, tetapi fakta bahwa dalam dunia nyata “tidak berlaku” atau “tidak diketahui” sering kali merupakan nilai yang valid tidak bisa dihilangkan, dan database harus menanganinya
Alur yang terasa relatable di sini dimulai dari “kita punya backup database penuh, jadi tinggal restore penuh”, lalu menjadi “restore penuh itu sulit dan punya downtime serta efek samping”, lalu berubah lagi menjadi “mungkin kita bisa secara cerdas memulihkan hanya data yang hilang”, kemudian saat dikerjakan manual muncul error aneh, akhirnya deploy pemulihan selektif sementara, dan terakhir membereskan lima data yang masih hilang. Sambil berharap tidak ada yang keenam terlewat
Setiap kali siapa pun berlatih backup/restore, selalu mengalir seperti ini. Pada akhirnya, data mana yang akan dikembalikan dari image backup selalu menjadi keputusan di level aplikasi
Namun dalam kasus ini saya tidak begitu paham apa masalahnya. Jika semuanya dipulihkan dari backup terakhir yang masih normal, sebagian postingan yang naik setelah itu memang akan hilang dan itu disayangkan, tetapi itu adalah cara yang langsung menyelesaikan masalah alih-alih pekerjaan manual dan ketidakpastian
Bagian yang mengesankan adalah Renaud, Claire, dan Eugen dari tim pengembang Mastodon membantu melebihi ekspektasi
Saya tidak tahu apakah Vivaldi mendukung Mastodon secara finansial, dan saya juga tidak menemukan namanya di halaman sponsor. Kalau belum, semoga kejadian ini mendorong Vivaldi atau perusahaan lain yang memakai Mastodon untuk mempertimbangkan sponsor atau kontrak dukungan
Sponsor terbuka dan benar-benar memberi dampak besar. Sangat penting bagi proyek untuk punya staf penuh waktu, tetapi saat ini di sisi teknis hanya ada 1 developer penuh waktu dan 1 orang DevOps selain pendirinya, Eugen
Ini termasuk salah satu postmortem yang cukup bagus yang saya baca setelah sekian lama
Fakta bahwa poin 2 dan 3 tidak diproses secara atomik terasa seperti masalah. Tentu mungkin ada alasan mengapa itu tidak sepele untuk dilakukan, tapi saya belum melihat kodenya dan suatu saat harus melihatnya
Sepertinya membuatnya atomik ternyata sepele
Sebelumnya memang tidak perlu saja. Tidak atomik pun tidak menjadi masalah, kecuali ada orang yang membuat konfigurasi buruk dengan menghubungkan sidekiq ke server database lama, yaitu replika. Di sini, konfigurasi itu tampaknya menjadi masalah utama
Saat pertama kali harus memulihkan SQL dump yang sangat besar, saya tidak bisa lupa melihat vim benar-benar mengalami segmentation fault ketika mencoba membacanya
Saat itu saya menemukan keajaiban split(1), yaitu memecah file menjadi potongan-potongan. Saya memecah dump besar menjadi satu file per tabel
Tentu saja satu tabel pun bisa sangat besar, tetapi setidaknya file-file menjadi lebih seragam sehingga lebih mudah mengubah query dengan alat lain seperti sed atau awk
Namun, jika sudah sampai pada titik harus mengedit dump untuk memulihkan data, berarti ada sesuatu yang sangat salah dengan prosedur pemulihan. Tentu ketika benar-benar berada dalam situasi itu, pengetahuan seperti itu tidak banyak membantu
Solusinya adalah menulis skrip Python untuk memproses semuanya secara bertahap, lalu memindahkan file ke subdirektori berdasarkan prefiks yang sama
Bagian “Claire meminta stack trace lengkap dari entri log, dan itu juga bisa diekstrak dari log” membuat saya mengernyit
Ini entah sihir voodoo tingkat dalam, atau kode/konfigurasinya membuat Xeon serasa 286. Bukankah ini bisa menjadi megabyte per request?
Itu perilaku bawaan Ruby on Rails. Jika terjadi 500 atau error yang tidak diketahui, ia mencetak stack trace, dan isinya kurang lebih nomor baris serta path file
Saya menjalankan aplikasi Rails dengan desain yang cukup buruk, dan setelah baru saja mengecek, stack trace untuk satu 500 berukuran 5KiB. Error 500 hanya terjadi kira-kira sekali per jam, jadi kurang dari 1MiB per hari
Menyimpan call stack di dekatnya sebenarnya cukup baik dari sisi performa. Perilaku exception bawaan Java juga menyertakan stack trace pada setiap exception, bahkan jika tidak dicetak, tetapi aplikasi Java berjalan baik-baik saja. Bagaimanapun kita harus tahu cara kembali, jadi call stack memang sudah ada, dan informasi tambahan yang diperlukan hanya debug symbol berupa nama file dan nomor baris. Dalam Ruby, karena sifat bahasanya, informasi itu memang tetap diperlukan
Bagaimana mungkin “semua akun lokal di instance Mastodon memiliki field URI bernilai null, sehingga semuanya cocok”?
NULL = NULL dievaluasi sebagai FALSE. SQL memakai logika tiga nilai, lebih tepatnya logika tiga nilai lemah Kleene, dan menerapkan operator apa pun pada NULL menghasilkan NULL
Saya tidak mengerti bagaimana akun-akun dengan nilai NULL di kolom URI bisa cocok dengan query. NULL tidak dibandingkan sebagai sama dengan NULL. Apakah ini sihir Rails yang mengerikan?
Melihat bagian bahwa 6 pengguna dengan simbol di username tidak bisa login, dan itu mudah diperbaiki karena kesalahan skrip pemulihan, rasanya UTF-8 berulah sekali lagi