1 poin oleh GN⁺ 3 jam lalu | 1 komentar | Bagikan ke WhatsApp
  • Serangan rantai pasok menjadi masalah yang makin besar karena biaya distribusi perangkat lunak turun sangat rendah dan otomatisasi build·deploy dipakai secara luas
  • Pada tahun 1970-an ada krisis perangkat lunak karena sulit membuat perangkat lunak yang bisa digunakan kembali, tetapi sekarang repositori paket dan manajer paket mengambil dan membangun kode hanya dengan nama dan versi
  • Pembaruan dependensi otomatis membuat perubahan berbahaya cepat menyebar melalui CI, dan serangan rantai pasok yang baik menyebar secepat runner CI dieksekusi
  • Vendoring, yaitu menaruh semua dependensi bersama di repositori proyek, memang memperbesar repositori, tetapi mencegah perubahan otomatis dan membuat skala serta biaya dependensi lebih terlihat
  • Ini bukan solusi untuk semua perangkat lunak, tetapi banyak perangkat lunak kecil bisa mendapat manfaat dengan mengurangi dependensi yang bisa berubah mendadak dari luar hingga sekitar 2~3 buah

Masalah

  • Serangan rantai pasok menjadi masalah yang makin besar bukan karena hakikat perangkat lunak atau pemeliharaannya berubah, melainkan karena model biaya berbagi dan distribusi perangkat lunak menjadi sangat rendah
  • Biaya distribusi menjadi sangat rendah sehingga banyak otomatisasi dipakai meskipun ada pemborosan, dan otomatisasi itu sendiri memang berguna
  • Setiap beberapa bulan muncul serangan rantai pasok baru yang merusak sebagian besar kode dunia

