1 पॉइंट द्वारा GN⁺ 3 시간 전 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • सप्लाई चेन हमले अब बड़ा मुद्दा बन गए हैं क्योंकि सॉफ़्टवेयर वितरित करने की लागत बहुत कम हो गई है और build·deployment automation व्यापक रूप से इस्तेमाल हो रही है
  • 1970 के दशक में software crisis था, जब पुन: उपयोग योग्य सॉफ़्टवेयर बनाना कठिन था, लेकिन आज package repositories और package managers केवल नाम और version के आधार पर code ला कर build कर देते हैं
  • automatic dependency updates की वजह से malicious बदलाव CI के ज़रिए तेज़ी से फैल सकते हैं, और अच्छा सप्लाई चेन हमला CI runner के चलने की रफ़्तार से फैलता है
  • सभी dependencies को project repository में साथ रखने वाला vendoring repository को बड़ा बनाता है, लेकिन automatic बदलावों को रोकता है और dependencies के पैमाने व लागत को अधिक स्पष्ट बनाता है
  • यह हर सॉफ़्टवेयर के लिए समाधान नहीं है, लेकिन बहुत से छोटे सॉफ़्टवेयर बाहरी रूप से अचानक बदल सकने वाली dependencies को 2~3 तक सीमित करने से लाभ उठा सकते हैं

समस्या

  • सप्लाई चेन हमले इसलिए नहीं बढ़े कि सॉफ़्टवेयर या maintenance की प्रकृति ही बदल गई, बल्कि इसलिए कि सॉफ़्टवेयर साझा करने और वितरित करने का cost model बहुत सस्ता हो गया है
  • deployment की लागत इतनी कम हो गई कि कुछ बर्बादी होने पर भी automation का बहुत उपयोग होने लगा, और automation अपने आप में उपयोगी है
  • हर कुछ महीनों में नया सप्लाई चेन हमला सामने आता है, जो दुनिया के code के बड़े हिस्से को प्रभावित कर देता है

