npm supply-chain attack থেকে সার্ভার রক্ষার উপায়
npm supply-chain attack কীভাবে আপনার Node অ্যাপ ও VPS সার্ভারে প্রবেশ করে তা জানুন। malicious patch, postinstall scripts ও typosquatting থেকে বাঁচতে সঠিক deploy পদ্ধতি অনুসরণ করুন।
npm supply-chain attack আপনার সার্ভারে কীভাবে কাজ করে
একটি npm supply-chain attack আপনার সার্ভারে পৌঁছায় আপনার ইনস্টল করা কোনো প্যাকেজের মাধ্যমে। এতে কোনো open port বা exploit ধাপের প্রয়োজন হয় না। npm (node package manager) কোড ইনস্টল করে এবং কোড ইনস্টল করার অর্থই হলো সেই কোডটি রান করা। তাই একটি ছোট Node অ্যাপ্লিকেশনের সাথে আপনি এমন শত শত প্যাকেজ ডাউনলোড করেন যা আপনি কখনো দেখেননি, এবং এর যেকোনো একটি প্যাকেজ যেকোনো সময় নতুন ভার্সন প্রকাশ করতে পারে।
আপনার deploy প্রক্রিয়া একটি ক্ষতিকারক ভার্সন ডাউনলোড করে কারণ আপনার install কমান্ডটি সর্বশেষ ভার্সনটি খুঁজে বের করার নির্দেশ দেয়। সেই কোডটি তখন যে ব্যক্তি install কমান্ডটি রান করেছেন, তার প্রিভিলেজ নিয়েই চলতে থাকে। নিচের সবকিছুই এই দুটি বাক্যের ফলাফল।
এই আক্রমণগুলো একজন ব্যক্তি যখন একটি Node অ্যাপ একটি VPS-এ deploy করেন, তখন কত ঘন ঘন ঘটে তার ভিত্তিতে সাজানো হয়েছে। বড় কোম্পানিগুলো যে ক্রম ব্যবহার করবে এটি তা নয়, কারণ বড় কোম্পানিগুলোর নিজস্ব internal registry, review team এবং public registry-র একটি mirror থাকে। আপনার কাছে আছে একটি deploy script।
আকৃতি 1: একজন মেইনটেইনারের অ্যাকাউন্ট হ্যাক হওয়া এবং একটি প্যাচ পাবলিশ করা
npm রেজিস্ট্রি কাউকে বিদ্যমান কোনো ভার্সনের বিষয়বস্তু পরিবর্তন করতে দেয় না। তাই একজন আক্রমণকারী যদি কোনো মেইনটেইনারকে ফিশিংয়ের মাধ্যমে ফাঁদে ফেলে বা পাবলিশ টোকেন চুরি করে, তবে সে 4.18.2 পুনরায় লিখতে পারবে না। এর পরিবর্তে তারা 4.18.3 পাবলিশ করে।
আপনার package.json ফাইলটি দেখুন। "express": "^4.18.2"-এর মতো একটি লাইন মানে এই নয় যে এটি শুধুমাত্র 4.18.2 ভার্সন। এখানে caret চিহ্নটির অর্থ হলো "এই ভার্সন বা এর চেয়ে বড় যেকোনো 4.x ভার্সন", এবং ~4.18.2 মানে হলো "যেকোনো 4.18.x ভার্সন"। npm install কমান্ডটি চালানোর সময় সেই রেঞ্জটি রেজলভ করে, তাই একই গিট কমিট একই বিকেলে দুইবার ডেপ্লয় করলে দুইবার ভিন্ন ভিন্ন কোড সেট ইনস্টল হতে পারে। এই ব্যবধানটিই হলো আক্রমণের ক্ষেত্র। এটি খোলার জন্য আপনার মেশিনের কোনো কিছু হ্যাক হওয়ার প্রয়োজন নেই।
ক্ষতিকারক রিলিজগুলো সাধারণত রিপোর্ট করা হয় এবং সরিয়ে ফেলা হয়, কিন্তু এই অপসারণের আগেই মানুষ সেগুলো ইনস্টল করে ফেলে। যে কেউ সেই সময়ের মধ্যে ডেপ্লয় করলে তার ডিস্কে সেই কোড থেকে যায়। যে পাইপলাইন প্রতিবার চলার সময় রেঞ্জ রেজলভ করে, সেটি কোনো মানুষের সিদ্ধান্ত ছাড়াই সপ্তাহে কয়েকবার স্বয়ংক্রিয়ভাবে সেই ঝুঁকির মুখে পড়ে।
আকৃতি 2: একটি ইনস্টল স্ক্রিপ্ট যে ব্যবহারকারী ডিপ্লয় করছেন তার অনুমতিতে চলে
একটি প্যাকেজের package.json তার scripts ব্লকে preinstall, install, postinstall এবং prepare ঘোষণা করতে পারে। npm ইনস্টলেশনের সময় এগুলো চালায়। এগুলো স্যান্ডবক্স করা থাকে না এবং কেউ এগুলো পর্যালোচনা করে না। এগুলো হলো শেল কমান্ড যা সেই ব্যবহারকারীর অনুমতিতে চলে যিনি ইনস্টল কমান্ডটি টাইপ করেছেন, সেই ব্যবহারকারীর হোম ডিরেক্টরিতে, সেই ব্যবহারকারীর নেটওয়ার্ক অ্যাক্সেস এবং সেই শেলের সম্পূর্ণ এনভায়রনমেন্ট ব্যবহার করে।
তাই কার্যকর প্রশ্নটি এটি নয় যে প্যাকেজটি কী করতে পারে। বরং প্রশ্নটি হলো সেই ব্যবহারকারী কী কী পড়তে পারেন। একটি সাধারণ ডিপ্লয় বক্সে এর উত্তরে অন্তর্ভুক্ত থাকে ~/.npmrc যেখানে একটি রেজিস্ট্রি টোকেন থাকে, ~/.ssh/id_ed25519 যা SSH (secure shell)-এর জন্য ডিপ্লয় কি হিসেবে ব্যবহৃত হয়, ~/.aws/credentials, ~/.docker/config.json এবং শেলের প্রতিটি এক্সপোর্ট করা ভেরিয়েবল, যেখানে সাধারণত DATABASE_URL থাকে।
এই ধরনের পেলোডের কোনো পারসিস্টেন্স বা প্রিভিলেজ এসকেলেশনের প্রয়োজন হয় না। এটি কয়েকটি ফাইল পড়ে, HTTPS-এর মাধ্যমে একটি হোস্টে পাঠায় এবং 0 স্ট্যাটাস নিয়ে এক্সিট করে। আপনি কিছুই দেখতে পাবেন না, কারণ npm ডিফল্টভাবে ইনস্টল-স্ক্রিপ্টের আউটপুট লুকিয়ে রাখে। এটি বন্ধ করুন এবং দেখুন আসলে কী চলছে:
npm ci --foreground-scriptsforeground-scripts npm প্রসেসের সাথে স্ট্যান্ডার্ড ইনপুট, আউটপুট এবং এরর শেয়ার করে, তাই বিল্ড স্ক্রিপ্টগুলো আপনার টার্মিনালে প্রিন্ট হয়, কোনো বাফারে নয় যা ইনস্টল সফল হলে npm মুছে ফেলে।
আকৃতি 3: typosquat এবং যে নামটি আপনি ভুল করে টাইপ করেছেন
একটি typosquat হলো জনপ্রিয় কোনো প্যাকেজের নামের কাছাকাছি নামে প্রকাশিত প্যাকেজ, যা ভুল করে টাইপ করা বা ভুলভাবে পেস্ট করা install কমান্ডের অপেক্ষায় থাকে। এই আক্রমণের মূল কৌশলটি কমান্ডের মধ্যে থাকে, কোডের মধ্যে নয়, তাই lockfile এখানে আপনাকে সাহায্য করতে পারে না: আপনি একবার ভুল নামটি যোগ করলে, lockfile বিশ্বস্ততার সাথে সেটিকে পিন করে রাখে।
যে ভেরিয়েন্টটি ব্যক্তিগত ব্যবহারকারীর চেয়ে টিমকে বেশি বিপদে ফেলে তা হলো dependency confusion। আপনার অভ্যন্তরীণ প্যাকেজের নাম যদি হয় billing-utils এবং এটি একটি private registry-তে থাকে, তবে সমস্যা হতে পারে। যদি public registry-তে billing-utils নামে কিছু না থাকে, তবে যে কেউ সেটি প্রকাশ করতে পারে। npm সাধারণত unscoped নামগুলোকে ডিফল্ট public registry-তে খোঁজে, তাই public কপিটি জিতে যেতে পারে। এর সমাধান হলো আপনার নিজস্ব একটি scope এবং সেই scope-এর জন্য একটি registry mapping, যা .npmrc-এ উল্লেখ করতে হয়:
@yourorg:registry=https://npm.yourorg.example/
//npm.yourorg.example/:_authToken=${NPM_TOKEN}এখন @yourorg/billing-utils শুধুমাত্র সেই host থেকেই আনা হবে, কারণ ডিফল্ট registry-র আগে scope-to-registry ম্যাপিং যাচাই করা হয়। একটি unscoped অভ্যন্তরীণ নামের কোনো ম্যাপিং থাকে না, তাই সেটির কোনো সুরক্ষা নেই।
নতুন কোনো dependency যোগ করার আগে, সেটির ডাউনলোড ব্যাজের দিকে না তাকিয়ে বরং নিচের বিষয়গুলো দেখুন:
npm view some-lib repository.url maintainers time.created time.modifiedগত মাসে তৈরি একটি প্যাকেজ, যা এমন কোনো অ্যাকাউন্ট থেকে প্রকাশিত হয়েছে যার সাথে কোনো public repository-র সংযোগ খুঁজে পাওয়া যাচ্ছে না, সেটি ছয় বছরের ইতিহাস থাকা প্যাকেজের চেয়ে ভিন্ন ধরনের ঝুঁকি তৈরি করে। কোনো তথ্যই চূড়ান্ত প্রমাণ নয়। তবে উভয়ই যাচাই করা সহজ।
আকৃতি 4: যে ডিপেন্ডেন্সির মালিক নীরবে পরিবর্তিত হয়েছে
মেইনটেইনাররা প্যাকেজের দায়িত্ব হস্তান্তর করেন। কেউ ক্লান্ত হয়ে পড়লে একজন অপরিচিত ব্যক্তি সাহায্যের প্রস্তাব দেন, পাবলিশ করার অধিকার হস্তান্তরিত হয়, কিন্তু এর ওপর নির্ভরশীল প্রজেক্টগুলো কোনো ধরনের নোটিফিকেশন পায় না। এতে কোনো কিছু কম্প্রোমাইজ হয় না। 2021 সালে আপনি যে আস্থা রেখেছিলেন, তা এখন অন্য কারো হাতে।
এটি সবচেয়ে ধীরগতির আকৃতি এবং শনাক্ত করা সবচেয়ে কঠিন, কোনো কমান্ড সরাসরি এর উত্তর দেয় না। দুটি বিষয় এটি চিহ্নিত করতে সাহায্য করে। কোনো প্যাকেজ গ্রহণ করার আগে npm view লাইনটি ব্যবহার করে দেখুন কে এটি পাবলিশ করতে পারে। এরপর যখন আপনার নির্ভরশীল কোনো প্যাকেজ পরিবর্তিত হয়, তখন তার diff পড়ে দেখুন:
npm diff --diff-name-only --diff=some-lib@1.4.2 --diff=some-lib@1.4.3
npm diff --diff=some-lib@1.4.2 --diff=some-lib@1.4.3প্রথম ফরম্যাটটি শুধুমাত্র পরিবর্তিত ফাইলের নামগুলো দেখায়, যা আপনার গুরুত্বপূর্ণ প্যাকেজগুলোর প্রতিটি আপগ্রেডের সময় চেক করার জন্য যথেষ্ট দ্রুত। একটি প্যাচ রিলিজ যা কোনো বিল্ড স্ক্রিপ্ট পরিবর্তন করে, প্যাকেজের রুটে নতুন ফাইল যোগ করে, অথবা scripts ব্লকটি এডিট করে, তা আপনার সার্ভারে পৌঁছানোর আগেই সম্পূর্ণ পড়ে দেখা উচিত।
npm ci ব্যবহার করে একটি কমিট করা lockfile থেকে বিল্ড করা
package-lock.json ট্রি-তে থাকা প্রতিটি প্যাকেজের সঠিক সংস্করণ, প্রতিটি প্যাকেজ যে URL থেকে এসেছে, প্রতিটি tarball-এর sha512 ইন্টিগ্রিটি হ্যাশ এবং কোন প্যাকেজটি সেটির ওপর নির্ভরশীল তা রেকর্ড করে। এটি কমিট করুন। এটিই একমাত্র ফাইল যা বলে আপনি আসলে কী পরীক্ষা করেছেন।
এরপর ডেভেলপার ল্যাপটপ নয় এমন যেকোনো মেশিনে npm ci ব্যবহার করে ইনস্টল করুন, কখনোই npm install ব্যবহার করবেন না:
npm ci --omit=dev --ignore-scriptsnpm ci এবং npm install-এর মধ্যে পার্থক্যগুলো এখানে গুরুত্বপূর্ণ। npm ci-এর জন্য একটি lockfile থাকা আবশ্যক। এটি শুরু করার আগেই বিদ্যমান যেকোনো node_modules মুছে ফেলে, যাতে আগের কোনো ডেপ্লয়মেন্টের অবশিষ্ট অংশ বর্তমান ডেপ্লয়মেন্টে থেকে না যায়। এটি কখনোই package.json বা lockfile-এ কিছু লেখে না, তাই ইনস্টল করার সময় এটি নীরবে আপনাকে নতুন সংস্করণে নিয়ে যেতে পারে না। যদি lockfile এবং package.json-এর মধ্যে অমিল থাকে, তবে এটি কোনো পার্থক্য সমাধান না করে ত্রুটি (error) দেখিয়ে বন্ধ হয়ে যায়।
এই ত্রুটিটি একটি ফিচার, কোনো বিরক্তির কারণ নয়। এর অর্থ হলো, কোনো ডিপেন্ডেন্সি পরিবর্তন অবশ্যই এমন একটি কমিট হিসেবে আসতে হবে যা কেউ পর্যালোচনা করেছে, রাত 2টার সময় কোনো ডেপ্লয়মেন্টের পার্শ্বপ্রতিক্রিয়া হিসেবে নয়।
প্রতিবার ফাইল আনার সময় ইন্টিগ্রিটি হ্যাশ যাচাই করা হয়। যে tarball-এর বাইটগুলো রেকর্ড করা হ্যাশের সাথে মেলে না, তা আনপ্যাক না হয়ে code EINTEGRITY ত্রুটির মাধ্যমে ইনস্টলেশন ব্যর্থ করে দেয়। এটি আপনাকে কী সুবিধা দেয় তা স্পষ্টভাবে বুঝুন: এটি প্রমাণ করে যে আপনি যে ফাইলটি পেয়েছেন তা lockfile-এ পিন করা ফাইলটির মতোই, যা checksum দিয়ে ডাউনলোড যাচাইকরণ আপনাকে একই নিশ্চয়তা দেয় এবং এটি একইভাবে সীমাবদ্ধ। পিন করা সংস্করণটি প্রকাশের সময় ক্ষতিকারক ছিল কি না, সে সম্পর্কে এটি কিছুই বলে না।
--omit=dev সম্পর্কে একটি বিষয়: এই প্যাকেজগুলো তখনও সমাধান করা হয় এবং lockfile-এ লেখা হয়। এগুলো কেবল ডিস্কে রাখা হয় না। ডিস্কে কম প্যাকেজ থাকার অর্থ হলো কম ইনস্টল স্ক্রিপ্ট এবং রানটাইমে কম কোড লোড হওয়া, তাই এটি করা লাভজনক। এটি আপনার ট্রি থেকে কোনো ডিপেন্ডেন্সি মুছে ফেলে না।
ইনস্টল স্ক্রিপ্টগুলোকে কোড হিসেবে বিবেচনা করুন এবং সেগুলো প্রত্যাখ্যান করতে শিখুন
আপনি ইনস্টল স্ক্রিপ্টগুলো বন্ধ করে রাখতে পারেন। প্রজেক্টের .npmrc ফাইলে নিচের অংশটি যোগ করুন এবং lockfile-এর পাশাপাশি এটি কমিট করুন:
ignore-scripts=true
save-exact=trueignore-scripts=true ব্যবহারের ফলে npm ডিপেন্ডেন্সিতে ঘোষিত স্ক্রিপ্টগুলো চালানো বন্ধ করে দেয়। save-exact=true ব্যবহারের ফলে npm install some-lib, ^1.4.2-এর পরিবর্তে 1.4.2-কে package.json ফাইলে লিখে রাখে, যাতে কোনো resolving range দুর্ঘটনাক্রমে আপনার ম্যানিফেস্টে প্রবেশ করতে না পারে।
এটি ব্যবহারের ফলে কিছু সমস্যা হতে পারে এবং এটি সক্রিয় করার আগে আপনার জানা উচিত কীভাবে তা সমাধান করতে হয়। যেসব প্যাকেজ native addon কম্পাইল করে বা prebuilt binary ডাউনলোড করে, তারা ইনস্টল স্ক্রিপ্টের মাধ্যমে সেই কাজগুলো সম্পন্ন করে। স্ক্রিপ্ট বন্ধ থাকলে ইনস্টলেশন সফল হয়, কিন্তু পরবর্তীতে রানটাইমে মডিউলটি তার binding file লোড করতে না পারায় ব্যর্থতা দেখা দেয়। এর সমাধান হলো একটি allowlist তৈরি করা:
npm ci --ignore-scripts
npm rebuild better-sqlite3npm rebuild <package> শুধুমাত্র সেই নির্দিষ্ট প্যাকেজের জন্য বিল্ড স্ক্রিপ্টগুলো চালায়। এখন আপনি ঢালাওভাবে কয়েকশ অপরিচিত ব্যক্তিকে এক্সিকিউট পারমিশন না দিয়ে, প্রতিটি প্যাকেজের জন্য আলাদাভাবে সিদ্ধান্ত নিচ্ছেন।
বর্তমানে এই অনুমতির পরিধি কত বড় তা দেখতে npm-কে জিজ্ঞাসা করুন:
npm query ":attr(scripts, [postinstall])"এটি ইনস্টল করা ট্রিতে থাকা প্রতিটি প্যাকেজকে প্রিন্ট করবে যেগুলোতে postinstall স্ক্রিপ্ট রয়েছে। একটি সাধারণ অ্যাপ্লিকেশনের ক্ষেত্রে এই তালিকাটি মানুষের প্রত্যাশার চেয়ে ছোট হয়, যা এই allowlist-কে কার্যকর করে তোলে।
বিল্ড প্রক্রিয়া এবং ট্রাফিক সার্ভ করার প্রক্রিয়া আলাদা করুন
deploy ব্যবহারকারীর node_modules-এ লেখার অনুমতি প্রয়োজন। কিন্তু HTTP অনুরোধের উত্তর দেওয়া প্রসেসটির এই অনুমতির প্রয়োজন নেই। যদি উভয়ই একই অ্যাকাউন্টের অধীনে চলে, তবে ইন্সটলেশনের সময় চলা কোড আপনার ব্যবহারকারীদের জন্য সার্ভ করা কোডটি পরিবর্তন করে ফেলতে পারে, আবার রানটাইমে চলা কোডও তা করতে পারে।
এগুলোকে আলাদা করুন। এক ব্যবহারকারী দিয়ে বিল্ড করুন, অন্য ব্যবহারকারী দিয়ে সার্ভ করুন এবং সার্ভ করা ডিরেক্টরিটিকে সার্ভিং অ্যাকাউন্টের জন্য রিড-অনলি (read-only) করে দিন:
sudo useradd --system --home-dir /srv/nodeapp --shell /usr/sbin/nologin nodeapp
sudo chown -R deploy:nodeapp /srv/nodeapp
sudo chmod -R o-rwx /srv/nodeappএরপর systemd-কে এটি কার্যকর করতে দিন। /etc/systemd/system/nodeapp.service লিখুন:
[Unit]
Description=Node application
After=network-online.target
[Service]
User=nodeapp
Group=nodeapp
WorkingDirectory=/srv/nodeapp/current
EnvironmentFile=/etc/nodeapp/env
ExecStart=/usr/bin/node server.js
Restart=on-failure
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/srv/nodeapp/shared
NoExecPaths=/srv/nodeapp/shared
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
[Install]
WantedBy=multi-user.targetProtectSystem=strict এই সার্ভিসের জন্য পুরো ফাইল সিস্টেমকে রিড-অনলি হিসেবে মাউন্ট করে, শুধুমাত্র /dev, /proc, /sys এবং ReadWritePaths-এ আপনার তালিকাভুক্ত ডিরেক্টরিগুলো ছাড়া। তাই অ্যাপ্লিকেশনটি যদি node_modules-এ কিছু লেখার চেষ্টা করে, তবে তা EROFS: read-only file system এর কারণে ব্যর্থ হবে, যা আপনি এক মিনিটের মধ্যেই আপনার লগে যাচাই করতে পারবেন। NoExecPaths রাইটেবল আপলোড ডিরেক্টরিটিকে কভার করে: সার্ভিসটি সেখানে ফাইল লিখতে পারবে কিন্তু কার্নেল সেগুলোকে এক্সিকিউট করতে দেবে না। এই অপশনটির জন্য systemd 249 বা তার পরবর্তী ভার্সন প্রয়োজন, এবং Ubuntu 24.04-এ 255 ভার্সনটি থাকে।
এই ইউনিট ফাইলে দুটি ফাঁদ রয়েছে। প্রথমত, MemoryDenyWriteExecute=yes যোগ করবেন না। এটি বেশিরভাগ systemd হার্ডেনিং তালিকায় থাকে, কিন্তু এটি Node-কে স্টার্ট হতে বাধা দেয়, কারণ V8 রানটাইমে JavaScript-কে মেশিন কোডে কম্পাইল করে এবং এমন পেজ প্রয়োজন হয় যা একই সাথে রাইটেবল এবং এক্সিকিউটেবল। দ্বিতীয়ত, command -v node থেকে ExecStart পাথটি নিন। যদি Node কোনো ভার্সন ম্যানেজারের মাধ্যমে ইন্সটল করা থাকে, তবে এটি deploy ব্যবহারকারীর হোম ডিরেক্টরিতে থাকে; ProtectHome=yes তখন সার্ভিসটির কাছ থেকে সেই ডিরেক্টরিটিকে লুকিয়ে ফেলে এবং ইউনিটটি তাৎক্ষণিকভাবে status=203/EXEC এর সাথে ব্যর্থ হয় এবং লগে দেখায় যে এক্সিকিউটেবল ফাইলটি খুঁজে পাওয়া যায়নি।
ফাইলটিকে বিশ্বাস না করে ফলাফল যাচাই করুন:
sudo systemctl daemon-reload
sudo systemctl enable --now nodeapp
sudo systemd-analyze security nodeapp.service
sudo -u nodeapp touch /srv/nodeapp/current/probesystemd-analyze security প্রতিটি হার্ডেনিং সেটিং এবং সেগুলোর এক্সপোজার তালিকাভুক্ত করে, যাতে আপনি দেখতে পারেন কোনগুলো ডিফল্ট অবস্থায় আছে। touch কমান্ডটি Permission denied এর সাথে ব্যর্থ হওয়া উচিত, কারণ nodeapp এর current এর অধীনে কোনো কিছুর মালিকানা নেই। যদি এটি সফল হয়, তবে আপনার ফাইলের মালিকানা (ownership) ভুল আছে এবং systemd সেটিংস নীরবে তা ঢেকে রাখছে।
EnvironmentFile সম্পর্কে একটি নোট: systemd এটিকে root হিসেবে পড়ে, এরপর এটি User=nodeapp-এ নেমে আসে, তাই ফাইলটি root:root এবং মোড 600 হতে পারে। অ্যাপ্লিকেশনটি তবুও ভেরিয়েবলগুলো গ্রহণ করবে। nodeapp হিসেবে শেল অ্যাক্সেস আছে এমন যে কেউ /proc/<pid>/environ থেকে সেগুলো পড়তে পারবে, তাই এটি শুধুমাত্র সংরক্ষিত অবস্থায় (at rest) সিক্রেটকে রক্ষা করে, চলমান প্রসেসকে নয়।
ডিপ্লয়মেন্ট ক্রেডেনশিয়াল বিল্ড এনভায়রনমেন্টের বাইরে রাখুন
ইনস্টল স্ক্রিপ্টগুলো এনভায়রনমেন্টের বৈশিষ্ট্য উত্তরাধিকারসূত্রে পায়। এই একটি বিষয়ই নির্ধারণ করা উচিত যে আপনি কোথায় বিল্ড করবেন।
সবচেয়ে নিরাপদ উপায় হলো প্রোডাকশন সার্ভার নয় এমন কোনো জায়গায় বিল্ড করা এবং সেখান থেকে তৈরি ফাইলগুলো কপি করে আনা। বিল্ড মেশিনে তখন শুধুমাত্র একটি read-only registry token থাকবে, অন্য কিছু নয়। কোনো SSH deploy key, cloud access key, database password বা container registry login সেখানে থাকবে না।
npm token create --read-onlyএকটি read-only token শুধুমাত্র প্যাকেজ ডাউনলোড করতে পারে, কিন্তু কোনো কিছু পাবলিশ করতে পারে না। যদি এটি বিল্ড এনভায়রনমেন্ট থেকে চুরিও হয়, তবে ক্ষতির পরিমাণ কেবল পাবলিক প্যাকেজগুলো ডাউনলোড করার ক্ষমতার মধ্যেই সীমাবদ্ধ থাকবে।
যদি আপনাকে সার্ভারেই বিল্ড করতে হয়, তবে deploy ইউজার হিসেবে বিল্ড করুন এবং এনভায়রনমেন্টটি ইচ্ছাকৃতভাবে সীমিত রাখুন। রানটাইম সিক্রেটগুলো /etc/nodeapp/env-এ রাখুন, যা deploy পড়তে পারে না। একই যুক্তি আপনার নিজের হোস্ট করা বিল্ড অটোমেশনের ক্ষেত্রেও প্রযোজ্য: একটি self-hosted GitHub Actions runner টোকেনগুলো ধরে রাখে এবং প্রতিটি কাজের সময় যেকোনো পাবলিশ করা কোড এক্সিকিউট করে, যা এটিকে ছোটখাটো ডিপ্লয়মেন্টের ক্ষেত্রে সবচেয়ে মূল্যবান মেশিনে পরিণত করে। আপনার লেখা নয় এমন যেকোনো প্রোগ্রাম যা আপনার পুরো এনভায়রনমেন্ট পায়, সেটিও একই ক্যাটাগরিতে পড়ে। আর এই কারণেই AI এজেন্টের এনভায়রনমেন্ট থেকে সিক্রেট দূরে রাখা বিষয়টি মূলত একই সমস্যার ভিন্ন একটি রূপ, যেখানে মাঝে অন্য একটি প্রোগ্রাম কাজ করছে।
যা অডিট করতে পারেন না তা পিন করুন অথবা ভেন্ডর করুন
একটি পিন করা ডিপেন্ডেন্সি হলো এমন একটি ডিপেন্ডেন্সি যার ভার্সন কোনো কমিট ছাড়া পরিবর্তন করা সম্ভব নয়। কমিট করা lockfile ইতিমধ্যে পুরো ট্রি-এর জন্য এই কাজটি করে। তবে দুটি ক্ষেত্রে আরও সতর্কতার প্রয়োজন।
প্রথমটি হলো ট্রানজিটিভ ডিপেন্ডেন্সি। আপনার ডিপেন্ডেন্সিগুলো কিসের ওপর নির্ভর করছে তা আপনার নিয়ন্ত্রণে নেই। overrides-এর মাধ্যমে package.json-এ ট্রি-এর যেকোনো জায়গার ভার্সন জোরপূর্বক নির্ধারণ করা যায়:
{
"overrides": {
"some-transitive-lib": "1.4.2"
}
}এটি যোগ করার পর একবার npm install চালান যাতে lockfile ফলাফলটি রেকর্ড করতে পারে, তারপর উভয় ফাইল কমিট করুন।
দ্বিতীয় ক্ষেত্রটি হলো এমন একটি প্যাকেজ যা আপনি অডিট করতে পারেন না এবং বাদও দিতে পারেন না। সেটিকে ভেন্ডর করুন। npm pack কমান্ডটি ঠিক সেই tarball-টি ডাউনলোড করে যা রেজিস্ট্রি থেকে পাওয়া যেত, এবং একটি file: ডিপেন্ডেন্সি আপনার কপি থেকে সেটি ইনস্টল করে:
npm pack some-lib@1.4.2
mkdir -p vendor && mv some-lib-1.4.2.tgz vendor/{
"dependencies": {
"some-lib": "file:vendor/some-lib-1.4.2.tgz"
}
}tarball-টি এখন আপনার রিপোজিটরিতে সংরক্ষিত এবং এটি আপনার অজান্তে পরিবর্তিত হতে পারবে না। এর ফলে এর আপডেটগুলোর দায়িত্ব চিরতরে আপনার ওপর বর্তাবে, তাই এটি শুধুমাত্র ছোট পরিত্যক্ত প্যাকেজের ক্ষেত্রে ব্যবহার করুন, আপনার ওয়েব ফ্রেমওয়ার্কের জন্য নয়।
এছাড়া একটি কুলিং-অফ পিরিয়ড বা অপেক্ষার সময়ও রয়েছে, যার জন্য কোনো খরচ নেই:
npm install --before=2026-08-01before অপশনটি শুধুমাত্র সেই ভার্সনগুলো ব্যবহার করে ট্রি পুনর্নির্মাণ করে যা সেই তারিখ বা তার আগে প্রকাশিত হয়েছিল। ডিপেন্ডেন্সি রিফ্রেশ করার সময় এটিকে এক বা দুই সপ্তাহ পিছিয়ে দিন; এতে আপনি সেই সময়টুকু এড়িয়ে যেতে পারবেন যখন কোনো ত্রুটিপূর্ণ রিলিজ লাইভ থাকে কিন্তু এখনো রিপোর্ট করা হয়নি। এটি একটি স্থূল পদ্ধতি, কারণ এটি গুরুত্বপূর্ণ সিকিউরিটি ফিক্সগুলোকেও আটকে রাখে। রেঞ্জগুলো সমাধান করতে এটি ব্যবহার করুন, কী পরিবর্তন হয়েছে তা পড়ুন, তারপর lockfile কমিট করুন।
আমি কীভাবে নিশ্চিত হব যে কোন ভার্সনটি আসলে রিলিজ করা হয়েছে?
git-এর lockfile-এ কী ইনস্টল হওয়ার কথা ছিল তা লেখা থাকে। কিন্তু ডিস্কে কী আছে তা-ই আসল প্রমাণ। শুধুমাত্র দ্বিতীয়টিই নির্ভরযোগ্য।
npm ls some-lib
node -e "console.log(require('./node_modules/some-lib/package.json').version)"npm ls কমান্ডটি node_modules ফাইলটি পড়ে, তাই এটি lockfile-এর উদ্দেশ্যের পরিবর্তে বর্তমানে যা আছে তা রিপোর্ট করে। node -e লাইনটি পাথ অনুযায়ী ইনস্টল করা ম্যানিফেস্ট পড়ে, যা এমন প্যাকেজের ক্ষেত্রেও কাজ করে যাদের exports ফিল্ড subpath import ব্লক করে রাখে। এটি কোনো ট্রি ড্রয়িং ছাড়াই শুধুমাত্র একটি ভার্সন প্রিন্ট করে।
তুলনার অন্য অংশের জন্য git চেক করুন:
git log --oneline -- package-lock.json
git show <commit>:package-lock.json | grep -A3 '"node_modules/some-lib"'deploy লেআউটে কমিটটি যুক্ত করে এই দুইয়ের মধ্যে স্থায়ী সংযোগ তৈরি করুন। /srv/nodeapp/releases/<short commit sha> ডিরেক্টরিতে রিলিজ করুন এবং একটি symlink দিয়ে /srv/nodeapp/current কে সেটির দিকে নির্দেশ করুন। "বর্তমানে কী চলছে" তার উত্তর হবে readlink /srv/nodeapp/current, এবং এটি ভোর 03:00 টায় এমন কারো কাছেও উপলব্ধ থাকবে যিনি এটি deploy করেননি।
পরিশেষে, registry কী নিশ্চয়তা দিচ্ছে তা যাচাই করুন:
npm audit signaturesএটি আপনার ইনস্টল করা ট্রিতে থাকা প্যাকেজগুলোর registry স্বাক্ষর যাচাই করে এবং যেসব প্যাকেজের provenance attestation আছে তা পরীক্ষা করে। Provenance একটি প্রকাশিত tarball-কে সেটির নির্মাতা পাবলিক continuous integration (CI) বিল্ডের সাথে যুক্ত করে। তাই একটি যাচাইকৃত attestation মানে হলো, আপনি কোডটিকে কোনো অজানা ল্যাপটপের পরিবর্তে একটি নির্দিষ্ট কমিট পর্যন্ত ট্রেস করতে পারবেন। সব প্যাকেজের ক্ষেত্রে এটি প্রযোজ্য নয়, তাই কোনো attestation না থাকলে সেটিকে "তথ্য নেই" হিসেবে গণ্য করুন, "খারাপ প্যাকেজ" হিসেবে নয়।
একটি ত্রুটিপূর্ণ রিলিজ আপনার সার্ভারে পৌঁছে গেলে করণীয়
কী রান করেছে এবং কোন ব্যবহারকারীর অধীনে রান করেছে, তা থেকে কাজ শুরু করুন।
যদি ইনস্টল করার সময় কোডটি রান করে থাকে, তবে ধরে নিন বিল্ড ব্যবহারকারীর পাঠযোগ্য সবকিছুই বেহাত হয়েছে। রেজিস্ট্রি টোকেন, সেই হোম ডিরেক্টরির SSH keys, ক্লাউড ক্রেডেনশিয়াল এবং সেই শেলে এক্সপোর্ট করা যেকোনো সিক্রেট পরিবর্তন (rotate) করুন। রোটেশনই একমাত্র সঠিক পদক্ষেপ, কারণ কোনো ফাইল পড়া হয়নি তা আপনি প্রমাণ করতে পারবেন না।
যদি কোডটি রানটাইমে একটি লক-ডাউন সার্ভিস অ্যাকাউন্টের অধীনে রান করে, তবে আক্রান্ত হওয়ার পরিধি অনেক ছোট: শুধুমাত্র অ্যাপ্লিকেশনের নিজস্ব এনভায়রনমেন্ট ভেরিয়েবল এবং এর নেটওয়ার্ক অ্যাক্সেস যতটুকু পৌঁছাতে পারে। এটিই একটি VPS-এ আনপ্রিভিলেজড ব্যবহারকারী হিসেবে সার্ভিস চালানোর মূল যুক্তি। এটি সার্ভার আক্রান্ত হওয়া প্রতিরোধ করে না। এটি নির্ধারণ করে যে আক্রান্ত হওয়ার ফলে মেশিনের কতটা অংশ ক্ষতিগ্রস্ত হবে এবং রিস্টার্টের পরেও তা টিকে থাকবে কি না।
এরপর, ক্লিন করার পরিবর্তে নতুন করে বিল্ড করুন। node_modules ডিলিট করুন, package.json-এ আক্রান্ত প্যাকেজটিকে ত্রুটিপূর্ণ ভার্সনের নিচে পিন করুন, লকফাইল আপডেট করতে একবার npm install রান করুন, সেটি কমিট করুন এবং npm ci দিয়ে ডিপ্লয় করুন। বিদ্যমান ডিরেক্টরি বা ট্রি মেরামত করার চেষ্টা করবেন না। একটি ইনস্টল স্ক্রিপ্ট কোথায় কোথায় পরিবর্তন করেছে, তা আপনি পুরোপুরি শনাক্ত করতে পারবেন না।
সময়সীমাটিও লিখে রাখুন: প্রথম যে ডিপ্লয়টি ভার্সনটি টেনে থাকতে পারে এবং যে ডিপ্লয়টি সেটি সরিয়ে ফেলেছে। এই পরিসীমাটি আপনাকে বলে দেবে আপনার কোন লগগুলো পড়তে হবে, এবং এটি তখনই সম্ভব যদি আপনার রিলিজগুলো কমিটের নামানুসারে নামকরণ করা হয়।
যা এই পদ্ধতিগুলোর কোনোটিই সমাধান করে না
একটি lockfile কোনো dependency-কে নিরাপদ করে তোলে না। এটি আপনার dependency গ্রহণ করার মুহূর্তটিকে একটি অনিচ্ছাকৃত deploy-এর পার্শ্বপ্রতিক্রিয়া থেকে সরিয়ে একটি তারিখযুক্ত এবং পর্যালোচিত সিদ্ধান্তে রূপান্তর করে। উপরের প্রতিটি অনুশীলন একই রূপান্তর ঘটায়—দুর্ঘটনা থেকে পছন্দে।
npm audit এখানে কোনো প্রতিরক্ষা নয়। এটি আপনার dependency tree-কে রিপোর্ট করা vulnerability-র একটি ডেটাবেসের সাথে তুলনা করে, তাই এটি এমন সমস্যাগুলো খুঁজে পায় যা ইতিমধ্যে প্রকাশিত এবং চিহ্নিত হয়েছে। একটি supply-chain attack তার পুরো কার্যকর জীবনকাল জুড়ে নামহীন থাকে। পুরনো পরিচিত বাগগুলোর জন্য npm audit চালান, কিন্তু চার ঘণ্টা আগে রিলিজ হওয়া কোনো প্যাকেজের ক্ষেত্রে এর কাছ থেকে কোনো সহায়তা আশা করবেন না।
আপনার dependency-র সংখ্যা কমানো এই গাইডের যেকোনো টুলের চেয়ে বেশি কার্যকর, এবং এটিই সবচেয়ে কম জনপ্রিয় পরামর্শ। আপনি যে প্যাকেজটি যোগ করছেন না, সেই প্যাকেজের প্রকাশক আপনার হয়ে ফিশিংয়ের শিকার হতে পারবে না এবং সেই প্যাকেজের install script আপনার deploy user হিসেবে কখনোই চলবে না।
এর কোনোটিই শুধুমাত্র npm-এর জন্য নির্দিষ্ট নয়। একই চারটি বিষয় PyPI, RubyGems, container images এবং আপনার ডিস্ট্রিবিউশনের package manager-এর ক্ষেত্রেও প্রযোজ্য। npm-এ এটি সবচেয়ে বেশি দৃশ্যমান কারণ এখানে dependency tree সবচেয়ে গভীর এবং install script ডিফল্টভাবে চলে। আপনার চারপাশের মেশিনের কতটা অংশ আপনি রক্ষা করবেন তা নির্ভর করে এটি কোথায় চলছে তার ওপর, যা VPS হোস্টিং নিরাপদ কি না, সেই বৃহত্তর প্রশ্নের একটি অংশ।
FAQ
npm ci কি আমাকে কোনো compromised npm package থেকে সুরক্ষা দেয়?
এটি আপনাকে আপনার অজান্তে ভার্সন পরিবর্তন হওয়া থেকে সুরক্ষা দেয়। npm ci ঠিক সেই প্যাকেজটিই ইনস্টল করে যা package-lock.json-এ রেকর্ড করা আছে। এটি প্রতিটি tarball-কে তার sha512 integrity hash-এর সাথে মিলিয়ে দেখে। যদি package.json এবং lockfile-এর তথ্যের মধ্যে অমিল থাকে, তবে এটি কোনো কিছু সমাধান করার চেষ্টা না করে সরাসরি error প্রদর্শন করে বন্ধ হয়ে যায়। তবে, pinned ভার্সনটি নিরাপদ কি না, সে বিষয়ে এটি কোনো নিশ্চয়তা দেয় না। আপনি যদি এমন একটি lockfile commit করেন যাতে কোনো ক্ষতিকারক ভার্সন pin করা আছে, তবে npm ci প্রতিবার আপনার প্রতিটি সার্ভারে বিশ্বস্ততার সাথে সেই ক্ষতিকারক ভার্সনটিই ইনস্টল করবে।
আমার কি সবকিছুর জন্য ignore-scripts=true সেট করা উচিত?
প্রথমে এটি সেট করুন, তারপর allowlist ব্যবহার করুন। প্রজেক্টের .npmrc-এ ignore-scripts=true ব্যবহার করলে dependency install script-গুলো চলা বন্ধ হয়ে যায়। এটি ক্ষতিকারক প্যাকেজ থেকে আপনার deploy user-এর credentials চুরি হওয়ার সবচেয়ে সহজ পথটি বন্ধ করে দেয়। যেসব প্যাকেজ native addon compile করে বা prebuilt binary সংগ্রহ করে, তাদের ক্ষেত্রে script-এর প্রয়োজন হয়। script বন্ধ থাকলে সেগুলো ইনস্টল হওয়ার সময় নয়, বরং runtime-এ missing binding file error দেখাবে। npm ci --ignore-scripts চালান এবং যেসব প্যাকেজকে আপনি বিশ্বাস করেন, সেগুলোর জন্য npm rebuild <package> ব্যবহার করুন। npm query ":attr(scripts, [postinstall])" দেখালে বুঝতে পারবেন আসলে কতগুলো প্যাকেজ অনুমোদিত হয়েছে।
আমার সার্ভারে আসলে কোন ভার্সনের প্যাকেজ ইনস্টল হয়েছে তা কীভাবে জানব?
lockfile না দেখে সরাসরি ডিস্ক চেক করুন। npm ls <package> রিপোর্ট করে যে node_modules-এ বর্তমানে কী আছে, এবং node -e "console.log(require('./node_modules/<package>/package.json').version)" শুধুমাত্র ভার্সন স্ট্রিংটি প্রিন্ট করে। git-এর lockfile একটি ভিন্ন প্রশ্নের উত্তর দেয়, যা হলো কী ইনস্টল হওয়ার কথা ছিল। এই দুটির তুলনা করাই মূল উদ্দেশ্য। git commit-এর নামে নামকরণ করা ডিরেক্টরিতে deploy করলে কয়েক মাস পরেও আপনি উভয় তথ্যের উৎস খুঁজে পাবেন।
npm audit কি supply-chain আক্রমণ শনাক্ত করতে পারে?
না। npm audit আপনার dependency tree-কে রিপোর্ট করা vulnerabilities-এর একটি ডেটাবেসের সাথে মিলিয়ে দেখে। তাই এটি শুধুমাত্র সেই সমস্যাগুলোই খুঁজে পায় যেগুলো ইতিমধ্যে প্রকাশিত হয়েছে এবং একটি identifier পেয়েছে। একটি ক্ষতিকারক release যখন প্রথম ছাড়া হয়, তখন সেটির কোনো রিপোর্ট থাকে না, অথচ সেই সময়টিতেই ইনস্টল করাটা ঝুঁকিপূর্ণ। npm audit signatures কমান্ডটি এক্ষেত্রে বেশি কার্যকর: এটি আপনার ইনস্টল করা tree-তে registry signature যাচাই করে এবং provenance attestation পরীক্ষা করে। এটি আপনাকে নিশ্চিত করে যে tarball-টি কোনো অজানা মেশিন থেকে নয়, বরং একটি পাবলিক build থেকে এসেছে।
ইনস্টল করার সময় আক্রমণ হলে unprivileged user হিসেবে অ্যাপ চালানো কেন গুরুত্বপূর্ণ?
কারণ এই দুটি ব্যর্থতার প্রভাব ভিন্ন এবং আপনি উভয়টির বিরুদ্ধেই সুরক্ষা ব্যবস্থা নিচ্ছেন। install-time কোড deploy user হিসেবে চলে এবং সেই ব্যবহারকারীর SSH keys, registry tokens এবং cloud credentials পড়তে পারে। runtime কোড service account হিসেবে চলে। User=nodeapp, ProtectSystem=strict এবং ডিস্কে কোনো credentials না থাকলে, এর প্রভাব শুধুমাত্র অ্যাপের নিজস্ব environment এবং ডেটাবেসের মধ্যেই সীমাবদ্ধ থাকে। অ্যাকাউন্টগুলো আলাদা রাখার ফলে traffic সার্ভ করা প্রসেসটি node_modules পরিবর্তন করতে পারে না। ফলে runtime compromise হলে পরবর্তী রিস্টার্টেই তা মুছে যায় এবং স্থায়ী কোনো ক্ষতি করতে পারে না।