Bagaimana bisa sampai di sini

  • Pada akhir 1960-an dan awal 1970-an, orang belum benar-benar tahu cara membuat perangkat lunak yang dapat digunakan kembali, dan ini disebut krisis perangkat lunak
  • Permintaan perangkat lunak meningkat secara eksponensial, tetapi kemampuan membuat perangkat lunak baru yang sesuai dengan kompleksitas yang dibutuhkan meningkat lebih lambat
  • Masa itu melahirkan riset seperti modularitas dan structured programming, dan hampir semua sistem modul pada bahasa pemrograman yang dibuat setelah 1990 dapat ditelusuri garis keturunannya sampai Modula-2
  • Pada 1990-an dan 2000-an, internet menghadirkan solusi yang lebih kuat, build dan distribusi perangkat lunak menjadi murah, dan cukup banyak perangkat lunak yang benar-benar ingin dipakai adalah open source
  • Berdasarkan CPAN, CTAN, dan distribusi Linux, lahirlah banyak repositori paket dan manajer paket, dan alat-alat ini mencari, mengambil, dan membangun perangkat lunak hanya dari file manifest, nama, dan biasanya nomor versi yang cenderung arbitrer
  • Dari integrasi manual ke dependensi otomatis

    • Dulu, cara yang baik untuk membuat sistem perangkat lunak kompleks adalah merakit bagian-bagian yang sudah bekerja dengan hati-hati secara manual, dan distribusi Linux pada dasarnya melakukan pekerjaan seperti ini
    • Pada 2003, membangun SDL dengan semua fiturnya bisa memakan waktu berhari-hari dan sangat menyiksa, jadi tidak perlu merindukan masa itu
    • Jika distribusi Linux tersedia sebagai lingkungan dasar yang dikenal, banyak perangkat lunak kustom bisa berjalan di dunianya sendiri dan tidak perlu terlalu memikirkan bagian lain dari sistem
    • Saat harus berkomunikasi dengan perangkat lunak lain, sering kali itu dilakukan lewat file atau network socket yang memakai protokol yang sudah dikenal
    • Kini ada banyak perangkat lunak bagus yang dibangun dari awal dengan Rust atau Go, atau didistribusikan sebagai container Docker, dan perangkat lunak seperti ini hampir tidak berinteraksi dengan pustaka sistem
    • Alih-alih menyesuaikan diri dengan kumpulan perangkat lunak yang disediakan distribusi OS, pendekatan yang banyak dipakai adalah membiarkan sistem build mengambil sendiri pustaka yang dibutuhkan
  • Krisis ke arah sebaliknya

    • Sekarang, kebalikan dari tahun 1970-an terjadi: orang terlalu banyak menggunakan kembali perangkat lunak sehingga program justru menjadi lebih buruk
    • Distribusi perangkat lunak masih sangat murah, tetapi menggunakan perangkat lunak tetap punya biaya
    • Selama ini biaya terbesar adalah kompleksitas untuk membangun perangkat lunak dan membuatnya berjalan di komputer, tetapi masalah itu sebagian besar telah hilang berkat otomatisasi
    • Kini jauh lebih banyak perangkat lunak yang dibangun, didistribusikan, dan dipakai, dan biayanya muncul dalam bentuk dependency hell, pembengkakan, waktu build yang panjang, dan hilangnya paket atau manajer paket
    • Masalah terbesarnya adalah serangan rantai pasok
  • Struktur penyebaran serangan rantai pasok

    • Serangan rantai pasok adalah masalah yang setua open source itu sendiri
    • Di masa lalu, percobaan patch berbahaya yang mencoba memasukkan uid = 0 alih-alih uid == 0 ke kernel Linux adalah patch kernel berbahaya pertama yang terlihat di alam liar, dan itu termasuk upaya serangan rantai pasok
    • Alasan serangan rantai pasok menjadi lebih besar dan lebih bermasalah dalam 10 tahun terakhir adalah karena sistem build telah diotomatisasi untuk mengambil source code dan mendistribusikannya
    • Sistem CI biasanya berjalan untuk setiap perubahan kode atau perubahan besar, dan perubahan seperti ini otomatis tersedia bagi semua pihak yang bergantung pada kode tersebut
    • Sistem CI di sisi yang bergantung juga akan mengambil perubahan itu dan ikut memasukkan kode berbahaya yang baru ditambahkan, dan serangan rantai pasok yang baik menyebar seperti kebakaran hutan secepat runner CI dijalankan
    • Ada cara untuk memperlambat serangan rantai pasok, seperti cooldown dependensi, tetapi itu menimbulkan perdebatan soal kebijakan dan penanggung jawab