हम यहाँ तक कैसे पहुँचे

  • 1960 के दशक के अंत और 1970 के शुरुआती वर्षों में लोग reusable software बनाने का तरीका ठीक से नहीं जानते थे, और इसे software crisis कहा गया
  • सॉफ़्टवेयर की मांग घातीय रूप से बढ़ी, लेकिन आवश्यक जटिलता के अनुरूप नया सॉफ़्टवेयर बनाने की क्षमता उससे धीमी गति से बढ़ी
  • इस दौर ने modularity, structured programming जैसी research को जन्म दिया, और 1990 के बाद बनी लगभग हर programming language module system की वंशरेखा Modula-2 तक पहुँचती है
  • 1990 और 2000 के दशकों में इंटरनेट ने अधिक शक्तिशाली समाधान दिए, software build और distribution सस्ते हुए, और जिन सॉफ़्टवेयरों का लोग वास्तव में उपयोग करना चाहते थे उनमें से काफी open source थे
  • CPAN, CTAN, Linux distributions के आधार पर कई package repositories और package managers बने, और ये tools manifest file, नाम और अक्सर मनमाने version number के आधार पर software को ढूंढते, लाते और build करते हैं
  • मैनुअल integration से automatic dependencies तक

    • पहले जटिल software systems बनाने का अच्छा तरीका यह था कि काम करने वाले हिस्सों को हाथ से सावधानीपूर्वक जोड़ा जाए, और Linux distributions मूल रूप से यही काम करती हैं
    • 2003 में SDL को उसकी सारी functionality के साथ build करना इतना कष्टदायक था कि कई दिन लग जाते थे, और उस दौर को याद करने की ज़रूरत नहीं है
    • अगर Linux distribution एक ज्ञात base environment दे दे, तो बहुत सा custom software अपने ही छोटे संसार में काम कर सकता है और system के दूसरे हिस्सों की बहुत चिंता नहीं करनी पड़ती
    • दूसरे software से संवाद करना हो तो अक्सर files या network sockets के माध्यम से, well-known protocols का उपयोग किया जाता है
    • Rust या Go से शुरुआत से build किए गए, या Docker containers के रूप में deploy किए गए, अच्छे software की संख्या बढ़ी है, और ऐसे software system libraries के साथ लगभग कोई interaction नहीं करते
    • OS distribution द्वारा दिए गए software set के अनुसार ढलने के बजाय, build system द्वारा आवश्यक libraries को सीधे fetch करने का तरीका व्यापक हो गया है
  • उलटी दिशा का संकट

    • अब स्थिति 1970 के दशक के उलट है: लोग सॉफ़्टवेयर का इतना अधिक पुन: उपयोग कर रहे हैं कि programs बदतर हो रहे हैं
    • software distribution अभी भी बहुत सस्ता है, लेकिन software का उपयोग करने की लागत अभी भी बनी हुई है
    • लंबे समय तक सबसे बड़ी लागत software को build करके computer पर चलाने लायक बनाने की जटिलता थी, लेकिन यह समस्या काफी हद तक automation से कम हो गई
    • अब कहीं अधिक software build, deploy और use किया जा रहा है, और इसकी लागत dependency hell, bloat, लंबे build times, package या package manager के गायब हो जाने जैसी समस्याओं में दिखाई देती है
    • सबसे बड़ी समस्या सप्लाई चेन हमले हैं
  • सप्लाई चेन हमलों की फैलने की संरचना

    • सप्लाई चेन हमले open source software जितने पुराने हैं
    • अतीत में Linux kernel में uid == 0 की जगह uid = 0 डालने की malicious patch कोशिश जंगली परिवेश में देखी गई पहली malicious kernel patch थी, और यह सप्लाई चेन हमले के प्रयास में आती है
    • पिछले 10 वर्षों में सप्लाई चेन हमले इसलिए बड़े और गंभीर बने क्योंकि build systems को source code fetch करने और distribute करने के लिए automate कर दिया गया
    • CI systems आम तौर पर हर code change या बड़े बदलाव पर चलते हैं, और ये बदलाव उस code पर निर्भर सभी लोगों तक अपने आप उपलब्ध हो जाते हैं
    • dependency लेने वाले पक्ष के CI systems भी वह बदलाव ले आते हैं और नया शामिल हुआ malicious code भी साथ ले आते हैं, और अच्छा सप्लाई चेन हमला CI runner के चलने की गति से जंगल की आग की तरह फैलता है
    • dependency cooldown जैसी तकनीकें सप्लाई चेन हमलों को धीमा कर सकती हैं, लेकिन उनसे policy और accountability को लेकर बहस पैदा होती है