Solusi

  • Intinya adalah jangan biarkan sistem build seperti npm dan cargo otomatis mengambil dependensi dari lokasi jaringan setiap kali, melainkan sertakan semua dependensi bersama perangkat lunak
  • Lakukan vendoring untuk semua dependensi dalam proyek, lalu salin isi source control upstream ke repositori git dan commit
  • Jika ada pembaruan upstream, unduh lalu salin lagi; kalau pekerjaan manual ini melelahkan, biarkan alat build mengotomatiskannya
  • Jika sudah ada lockfile, cukup buat lockfile itu terhubung ke seluruh source tree di dalam source control
  • Miliki kendali kuat atas setiap baris source code
  • Biaya dan trade-off

    • Repositori akan membesar, tetapi ruang disk itu murah
    • Biaya transfer tidak semurah disk, tetapi dalam pembahasan ini tetap harus diterima
    • Waktu build tampaknya akan bertambah, tetapi belum tentu benar-benar meningkat karena dependensi itu toh memang sedang dibangun ulang
    • Reuse kode bisa menjadi lebih sulit, dan pada program seperti klien dan server yang memakai pustaka protokol bersama, ini bisa menjadi masalah nyata
    • Program seperti itu memang sudah punya masalah ketidakcocokan versi dan memang harus menanganinya, jadi dalam jangka panjang, memaksa orang lebih memperhatikannya tidak selalu lebih buruk
  • Sekat bakar untuk serangan rantai pasok

    • Jika dependensi tidak diperbarui otomatis, semua paket dalam ekosistem menjadi sekat bakar bagi serangan rantai pasok
    • Pendekatan yang sama juga menghambat penyebaran perbaikan bug dan patch, tetapi kalau perbaikannya penting, orang tetap akan mencarinya secara manual
    • Perbaikan yang tidak dicari manusia biasanya memang tidak terlalu penting
    • Efek serupa juga bisa dicapai dengan membuang konsep semver atau gagasan bahwa “dua kode yang berbeda harus bekerja dengan cara yang sama” dari sistem build, lalu memperlakukan semua nomor versi sebagai hal yang unik dan tidak saling terkait
    • Masalah semver adalah bahwa ia mengekspresikan niat manusia, bukan realitas yang sebenarnya, dan itu pun hanya bekerja jika digunakan dengan cukup benar
    • Memperlakukan nomor versi sebagai sesuatu yang unik tidak menyelesaikan masalah dependensi yang hilang, dirusak, atau isi paket yang mengalami kerusakan dengan cara lain
  • Visibilitas dependensi

    • Dengan melakukan vendoring untuk semua dependensi, selain memperlambat perubahan otomatis, biaya penggunaan dependensi juga sedikit naik
    • Kenaikan biaya itu bukan pada tingkat yang tidak bisa dipulihkan, hanya cukup untuk membuat orang berpikir sedikit lebih banyak saat memakai kode upstream
    • Ini menjadi mekanisme halus yang membuat orang bertanya lagi, “apakah ini benar-benar perlu,” saat menambahkan dependensi baru
    • Visibilitas dependensi meningkat, dan pembengkakan yang bersembunyi di balik dependensi jadi lebih sulit tersembunyi
    • Jika menambahkan pustaka sederhana yang terlihat seperti hanya sekitar 200 baris ternyata berisi 50.000 baris, akan lebih jelas bahwa kita harus berhenti dan bertanya kenapa
    • Sifat dependensi yang terasa seperti sihir berkurang, dan jalur dari bug di codebase kita ke kode orang lain jadi lebih mudah ditelusuri
  • Pohon dependensi dan masalah berbagi

    • Jika semua hal di-vendor secara default, ini bisa mendorong pohon dependensi yang lebih datar dan lebar
    • Tidak diinginkan jika sampai mencapai tingkat pustaka raksasa seperti Boost atau Qt di C++
    • Pustaka raksasa seperti itu ada karena membuat dan memakai pustaka kecil C/C++ terlalu sulit
    • Ada asumsi bahwa lebih baik integrator sistem seperti distribusi Linux melakukannya sekali saja daripada setiap orang harus memahami sendiri cara membangun hal seperti Boost atau Qt
    • Kekurangan nyatanya adalah dependensi transitif tidak dibagikan
    • Jika lib A dan lib B sama-sama bergantung pada Z, deduplikasi bukan tidak mungkin, tetapi menjadi lebih sulit dan membutuhkan kerja manual atau alat yang lebih canggih
    • Bahkan saat dependensi transitif dibagikan pun masalah tetap bisa muncul, dan keberadaan dependensi transitif itu sendiri adalah bagian dari masalah
    • Mengizinkan pustaka menentukan dependensi transitif berarti menyerahkan kendali atas program kepada orang lain

Analisis

  • Tidak semua perangkat lunak bisa memakai pendekatan ini
  • Men-vendor dan membangun seluruh Redis sebagai bagian dari deployment backend web app bukanlah hal yang terasa sangat masuk akal
  • Namun, jika deployment sudah diotomatisasi dengan Ansible atau image Docker, besar kemungkinan pada praktiknya Anda sudah melakukan hal yang mirip
  • Ada batas atas pada kompleksitas yang bisa ditangani pendekatan ini, tetapi perusahaan monorepo raksasa seperti Google dan Facebook menunjukkan bahwa batas itu mungkin lebih tinggi dari yang dibayangkan
  • Pada titik tertentu, dependensi akan bertemu dengan sistem operasi, dan sistem operasi sendiri adalah dependensi besar dengan banyak masalahnya sendiri
  • Ide unikernel untuk backend web memang menarik, tetapi ada masalah alat nyata dan kita belum sampai ke tahap itu
  • Distribusi Linux dan lingkungan build

    • Pendekatan ini bukan cara untuk membuat sistem yang sepenuhnya interaktif seperti distribusi Linux atau BSD
    • Sistem seperti itu memiliki banyak program dan pustaka yang harus bekerja bersama, jadi itu adalah masalah yang berbeda
    • Jika prinsip ini didorong sampai ujung, hasilnya akan mendekati pendekatan seperti Nix atau Guix
    • Konsep bahwa “lingkungan build” harus dirakit dengan benar lebih dekat pada cara malas dan tidak memadai dalam menyelesaikan pertanyaan “bagaimana perangkat lunak harus dibangun”
    • Konsep ini adalah sisa dari masa ketika perangkat lunak dibangun sekali pada semacam minikomputer lalu dibagikan luas sebagai biner
    • Saat ini, jauh lebih banyak perangkat lunak dibangun secara langsung dibandingkan pada tahun 1970-an
  • Cakupan yang bisa diterapkan

    • Pendekatan ini bukan solusi universal, tetapi banyak perangkat lunak dapat menerapkannya dan memperoleh manfaat
    • Sebagian besar perangkat lunak itu kecil, dan proyek besar biasanya memang sudah harus menyelesaikan banyak masalah seperti ini
    • Ada banyak pustaka yang hanya melakukan komputasi murni atau hanya bersentuhan dengan dunia luar lewat I/O dasar dan portabel seperti file dan network socket
    • Contoh seperti pustaka kompresi, libcurl, pustaka TUI, dan Django dapat diperlakukan sebagai target vendoring
    • Dengan vendoring, kita hampir bisa menghindari kasus build atau deployment ke sistem baru yang tiba-tiba rusak tanpa jelas penyebabnya karena konflik versi atau bug yang masuk lewat patch mendadak
    • Tujuannya adalah menurunkan dependensi yang bisa berubah tanpa pemberitahuan dari luar, bukan 200~300 buah melainkan paling banyak sekitar 2~3 buah

Kesimpulan

  • Mengurangi pembaruan dependensi otomatis dan membuat proyek menyimpan source dependensinya sendiri dapat memperlambat penyebaran otomatis serangan rantai pasok
  • Dengan sedikit menaikkan biaya penggunaan dependensi dan meningkatkan visibilitasnya, reuse yang tidak perlu dan pembengkakan tersembunyi jadi lebih mudah ditemukan
  • Pendekatan ini tidak cocok untuk semua sistem, tetapi punya manfaat praktis bagi perangkat lunak kecil dan banyak pustaka