समाधान

  • मुख्य विचार यह है कि npm, cargo जैसे build systems हर बार network location से dependencies अपने आप fetch न करें, बल्कि सभी dependencies को software के साथ ही रखा जाए
  • project में सभी dependencies को vendor किया जाए, और upstream source control की सामग्री को copy करके git repository में commit किया जाए
  • जब upstream update आए, तो उसे download करके फिर से copy कर दें; और अगर यह मैनुअल काम थकाऊ लगे, तो build tool इसे automate कर सकता है
  • अगर पहले से lockfile है, तो उसे source control के भीतर मौजूद पूरे source tree से जोड़ा जा सकता है
  • इसका मतलब है source code की हर पंक्ति पर मज़बूत नियंत्रण के साथ स्वामित्व रखना
  • लागत और trade-offs

    • repository बड़ी हो जाएगी, लेकिन disk space सस्ता है
    • transfer cost disk से कम सस्ती है, लेकिन इस चर्चा में इसे स्वीकार करने योग्य तत्व माना गया है
    • build time बढ़ता हुआ लग सकता है, लेकिन चूँकि आप वैसे भी उन dependencies को फिर से build कर रहे थे, इसलिए यह ज़रूरी नहीं कि वास्तव में बढ़े
    • code reuse अधिक कठिन हो सकता है, और shared protocol library इस्तेमाल करने वाले programs जैसे client और server में यह वास्तविक समस्या बन सकती है
    • ऐसे programs में पहले से ही version mismatch की समस्या होती है और उन्हें इससे निपटना ही पड़ता है, इसलिए वास्तव में लोगों को सावधान बनाना लंबे समय में और बुरा नहीं है
  • सप्लाई चेन हमलों के लिए firebreak

    • अगर dependencies अपने आप update नहीं होतीं, तो ecosystem का हर package सप्लाई चेन हमले के खिलाफ एक firebreak बन जाता है
    • यही तरीका bug fixes और patches के प्रसार को भी धीमा करता है, लेकिन अगर कोई fix महत्वपूर्ण है तो लोग वैसे भी उसे manually देखेंगे
    • जिन fixes को लोग देखने की ज़रूरत नहीं समझते, वे अक्सर महत्वपूर्ण नहीं होते
    • build system में semver या “दो अलग codebases को एक ही तरह व्यवहार करना चाहिए” जैसी अवधारणा छोड़कर, हर version number को एक-दूसरे से असंबंधित unique इकाई मानने से भी मिलता-जुलता प्रभाव पाया जा सकता है
    • semver की समस्या यह है कि वह वास्तविकता नहीं बल्कि लोगों की मंशा व्यक्त करता है, और वह भी तभी काम करता है जब उसे कुछ हद तक सही तरीके से इस्तेमाल किया जाए
    • version numbers को unique मानने का तरीका dependency के गायब हो जाने, tamper हो जाने या package contents के किसी और तरह से खराब हो जाने की समस्या हल नहीं करता
  • dependency visibility

    • सभी dependencies को vendor करने से automatic बदलाव धीमे होने के अलावा dependency उपयोग की लागत थोड़ी बढ़ जाती है
    • यह लागत-वृद्धि असहनीय नहीं है; बस upstream code इस्तेमाल करते समय थोड़ा अधिक सोचने को मजबूर करती है
    • नई dependency जोड़ते समय यह फिर पूछने का एक हल्का तंत्र बनता है: “क्या यह सच में ज़रूरी है?”
    • dependencies की visibility बढ़ती है, और dependencies के पीछे छिपा bloat कम छिपा रहता है
    • अगर आप कोई साधारण library जोड़ते हैं जो लगभग 200 lines की लगती है, लेकिन वह 50,000 lines निकलती है, तो रुककर यह पूछना और स्पष्ट हो जाता है कि ऐसा क्यों है
    • dependencies का जादुई-सा स्वभाव कम होता है, और codebase में bug का रास्ता दूसरे लोगों के code तक अधिक आसानी से trace किया जा सकता है
  • dependency tree और sharing की समस्या

    • अगर हर चीज़ को default रूप से vendor किया जाए, तो इससे अधिक सपाट और चौड़े dependency trees बन सकते हैं
    • C++ के Boost या Qt जैसी विशाल libraries के स्तर तक जाना वांछनीय नहीं है
    • ऐसी विशाल libraries इसलिए मौजूद हैं क्योंकि छोटी C/C++ libraries बनाना और इस्तेमाल करना बहुत कठिन है
    • धारणा यह है कि Boost या Qt जैसी चीज़ों को खुद build करने का तरीका समझने के बजाय, Linux distribution जैसे system integrator से यह काम एक बार करवा लेना बेहतर है
    • वास्तविक नुकसान यह है कि transitive dependencies shared नहीं होतीं
    • अगर lib A और lib B दोनों Z पर निर्भर हैं, तो deduplication असंभव नहीं है, लेकिन अधिक कठिन हो जाती है, और इसके लिए manual प्रयास या अधिक sophisticated tools चाहिए
    • transitive dependencies shared होने पर भी समस्याएँ पैदा होती हैं, और transitive dependencies का होना ही समस्या का एक हिस्सा है
    • libraries को transitive dependencies निर्दिष्ट करने देना, program पर नियंत्रण दूसरे लोगों को सौंपने जैसा है

विश्लेषण

  • हर सॉफ़्टवेयर इस तरीके का उपयोग नहीं कर सकता
  • web app backend deployment के हिस्से के रूप में पूरे Redis को vendor करके build करना खास तौर पर तर्कसंगत नहीं लगता
  • लेकिन अगर deployment Ansible या Docker image जैसी चीज़ों से automate है, तो संभव है कि आप व्यवहार में पहले से कुछ वैसा ही कर रहे हों
  • इस तरीके द्वारा संभाली जा सकने वाली जटिलता की एक सीमा है, लेकिन Google और Facebook जैसी बड़ी monorepo कंपनियाँ दिखाती हैं कि यह सीमा कल्पना से अधिक ऊँची हो सकती है
  • किसी बिंदु पर dependencies operating system से मिलती हैं, और operating system खुद अपनी समस्याओं वाला एक बड़ा dependency है
  • web backend के लिए unikernel का विचार आकर्षक है, लेकिन असली tooling समस्याएँ हैं और हम अभी उस स्तर तक नहीं पहुँचे हैं
  • Linux distributions और build environment

    • यह तरीका Linux distributions या BSD जैसे पूर्ण परस्पर-क्रियाशील systems बनाने का तरीका नहीं है
    • ऐसे systems एक अलग समस्या हैं, क्योंकि उनमें बहुत सारे programs और libraries को साथ काम करना होता है
    • इस सिद्धांत को अंत तक ले जाएँ तो यह Nix या Guix जैसे तरीकों के करीब पहुँचता है
    • “build environment” को सही तरह assemble करना चाहिए — यह विचार “software को build कैसे करना है” जैसी समस्या का कुछ आलसी और अपर्याप्त समाधान अधिक लगता है
    • यह अवधारणा उस समय की बची हुई विरासत है जब किसी minicomputer पर software एक बार build करके binary रूप में व्यापक रूप से साझा किया जाता था
    • आज 1970 के दशक की तुलना में कहीं अधिक software मौके पर ही build किया जाता है
  • लागू होने की सीमा

    • यह कोई सर्व-उपयोगी समाधान नहीं है, लेकिन बहुत से software इसके लिए उपयुक्त हैं और इससे लाभ पा सकते हैं
    • अधिकतर software छोटे होते हैं, और बड़े projects को पहले से ही इनमें से कई समस्याओं का समाधान करना पड़ता है
    • बहुत-सी libraries केवल pure computation करती हैं या files और network sockets जैसे बुनियादी, portable I/O के माध्यम से ही बाहरी दुनिया से जुड़ती हैं
    • compression libraries, libcurl, TUI libraries, Django जैसे उदाहरण vendoring के उम्मीदवार हो सकते हैं
    • vendoring करने पर नए systems पर deploy या build करते समय version conflict या अचानक आए patch bugs की वजह से बिना कारण समझे टूट जाने की स्थिति से काफी हद तक बचा जा सकता है
    • लक्ष्य यह है कि बाहरी रूप से बिना चेतावनी बदल सकने वाली dependencies को 200~300 के बजाय अधिकतम 2~3 तक सीमित किया जाए

निष्कर्ष

  • dependency auto-update को कम करके और project द्वारा dependency source को सीधे अपने पास रखकर सप्लाई चेन हमलों के automatic प्रसार को धीमा किया जा सकता है
  • dependency उपयोग की लागत थोड़ा बढ़ाने और visibility बढ़ाने से अनावश्यक reuse और छिपे हुए bloat को अधिक आसानी से पहचाना जा सकता है
  • यह तरीका हर system पर फिट नहीं बैठता, लेकिन छोटे software और कई libraries के लिए व्यावहारिक लाभ देता है