1 komentar

 
GN⁺ 3 jam lalu
Komentar Lobste.rs
  • Menurut saya manajer paket Zig adalah kompromi yang cukup bagus
    Semua paket dipatok dengan hash konten, jadi pada dasarnya seperti selalu punya lockfile, yang menghindari masalah “repositori upstream tiba-tiba menjadi berbahaya”, tetapi masalah “repositori upstream menghilang” tetap ada
    Namun, karena ada cache global/lokal dan berbasis hash konten, kalau repositori upstream hilang, kita cukup melempar tarball dari salinan lokal ke tempat yang dibutuhkan
    Ini terlihat seperti kompromi yang baik antara “mem-vendor source” dan “software yang sederhana serta bisa digunakan ulang”

    • Pendekatan itu mungkin juga bisa diperluas ke semua software dan terdengar cukup keren
      Semua source diletakkan dalam content-addressed store, lalu tiap program cukup di-hash berdasarkan hash dari input-inputnya
    • Secara umum saya setuju, tetapi saya agak penasaran bagaimana susunan seperti itu bisa diserang
      Mungkin harus memodifikasi lockfile atau menemukan hash collision, dan keduanya tidak terlihat mudah
      Hanya saja, karena saya terbiasa dengan ekosistem cargo, rasanya tetap belum sepenuhnya memuaskan. Saat menaikkan dependensi, dependensi transitifnya juga cenderung ikut naik tanpa banyak pemberitahuan, dan hal-hal lain yang masih cocok dengan rentang semantic version juga ikut berubah
  • Kalau disebut “serangan rantai pasok”, menurut saya itu bukan rantai pasok, karena tidak ada kontrak bertanda tangan dengan proposal dan imbalan
    Di sisi lain, dari sudut pandang menjamin dependensi tidak berubah dari bawah, lockfile berisi hash atau metode minimum version selection milik Go pada dasarnya setara dengan vendoring dependensi
    Saya paham bedanya adalah vendoring menambah friksi, tetapi kalau dibawa ke ekstrem, akhirnya orang akan implementasi sendiri atau, lebih buruk lagi, menjadikan dependensi sebagai kode hasil generasi dadakan, jadi menurut saya lebih baik memakai software yang ditulis pakar domain dan sudah cukup teruji
    Saya pernah mengerjakan hal seperti ini di Facebook, dan pengelolaan dependensi pihak ketiga di sana benar-benar tidak ingin saya rekomendasikan ke siapa pun. Untuk dependensi langsung dari crate Rust tertentu, di seluruh fbsource hanya diizinkan paling banyak dua versi sekaligus yang tidak kompatibel secara semantic version. Kalau ingin memperbarui dependensi, Anda harus menanggung beban memperbarui seluruh fbsource
    Mungkin itu cocok untuk Facebook, tetapi saya tidak melihatnya sebagai sesuatu yang sangat bagus atau berkelanjutan

    • Saya penasaran kenapa “maksimal dua”. Apakah untuk migrasi bertahap dari versi lama ke versi baru?
      Saya curiga bagian “tidak terlalu bagus atau berkelanjutan” lebih merupakan fungsi skala daripada kebijakannya sendiri. Kalau banyak versi diizinkan, masalah lain muncul, karena kebanyakan bahasa modern selain TypeScript memakai tipe nominal secara dominan atau sepenuhnya, jadi setiap perubahan yang merusak akan menghalangi pemakaian ulang tipe lintas versi kecuali memakai “semver trick”
      Saat Log4Shell, saya sangat ingat bahwa perusahaan dengan banyak versi yang tersebar di berbagai tempat lebih kesulitan melakukan upgrade dibanding perusahaan yang jumlah versinya sedikit atau sudah dipatok
    • Benar juga, kalau begitu sebut saja serangan dependensi <3
  • Menurut The Third Networking Truth, “with sufficient thrust, pigs fly just fine. However, this is not necessarily a good idea”
    Banyak praktik yang dikutip dari tempat seperti Google/Facebook hanya berhasil karena perusahaan-perusahaan itu bisa mengerahkan daya dorong yang cukup
    Misalnya, saya tahu beberapa tempat seperti itu menugaskan tim yang lebih besar daripada seluruh jumlah karyawan di perusahaan saya hanya untuk mendukung pilihan terkait monorepo dan dependensi. Mereka mampu menanggungnya, tetapi kebanyakan dari kita tidak

  • Pandangan yang bagus. Saya sangat setuju bahwa “kalau semua dependensi di-vendor, biaya memakai dependensi jadi naik”
    Namun, jangan menyalin-tempel libcurl begitu saja. Untuk kebanyakan library itu strategi yang masuk akal, tetapi untuk program C yang menangani input bermusuhan, itu bukan saran yang baik. Kita tidak akan bisa menjaga libcurl tetap aman lebih baik daripada sistem operasi
    Satu hal yang belum pernah saya pikirkan adalah bahwa agak aneh kalau manajer paket untuk pengguna akhir seperti apt muncul lebih dulu, lalu manajer paket level bahasa datang belakangan
    Menurut saya ini benar-benar menimbulkan banyak masalah. Kalau melihat rubygems di awal 2000-an, cukup jelas bahwa mereka mencoba membuat “apt untuk Ruby” dengan default instalasi seluruh sistem, bukan pengelolaan per proyek. Butuh puluhan tahun dan penambahan bundler untuk memulihkan dampak dari kesalahan itu, padahal kalau kebutuhan isolasi proyek diakui sejak awal, bundler tidak akan diperlukan
    Python masih berusaha merapikan kekacauan ini, dan mungkin Perl juga, walaupun saya tidak terlalu tahu detailnya

    • Jadi memang ada batasnya :-) yang sulit hanyalah menentukan garisnya di mana
      Secara historis, manajer paket pada awalnya adalah cara untuk membangun sistem, dan sistem seperti itu punya banyak pengguna, lingkungan desktop, serta banyak software yang harus bekerja bersama
      Membangun software memakan banyak waktu dan memori, dan jumlah software sangat besar dibanding disk dan RAM, jadi penggunaan ulang library itu penting
      Ketika web app mulai naik daun, sebagian besar komputer penting berubah menjadi server yang sepanjang hidupnya hanya menjalankan sedikit program, dan disk serta RAM menjadi cukup murah sehingga ukuran biner kode jadi kurang penting
      Alat untuk membangun sistem tidak cukup mengikuti perubahan zaman, sehingga kebanyakan orang yang membuat software sebenarnya hanya butuh alat untuk membuat satu program dengan baik, bukan sistem raksasa yang saling terhubung dengan banyak shared library
      Sejalan dengan sejarah ini ada juga alur “C tidak punya sistem modul yang layak”, tetapi itu kurang penting di sini
  • Bisa jadi saya salah, tetapi sepertinya ada kelemahan bahwa kalau ada bug pada dependensi yang disalin, scanner tidak bisa mendeteksinya
    Kalau begitu, potensi masalah yang tadinya akan memunculkan notifikasi bisa tetap diam-diam tertinggal

    • Melihat banyaknya false positive yang dihasilkan scanner seperti ini, itu malah bisa jadi keuntungan
      Scanner sangat berguna untuk menunjukkan hal-hal yang mungkin bermasalah, tetapi sangat merepotkan ketika scanner menganggap sesuatu itu masalah padahal sebenarnya tidak, lalu kita mendadak harus menunda pekerjaan yang sudah direncanakan untuk memperbaikinya
  • Kalau sesuai usulan semua dependensi dimasukkan ke dalam software, pengelolaan source upstream disalin ke repositori git lalu di-commit, dan kalau pekerjaan manualnya membosankan dibuat otomatis oleh build tool, bukankah pada akhirnya kita berputar satu lingkaran dan kembali ke situasi memasukkan software pihak ketiga tanpa benar-benar melihatnya?

    • Kalau terus dibaca, katanya efek yang sama bisa dicapai juga dengan membuang semantic version dalam build system, atau membuang gagasan bahwa “dua kode yang berbeda harus berperilaku sama”, lalu memperlakukan semua nomor versi sebagai sesuatu yang unik dan tidak saling terkait
      Tetapi pendekatan itu tidak menyelesaikan masalah dependensi yang menghilang atau dirusak, atau masalah ketika seseorang mengutak-atik isi paket dengan cara lain. Itu lebih mirip optimisasi, dan menurut saya optimisasi prematur. Mungkin suatu hari kita akan sampai ke sana, tetapi jangan menjadikannya titik awal