1 टिप्पणियां

 
GN⁺ 3 시간 전
Lobste.rs की राय
  • मुझे लगता है Zig package manager काफ़ी अच्छा समझौता है
    हर package content hash पर pin होता है, इसलिए डिफ़ॉल्ट रूप से lock file जैसा असर मिलता है, और “upstream repository अचानक malicious हो जाना” वाली समस्या से बचाव हो जाता है, लेकिन “upstream repository गायब हो जाना” वाली समस्या बनी रहती है
    फिर भी global/local cache दोनों होते हैं और यह content hash आधारित है, इसलिए अगर upstream repository गायब हो जाए तो local copy की tarball को ज़रूरी जगह डालकर काम चल सकता है
    यह “source को vendor करना” और “simple, reusable software” के बीच अच्छा समझौता लगता है

    • उस तरीके को पूरे software पर भी लागू किया जा सकता है, और यह काफ़ी शानदार लगेगा
      सारे source को content-addressed repository में रखा जाए, और हर program को उसके inputs के hash के आधार पर hash किया जाए
    • कुल मिलाकर सहमत हूँ, लेकिन थोड़ा जानना चाहूँगा कि उस setup पर हमला कैसे किया जा सकता है
      शायद lock file को बदलना होगा या hash collision ढूँढनी होगी, और दोनों आसान नहीं लगते
      फिर भी, cargo ecosystem की आदत होने की वजह से यह पूरी तरह पसंद नहीं आता। dependency upgrade करने पर उसकी transitive dependencies भी बिना ज़्यादा बताए साथ में upgrade हो जाती हैं, और semantic version range से मेल खाने वाली दूसरी चीज़ें भी साथ बदल जाती हैं
  • इसे “supply-chain attack” कहना ठीक नहीं लगता, क्योंकि इसमें प्रस्ताव और प्रतिफल वाला signed contract नहीं है, इसलिए यह supply chain नहीं है
    अलग बात यह है कि dependency नीचे से बदल न जाए, इसकी गारंटी देने के नज़रिए से hash वाले lock file या Go की minimal version selection पद्धति, dependency vendoring के बराबर है
    मैं मानता हूँ कि vendoring में friction आती है, लेकिन अगर इसे चरम तक ले जाएँ तो लोग खुद implementation लिखने लगेंगे या उससे भी बुरा, dependency को ad-hoc generated code बना देंगे, इसलिए domain expert द्वारा लिखे और अच्छी तरह जाँचे गए software का इस्तेमाल बेहतर है
    मैंने Facebook में इस तरह का काम किया है, और वहाँ का third-party dependency management मैं किसी को recommend नहीं करूँगा। किसी खास Rust crate की direct dependency के लिए fbsource में semantic version के लिहाज़ से incompatible versions एक साथ ज़्यादा से ज़्यादा दो ही allowed हैं। dependency update करने का मतलब fbsource भर को update करने का बोझ उठाना है
    यह Facebook के लिए ठीक हो सकता है, लेकिन इसे खास तौर पर शानदार या टिकाऊ नहीं कहूँगा

    • जिज्ञासा है, “ज़्यादा से ज़्यादा दो” ही क्यों, समझ नहीं आता। क्या यह पुराने version से नए version तक gradual migration के लिए है?
      मेरा शक है कि “खास तौर पर शानदार या टिकाऊ नहीं” होना policy से ज़्यादा scale का मामला है। कई versions allow करने पर दूसरी समस्याएँ आती हैं, क्योंकि TypeScript को छोड़कर ज़्यादातर आधुनिक भाषाएँ मुख्य रूप से या पूरी तरह nominal types इस्तेमाल करती हैं, इसलिए हर breaking change पर “semver trick” न अपनाएँ तो versions के बीच type reuse रुक जाता है
      Log4Shell के समय मेरी साफ़ याद है कि जिन कंपनियों में versions ज़्यादा थे और वे कई जगह बिखरे हुए थे, उन्हें upgrade में उन कंपनियों से ज़्यादा मुश्किल हुई जिनमें versions कम थे या fixed थे
    • सही है, तो फिर इसे dependency attack कह सकते हैं <3
  • The Third Networking Truth के अनुसार, “काफ़ी thrust हो तो सूअर भी उड़ सकते हैं। लेकिन इसका मतलब यह नहीं कि यह अच्छा विचार है”
    Google/Facebook जैसी जगहों से उद्धृत की जाने वाली बहुत-सी practices सिर्फ़ इसलिए काम करती हैं क्योंकि वे कंपनियाँ काफ़ी thrust लगा सकती हैं
    उदाहरण के लिए, मैं जानता हूँ कि ऐसी कुछ कंपनियाँ monorepo और dependency से जुड़े अपने विकल्पों को support करने के लिए मेरी कंपनी की कुल हेडकाउंट से भी बड़ी team लगा देती हैं। वे इसे वहन कर सकते हैं, लेकिन हममें से ज़्यादातर के लिए यह कठिन है

  • बढ़िया नज़रिया है। “सारी dependencies को vendor करने से dependency इस्तेमाल करने की cost बढ़ जाती है” — इस बात से मैं मज़बूती से सहमत हूँ
    लेकिन libcurl को copy-paste नहीं करना चाहिए। ज़्यादातर libraries के लिए यह ठीक strategy है, लेकिन hostile input सँभालने वाले C programs के लिए यह अच्छी सलाह नहीं है। operating system, libcurl को सुरक्षित रखने का काम आपसे बेहतर कर सकता है
    एक बात जिस पर मैंने कभी ध्यान नहीं दिया था, वह यह है कि apt जैसे end-user package manager पहले आए और language-level package manager बाद में आए — यह कम से कम थोड़ा अजीब है
    मेरा मानना है कि इससे वास्तव में बहुत-सी समस्याएँ पैदा हुईं। 2000 के शुरुआती दशक के rubygems को देखें तो काफ़ी साफ़ है कि वह project-by-project management नहीं, बल्कि system-wide install को default मानकर “Ruby के लिए apt” बनाने की कोशिश कर रहा था। उस गलती के नुकसान को पलटने में bundler जोड़ते-जोड़ते दशकों लग गए, जबकि अगर शुरू से project isolation की ज़रूरत मानी गई होती तो bundler की ज़रूरत ही नहीं पड़ती
    Python अभी भी इस उलझन को सुलझा रहा है, और शायद Perl भी, हालांकि मुझे विस्तार से नहीं पता

    • यानी कुछ सीमाएँ तो हैं ही :-) मुश्किल बस यह तय करना है कि रेखा कहाँ खींची जाए
      ऐतिहासिक रूप से package manager असल में system build करने का तरीका थे, और ऐसे systems में कई users, desktop environments, और साथ काम करने वाले बहुत-से software होते थे
      software build करने में समय और memory बहुत लगती थी, और disk तथा RAM की तुलना में software बहुत ज़्यादा था, इसलिए library reuse महत्वपूर्ण था
      web apps के उभार के साथ ज़्यादातर महत्वपूर्ण computers ऐसे servers बन गए जो अपनी पूरी ज़िंदगी में बस कुछ ही programs चलाते हैं, और disk व RAM इतनी सस्ती हो गईं कि code binary size कम अहम रह गया
      systems बनाने वाले tools समय के इस बदलाव के साथ उतना नहीं चल पाए, इसलिए software बनाने वाले ज़्यादातर लोगों को अब shared libraries वाले विशाल interlinked system नहीं, बल्कि एक single program को अच्छे से बनाने वाले tools चाहिए
      इस इतिहास के साथ-साथ “C में ठीक-ठाक module system नहीं है” वाली धारा भी चलती है, लेकिन यहाँ वह कम महत्वपूर्ण है
  • मैं ग़लत भी हो सकता हूँ, लेकिन लगता है कि copy की गई dependency में bug हो तो scanner उसे detect नहीं कर पाएगा — यह एक downside हो सकता है
    ऐसे में वह संभावित समस्या, जिसकी सामान्यतः warning मिल जाती, चुपचाप बनी रह सकती है

    • इन scanners के false positives की संख्या देखें तो यह उल्टा फ़ायदा भी हो सकता है
      scanners संभावित समस्याएँ दिखाने में बहुत उपयोगी हैं, लेकिन जब scanner किसी चीज़ को समस्या मान ले जबकि वह असल में न हो, और उसे ठीक करने के चक्कर में तय काम अचानक टल जाए, तब यह बहुत सिरदर्द बन जाता है
  • अगर प्रस्ताव के मुताबिक software में सारी dependencies शामिल कर दी जाएँ, upstream source management को git repository में copy करके commit कर दिया जाए, और manual काम से बचने के लिए build tools से इसे automate कर दिया जाए, तो क्या हम पूरा चक्कर लगाकर फिर से third-party software को देखे बिना ही शामिल करने पर नहीं पहुँच जाते?

    • आगे पढ़ने पर कहा गया है कि build system में semantic version या “दो अलग code को एक जैसा व्यवहार करना चाहिए” जैसी धारणा छोड़कर, हर version number को एक-दूसरे से uniquely अलग और unrelated मानकर भी वही असर पाया जा सकता है
      लेकिन वह तरीका dependency के गायब हो जाने या छेड़छाड़ होने की समस्या, या किसी के package contents को किसी और तरीके से बदल देने की समस्या हल नहीं करता। यह optimization के क़रीब है, और मेरी राय में premature optimization है। कभी न कभी उस दिशा में जा सकते हैं, लेकिन शुरुआत वहीं से नहीं करनी चाहिए