SSD Nodes Learn Hosting plans →
নির্দেশিকা Matt Connorদ্বারা Matt Connor · আপডেট করা হয়েছে 2026-08-27

npm supply-chain attack থেকে সার্ভার সুরক্ষিত রাখার উপায়

npm supply-chain attack কীভাবে আপনার Node অ্যাপ্লিকেশনে প্রবেশ করে তা জানুন। malicious patch, postinstall scripts এবং typosquats থেকে সার্ভার বাঁচাতে সঠিক deploy পদ্ধতি অনুসরণ করুন।

npm supply-chain attack কী এবং তা আপনার সার্ভারে কীভাবে কাজ করে

একটি npm supply-chain attack আপনার সার্ভারে পৌঁছায় এমন একটি প্যাকেজের মাধ্যমে যা আপনি ইনস্টল করার সিদ্ধান্ত নিয়েছেন। এতে কোনো open port বা exploit ধাপের প্রয়োজন হয় না। npm (node package manager) কোড ইনস্টল করে এবং কোড ইনস্টল করার অর্থই হলো সেই কোডটি রান করা। তাই একটি ছোট Node অ্যাপ্লিকেশন এমন কয়েকশ প্যাকেজ টেনে আনে যা আপনি কখনোই পড়েননি, এবং এর যেকোনো একটি প্যাকেজ যেকোনো সময় নতুন ভার্সন প্রকাশ করতে পারে।

আপনার deploy প্রক্রিয়া একটি ক্ষতিকারক ভার্সন ডাউনলোড করে কারণ আপনার install কমান্ডটি নতুন ভার্সনটি খুঁজে বের করার নির্দেশ দেয়। এরপর সেই কোডটি যার প্রিভিলেজ বা অনুমতিতে ইনস্টল করা হয়েছে, সেই একই প্রিভিলেজে রান করে। নিচের সবকিছুই এই দুটি বাক্যের ফলাফল।

এই আক্রমণের ধরনগুলোকে সাজানো হয়েছে সেই অনুযায়ী, যেভাবে একজন ব্যক্তি একটি Node অ্যাপ একটি VPS-এ deploy করার সময় আক্রান্ত হন। এই ক্রমটি কোনো বড় কোম্পানির জন্য প্রযোজ্য নয়, কারণ বড় কোম্পানির নিজস্ব registry, review team এবং public registry-এর একটি mirror থাকে। আপনার ক্ষেত্রে আছে একটি deploy script।

আকৃতি 1: একজন মেইনটেইনারের অ্যাকাউন্ট হ্যাক হওয়া এবং একটি প্যাচ পাবলিশ করা

npm registry বিদ্যমান কোনো ভার্সনের বিষয়বস্তু পরিবর্তন করতে দেয় না। তাই কোনো আক্রমণকারী যদি ফিশিংয়ের মাধ্যমে মেইনটেইনারের তথ্য হাতিয়ে নেয় বা পাবলিশ টোকেন চুরি করে, তবে তারা 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 কমান্ডটি রান করার সময় সেই রেঞ্জটি নির্ধারণ করে, তাই একই দিনে দুইবার একই git commit ডেপ্লয় করলেও দুইবার ভিন্ন ভিন্ন কোড সেট ইনস্টল হতে পারে। এই ব্যবধানটিই হলো আক্রমণের সুযোগ বা attack surface। এটি ঘটার জন্য আপনার মেশিনের কোনো কিছু হ্যাক হওয়ার প্রয়োজন নেই।

ক্ষতিকারক রিলিজগুলো সাধারণত রিপোর্ট করা হয় এবং সরিয়ে ফেলা হয়, কিন্তু এই অপসারণের আগেই মানুষ সেগুলো ইনস্টল করে ফেলে। যে কেউ যদি সেই সময়ের মধ্যে ডেপ্লয় করে থাকে, তবে তাদের ডিস্কে সেই কোড থেকে যায়। যে পাইপলাইন প্রতিবার রান করার সময় রেঞ্জ নির্ধারণ করে, সেটি কোনো মানুষের সিদ্ধান্ত ছাড়াই সপ্তাহে কয়েকবার স্বয়ংক্রিয়ভাবে সেই ঝুঁকির মুখে পড়ে।

আকৃতি 2: একটি ইনস্টল স্ক্রিপ্ট যে ব্যবহারকারী ডিপ্লয় করছেন তার পরিচয়ে চলে

একটি প্যাকেজের package.json তার scripts ব্লকের মধ্যে preinstall, install, postinstall এবং prepare ঘোষণা করতে পারে। npm ইনস্টলেশনের সময় এগুলো চালায়। এগুলো কোনো স্যান্ডবক্সের মধ্যে থাকে না এবং কেউ এগুলো পর্যালোচনাও করে না। এগুলো শেল কমান্ড হিসেবে চলে, যা সেই ব্যবহারকারীর পরিচয়ে চলে যিনি ইনস্টল কমান্ডটি দিয়েছেন। এটি সেই ব্যবহারকারীর হোম ডিরেক্টরিতে, তার নেটওয়ার্ক অ্যাক্সেস এবং সেই শেলের সম্পূর্ণ এনভায়রনমেন্ট ব্যবহার করে কার্যকর হয়।

তাই কার্যকর প্রশ্নটি এটি নয় যে প্যাকেজটি কী করতে পারে। বরং প্রশ্নটি হলো সেই ব্যবহারকারী কী কী পড়তে পারেন। একটি সাধারণ ডিপ্লয় বক্সে এর উত্তরে থাকে ~/.npmrc, যেখানে একটি রেজিস্ট্রি টোকেন থাকে; SSH (secure shell)-এর জন্য ব্যবহৃত ডিপ্লয় কি ~/.ssh/id_ed25519; ~/.aws/credentials; ~/.docker/config.json; এবং শেলের প্রতিটি এক্সপোর্ট করা ভেরিয়েবল, যেখানে সাধারণত DATABASE_URL থাকে।

এই ধরনের পেলোডের কোনো পারসিস্টেন্স বা প্রিভিলেজ এসকেলেশনের প্রয়োজন হয় না। এটি কয়েকটি ফাইল পড়ে, HTTPS-এর মাধ্যমে কোনো হোস্টের কাছে পাঠিয়ে দেয় এবং 0 স্ট্যাটাস নিয়ে এক্সিট করে। আপনি কিছুই দেখতে পাবেন না, কারণ npm ডিফল্টভাবে ইনস্টল-স্ক্রিপ্টের আউটপুট লুকিয়ে রাখে। এটি বন্ধ করুন এবং দেখুন আসলে কী চলছে:

npm ci --foreground-scripts

foreground-scripts, npm প্রসেসের সাথে স্ট্যান্ডার্ড ইনপুট, আউটপুট এবং এরর শেয়ার করে। ফলে বিল্ড স্ক্রিপ্টগুলো আপনার টার্মিনালে প্রিন্ট হয়, কোনো বাফারে নয় যা npm ইনস্টল সফল হলে মুছে ফেলে।

আকৃতি 3: typosquats এবং যে নামটি আপনি ভুল করে টাইপ করেছেন

একটি 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 শুধুমাত্র সেই নির্দিষ্ট হোস্ট থেকেই আনা হবে, কারণ ডিফল্ট registry-র আগে scope-to-registry ম্যাপিং যাচাই করা হয়। একটি unscoped অভ্যন্তরীণ নামের কোনো ম্যাপিং থাকে না, তাই সেটির কোনো সুরক্ষা নেই।

নতুন কোনো dependency যোগ করার আগে, সেটির ডাউনলোড ব্যাজ দেখার চেয়ে প্যাকেজটি নিজে যাচাই করুন:

npm view some-lib repository.url maintainers time.created time.modified

গত মাসে তৈরি একটি প্যাকেজ, যা এমন কোনো অ্যাকাউন্ট থেকে পাবলিশ করা হয়েছে যার সাথে কোনো public repository-র সংযোগ খুঁজে পাওয়া যাচ্ছে না, সেটি ছয় বছরের ইতিহাস থাকা প্যাকেজের চেয়ে ভিন্ন ধরনের ঝুঁকি তৈরি করে। কোনো তথ্যই চূড়ান্ত প্রমাণ নয়। তবে উভয়ই যাচাই করা সহজ।

আকৃতি 4: যে ডিপেন্ডেন্সির মালিক নীরবে পরিবর্তিত হয়েছে

মেইনটেইনাররা প্যাকেজের দায়িত্ব হস্তান্তর করেন। কেউ ক্লান্ত হয়ে পড়লে অন্য কেউ সাহায্যের প্রস্তাব দেন, পাবলিশ করার অধিকার হাতবদল হয়, কিন্তু এর ওপর নির্ভরশীল প্রজেক্টগুলো কোনো নোটিফিকেশন পায় না। এতে কোনো কিছু কম্প্রোমাইজ হয় না। 2021 সালে আপনি যে আস্থা রেখেছিলেন, তা এখন অন্য কারো হাতে।

এটি সবচেয়ে ধীরগতির এবং শনাক্ত করা কঠিন আকৃতি, কোনো কমান্ড সরাসরি এর উত্তর দিতে পারে না। দুটি বিষয় এটি যাচাই করতে সাহায্য করে। কোনো প্যাকেজ গ্রহণ করার আগে npm view লাইনটি ব্যবহার করে দেখুন কারা এটি পাবলিশ করতে পারে। এরপর যখন আপনার কোনো প্রয়োজনীয় প্যাকেজ পরিবর্তিত হয়, তখন নিচের ডিফারেন্সটি দেখুন:

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-scripts

npm ci এবং npm install-এর মধ্যে পার্থক্যগুলো এখানে অত্যন্ত গুরুত্বপূর্ণ। npm ci-এর জন্য একটি lockfile থাকা আবশ্যক। এটি শুরু করার আগেই বিদ্যমান যেকোনো node_modules মুছে ফেলে, যাতে আগের কোনো ডেপ্লয়মেন্টের অবশিষ্টাংশ বর্তমান ডেপ্লয়মেন্টে থেকে না যায়। এটি কখনোই package.json বা lockfile-এ কিছু লেখে না, তাই ইন্সটলেশন প্রক্রিয়া নীরবে আপনাকে নতুন সংস্করণে নিয়ে যেতে পারে না। যদি lockfile এবং package.json-এর মধ্যে অমিল থাকে, তবে এটি কোনো পার্থক্য সমাধান না করে ত্রুটি (error) প্রদর্শন করে বন্ধ হয়ে যায়।

এই ত্রুটিটি কোনো বিরক্তির কারণ নয়, বরং এটি একটি ফিচার। এর অর্থ হলো, কোনো ডিপেন্ডেন্সি পরিবর্তন অবশ্যই এমন একটি কমিট হিসেবে আসতে হবে যা কেউ পর্যালোচনা করেছে, রাত 2টার সময় ডেপ্লয়মেন্টের পার্শ্বপ্রতিক্রিয়া হিসেবে নয়।

প্রতিটি ফেচ (fetch)-এর সময় ইন্টিগ্রিটি হ্যাশ যাচাই করা হয়। যে tarball-এর বাইটগুলো রেকর্ড করা হ্যাশের সাথে মেলে না, তা আনপ্যাক না হয়ে code EINTEGRITY ত্রুটির মাধ্যমে ইন্সটলেশন ব্যর্থ করে দেয়। এটি আপনাকে কী সুবিধা দেয় তা স্পষ্টভাবে বুঝুন: এটি প্রমাণ করে যে আপনি যে ফাইলটি পেয়েছেন তা সেই ফাইলই যা lockfile-এ পিন করা ছিল। এটি সেই একই নিশ্চয়তা দেয় যা চেকসাম দিয়ে ডাউনলোড যাচাই করা আপনাকে প্রদান করে এবং এটি একইভাবে সীমাবদ্ধ। পিন করা সংস্করণটি প্রকাশের সময় ক্ষতিকারক ছিল কি না, সে সম্পর্কে এটি কিছুই বলে না।

--omit=dev সম্পর্কে একটি বিষয়: এই প্যাকেজগুলো তখনও রিজলভ হয় এবং lockfile-এ লেখা হয়। এগুলো কেবল ডিস্কে রাখা হয় না। ডিস্কে কম প্যাকেজ থাকার অর্থ হলো কম ইন্সটল স্ক্রিপ্ট এবং রানটাইমে কম কোড লোড হওয়া, তাই এটি করা লাভজনক। এটি আপনার ট্রি থেকে কোনো ডিপেন্ডেন্সি মুছে ফেলে না।

ইনস্টল স্ক্রিপ্টগুলোকে কোড হিসেবে বিবেচনা করুন এবং সেগুলো প্রত্যাখ্যান করতে শিখুন

আপনি চাইলে ইনস্টল স্ক্রিপ্টগুলো বন্ধ করে রাখতে পারেন। প্রজেক্টের .npmrc ফাইলে নিচের অংশটি যোগ করুন এবং lockfile-এর সাথে এটি commit করুন:

ignore-scripts=true
save-exact=true

ignore-scripts=true ব্যবহারের ফলে npm আর dependencies-এ ঘোষিত স্ক্রিপ্টগুলো চালাতে পারে না। save-exact=true ব্যবহারের ফলে npm install some-lib, ^1.4.2-এর পরিবর্তে package.json ফাইলে 1.4.2 লিখে রাখে, যাতে কোনো resolving range ভুলবশত আপনার manifest-এ প্রবেশ করতে না পারে।

এটি ব্যবহারের ফলে কিছু বিষয় কাজ করা বন্ধ করে দিতে পারে, তাই এটি সক্রিয় করার আগে আপনার জানা উচিত কীভাবে তা ঠিক করতে হয়। যেসব প্যাকেজ native addon কম্পাইল করে বা prebuilt binary ডাউনলোড করে, তারা এই কাজটি ইনস্টল স্ক্রিপ্টের মাধ্যমে করে থাকে। স্ক্রিপ্ট বন্ধ থাকলে ইনস্টলেশন সফল হয়, কিন্তু পরবর্তীতে রানটাইমে ত্রুটি দেখা দেয়, কারণ মডিউলটি তার binding file লোড করতে পারে না। এর সমাধান হলো একটি allowlist তৈরি করা:

npm ci --ignore-scripts
npm rebuild better-sqlite3

npm 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.target

ProtectSystem=strict এই সার্ভিসের জন্য পুরো ফাইল সিস্টেমকে read-only করে দেয়, শুধুমাত্র /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/probe

systemd-analyze security প্রতিটি হার্ডেনিং সেটিংস এবং সেগুলোর এক্সপোজার তালিকাভুক্ত করে, যাতে আপনি দেখতে পারেন কোনগুলো এখনো ডিফল্ট অবস্থায় আছে। touch কমান্ডটি Permission denied এর কারণে ব্যর্থ হওয়া উচিত, কারণ current-এর অধীনে কোনো কিছুর মালিক nodeapp নয়। যদি এটি সফল হয়, তবে আপনার ফাইলের মালিকানা (ownership) ভুল এবং systemd সেটিংস নীরবে তা ঢেকে রাখছে।

EnvironmentFile সম্পর্কে একটি নোট: systemd এটিকে root হিসেবে পড়ে, এরপর এটি User=nodeapp-এ নেমে আসে, তাই ফাইলটি root:root এবং 600 মোডে থাকতে পারে। অ্যাপ্লিকেশনটি তবুও ভেরিয়েবলগুলো গ্রহণ করে। nodeapp হিসেবে শেল অ্যাক্সেস থাকা যে কেউ /proc/<pid>/environ থেকে এগুলো পড়তে পারে, তাই এটি শুধুমাত্র সংরক্ষিত অবস্থায় (at rest) সিক্রেটকে রক্ষা করে, চলমান প্রসেসকে নয়।

ডিপ্লয়মেন্ট ক্রেডেনশিয়ালগুলোকে বিল্ড এনভায়রনমেন্ট থেকে দূরে রাখুন

ইনস্টল স্ক্রিপ্টগুলো এনভায়রনমেন্টের বৈশিষ্ট্য উত্তরাধিকারসূত্রে পায়। এই একটি তথ্যই নির্ধারণ করা উচিত যে আপনি কোথায় বিল্ড করবেন।

সবচেয়ে নিরাপদ পদ্ধতি হলো প্রোডাকশন সার্ভার নয় এমন কোনো জায়গায় বিল্ড করা এবং সেখান থেকে তৈরি ফাইলগুলো সার্ভারে কপি করা। সেক্ষেত্রে বিল্ড মেশিনে শুধুমাত্র একটি রিড-অনলি রেজিস্ট্রি টোকেন থাকবে, অন্য কিছু নয়। সেখানে কোনো SSH ডিপ্লয় কি, ক্লাউড অ্যাক্সেস কি, ডাটাবেস পাসওয়ার্ড বা কন্টেইনার রেজিস্ট্রি লগইন থাকবে না।

npm token create --read-only

একটি রিড-অনলি টোকেন দিয়ে প্যাকেজ ডাউনলোড করা যায়, কিন্তু পাবলিশ করা যায় না। যদি এটি বিল্ড এনভায়রনমেন্ট থেকে চুরিও হয়ে যায়, তবে ক্ষতির পরিমাণ শুধুমাত্র পাবলিক প্যাকেজ ডাউনলোড করার সক্ষমতা হারানোর মধ্যেই সীমাবদ্ধ থাকবে।

যদি আপনাকে সার্ভারেই বিল্ড করতে হয়, তবে 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-01

before অপশনটি শুধুমাত্র সেই ভার্সনগুলো ব্যবহার করে ট্রি পুনর্নির্মাণ করে যা সেই তারিখ বা তার আগে প্রকাশিত হয়েছিল। ডিপেন্ডেন্সি রিফ্রেশ করার সময় এটিকে এক বা দুই সপ্তাহ পিছিয়ে দিন, এতে আপনি সেই সময়কালটি এড়িয়ে যেতে পারবেন যখন কোনো ত্রুটিপূর্ণ রিলিজ লাইভ থাকে কিন্তু রিপোর্ট করা হয়নি। এটি একটি স্থূল পদ্ধতি, কারণ এটি প্রয়োজনীয় সিকিউরিটি ফিক্সগুলোকেও আটকে দেয়। রেঞ্জগুলো সমাধান করতে এটি ব্যবহার করুন, কী পরিবর্তন হয়েছে তা পড়ুন, তারপর lockfile কমিট করুন। একই সতর্কতা npm থেকে ইনস্টল করা কমান্ড লাইন টুলগুলোর ক্ষেত্রেও প্রযোজ্য, যেখানে একটি আনপিন করা npx কল সেই সকালে যা রিলিজ হয়েছে তা-ই নিয়ে আসে, এবং একটি নির্দিষ্ট dsh ভার্সন পিন করা হলো সেই উপায় যা দুটি মেশিনে একই কোড চালানো নিশ্চিত করে।

আমি কীভাবে জানব কোন ভার্সনটি আসলে রিলিজ করা হয়েছে?

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 লেআউটে commit-টি যুক্ত করে এই দুটির মধ্যে সংযোগ স্থায়ী করুন। /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-এর অর্থ হলো আপনি কোডটিকে কোনো অজানা ল্যাপটপের পরিবর্তে একটি নির্দিষ্ট commit-এর সাথে মিলিয়ে দেখতে পারবেন। সব প্যাকেজের ক্ষেত্রে এটি প্রযোজ্য নয়, তাই কোনো attestation না থাকলে সেটিকে "তথ্য নেই" হিসেবে ধরুন, "খারাপ প্যাকেজ" হিসেবে নয়।

একটি ত্রুটিপূর্ণ রিলিজ আপনার সার্ভারে পৌঁছালে যা করবেন

যা রান করেছে এবং যে ব্যবহারকারীর অধীনে রান করেছে, সেখান থেকে কাজ শুরু করুন।

যদি ইনস্টলেশনের সময় কোডটি রান করে থাকে, তবে ধরে নিন বিল্ড ব্যবহারকারীর পড়ার যোগ্য সবকিছুই বেহাত হয়েছে। রেজিস্ট্রি টোকেন, সেই হোম ডিরেক্টরিতে থাকা SSH keys, ক্লাউড ক্রেডেনশিয়াল এবং সেই শেলে এক্সপোর্ট করা যেকোনো সিক্রেট পরিবর্তন (rotate) করুন। রোটেশনই একমাত্র সঠিক পদক্ষেপ, কারণ কোনো ফাইল পড়া হয়নি তা আপনি প্রমাণ করতে পারবেন না।

যদি কোডটি রানটাইমে একটি লক-ডাউন সার্ভিস অ্যাকাউন্টের অধীনে রান করে, তবে এর নাগাল অনেক সীমিত: অ্যাপ্লিকেশনটির নিজস্ব এনভায়রনমেন্ট ভেরিয়েবল এবং এর নেটওয়ার্ক অ্যাক্সেস যা কিছু স্পর্শ করতে পারে। VPS-এ আনপ্রিভিলেজড ব্যবহারকারী হিসেবে সার্ভিস রান করার পুরো যুক্তিটিই এটি। এটি কম্প্রোমাইজ প্রতিরোধ করে না। এটি নির্ধারণ করে যে কম্প্রোমাইজটি মেশিনের কতটা ক্ষতি করতে পারবে এবং রিস্টার্টের পরেও তা টিকে থাকবে কি না।

এরপর ক্লিন করার পরিবর্তে নতুন করে বিল্ড করুন। node_modules ডিলিট করুন, package.json-এ ক্ষতিগ্রস্ত প্যাকেজটিকে ত্রুটিপূর্ণ ভার্সনের নিচে পিন করুন, লকফাইল আপডেট করতে একবার npm install রান করুন, এটি কমিট করুন এবং npm ci দিয়ে ডিপ্লয় করুন। কোনো ট্রি (tree) সরাসরি মেরামত করার চেষ্টা করবেন না। একটি ইনস্টল স্ক্রিপ্ট কী কী পরিবর্তন করেছে তা আপনি পুরোপুরি তালিকাভুক্ত করতে পারবেন না।

সময়সীমাটিও লিখে রাখুন: প্রথম যে ডিপ্লয়টি ভার্সনটি টেনে থাকতে পারে এবং যে ডিপ্লয়টি এটি সরিয়ে ফেলেছে। সেই রেঞ্জটি আপনাকে বলে দেবে আপনার কোন লগগুলো পড়তে হবে, এবং এটি কেবল তখনই জানা সম্ভব যদি আপনার রিলিজগুলোর নামকরণ কমিট অনুযায়ী করা থাকে।

যা এর কোনোটিই সমাধান করে না

একটি lockfile কোনো dependency-কে নিরাপদ করে তোলে না। এটি আপনার dependency গ্রহণ করার মুহূর্তটিকে একটি deploy-এর পার্শ্বপ্রতিক্রিয়া না বানিয়ে, একটি তারিখযুক্ত এবং পর্যালোচিত সিদ্ধান্তে রূপান্তর করে। উপরের প্রতিটি অনুশীলন একই রূপান্তর ঘটায়, অর্থাৎ দুর্ঘটনা থেকে পছন্দে পরিণত করে।

npm audit এখানে কোনো প্রতিরক্ষা নয়। এটি আপনার dependency tree-কে রিপোর্ট করা vulnerability-র একটি ডেটাবেসের সাথে তুলনা করে, তাই এটি এমন সমস্যাগুলো খুঁজে পায় যা ইতিমধ্যে প্রকাশিত এবং চিহ্নিত হয়েছে। একটি supply-chain attack তার পুরো কার্যকর জীবনকাল জুড়ে নামহীন থাকে। পুরনো পরিচিত বাগগুলোর জন্য npm audit চালান, কিন্তু চার ঘণ্টা আগে প্রকাশিত কোনো release-এর বিষয়ে এর কাছ থেকে কোনো কিছু আশা করবেন না।

আপনার dependency-র সংখ্যা কমানো এই গাইডের যেকোনো টুলের চেয়ে বেশি কার্যকর, এবং এটিই সবচেয়ে কম জনপ্রিয় পরামর্শ। আপনি যে প্যাকেজটি যোগ করেন না, সেটি এমন একজন প্রকাশককে বাদ দেয় যাকে আপনার পক্ষ থেকে ফিশিং করা সম্ভব নয়, এবং এটি এমন একটি install script যা আপনার deploy user হিসেবে কখনোই চলে না।

npm-এ এটি নির্দিষ্ট কোনো বিষয় নয়। একই চারটি ধরন PyPI, RubyGems, container image এবং আপনার distribution-এর package manager-এর ক্ষেত্রেও প্রযোজ্য। npm-এ এটি বেশি চোখে পড়ে, কারণ dependency tree বেশি গভীর এবং install script ডিফল্টভাবে চলে। আপনি যে tool আগে থেকেই চালান, সেটিকে সম্প্রসারিত করে এমন যেকোনো কিছু একই সমস্যা উত্তরাধিকারসূত্রে পায়। তাই কোনো dsh plugin install করার আগে এটি কী কী access করতে পারে তা নির্ধারণ করা এবং একটি postinstall script পড়া একই ধরনের কাজ। এখানে আপনার deploy user-এর permission-এর বদলে agent-এর permission প্রযোজ্য। আশপাশের মেশিনের কতটা অংশ সুরক্ষার দায়িত্ব আপনার, তা নির্ভর করে এটি কোথায় চলে তার ওপর। এটি VPS hosting নিরাপদ কি না—এই বৃহত্তর প্রশ্নের অংশ।

FAQ

npm ci কি আমাকে compromised npm package থেকে রক্ষা করে?

এটি আপনাকে আপনার অজান্তে version পরিবর্তিত হওয়া থেকে রক্ষা করে। npm ci ঠিক সেই ফাইলটিই install করে যা package-lock.json-এ রেকর্ড করা আছে। এটি প্রতিটি tarball-কে তার sha512 integrity hash-এর সাথে মিলিয়ে দেখে। যদি package.json এবং lockfile-এর মধ্যে অমিল থাকে, তবে এটি কোনো কিছু সমাধান না করে error প্রদর্শন করে বন্ধ হয়ে যায়। pinned version-টি নিরাপদ কি না, সে বিষয়ে এটি কোনো নিশ্চয়তা দেয় না। আপনি যদি এমন একটি lockfile commit করেন যা কোনো malicious version-কে pin করে রেখেছে, তবে npm ci প্রতিবার আপনার প্রতিটি সার্ভারে বিশ্বস্ততার সাথে সেই version-টিই install করবে।

আমার কি সবকিছুর জন্য ignore-scripts=true সেট করা উচিত?

এটি সেট করুন এবং তারপর allowlist ব্যবহার করুন। প্রজেক্টের .npmrc-এ ignore-scripts=true ব্যবহার করলে dependency install script-গুলো চলা বন্ধ হয়ে যায়। এটি একটি ক্ষতিকারক package থেকে আপনার deploy user-এর credentials-এ পৌঁছানোর সবচেয়ে সরাসরি পথটি বন্ধ করে দেয়। যেসব package native addon compile করে বা prebuilt binary সংগ্রহ করে, তাদের ক্ষেত্রে script-এর প্রয়োজন হয়। script বন্ধ থাকলে সেগুলো install-এর সময় না হলেও, runtime-এ missing binding file error-এর মাধ্যমে ব্যর্থ হবে। npm ci --ignore-scripts চালান এবং তারপর যে কয়েকটি package-কে আপনি বিশ্বাস করার সিদ্ধান্ত নিয়েছেন, সেগুলোর জন্য npm rebuild <package> ব্যবহার করুন। npm query ":attr(scripts, [postinstall])" দেখালে বুঝতে পারবেন আসলে কতগুলো package রয়েছে।

আমার সার্ভারে আসলে কোন version-এর package install হয়েছে তা কীভাবে জানব?

lockfile নয়, বরং disk চেক করুন। npm ls <package> রিপোর্ট করে যে node_modules-এ কী কী আছে এবং node -e "console.log(require('./node_modules/<package>/package.json').version)" শুধুমাত্র version string-টি প্রিন্ট করে। git-এ থাকা lockfile একটি ভিন্ন প্রশ্নের উত্তর দেয়, যা হলো কী install হওয়ার কথা ছিল। এই দুটির তুলনা করাই মূল উদ্দেশ্য। git commit-এর নামে নামকরণ করা ডিরেক্টরিতে deploy করলে কয়েক মাস পরেও যখন প্রয়োজন হবে, তখন উভয় উত্তরই পাওয়া সম্ভব।

npm audit কি supply-chain আক্রমণ খুঁজে বের করতে পারে?

না। npm audit আপনার tree-কে রিপোর্ট করা vulnerabilities-এর একটি database-এর সাথে মিলিয়ে দেখে। তাই এটি শুধুমাত্র সেই সমস্যাগুলোই খুঁজে পায় যা ইতিমধ্যে প্রকাশিত হয়েছে এবং একটি identifier পেয়েছে। একটি malicious release যখন প্রথম ছাড়া হয়, তখন সেটির বিষয়ে কোনো রিপোর্ট থাকে না, অথচ সেই সময়টিতেই install করাটা ঝুঁকিপূর্ণ। npm audit signatures কমান্ডটি বেশি কার্যকর: এটি আপনার install করা tree-তে registry signature যাচাই করে এবং যেখানে publisher-রা provenance attestation তৈরি করেছে, সেগুলো পরীক্ষা করে। এটি আপনাকে নিশ্চিত করে যে tarball-টি কোনো অজানা মেশিন থেকে নয়, বরং একটি public build থেকে এসেছে।

install-এর সময় আক্রমণ হলে, unprivileged user হিসেবে অ্যাপ চালানো কেন গুরুত্বপূর্ণ?

কারণ এই দুটি ব্যর্থতার প্রভাব ভিন্ন এবং আপনি উভয়টির বিরুদ্ধেই প্রতিরক্ষা করছেন। install-এর সময় চলা কোড deploy user হিসেবে কাজ করে এবং সেই user-এর SSH keys, registry tokens এবং cloud credentials পড়তে পারে। runtime কোড service account হিসেবে চলে। User=nodeapp, ProtectSystem=strict এবং disk-এ কোনো credentials না থাকলে, এর প্রভাব শুধুমাত্র অ্যাপের নিজস্ব environment এবং database-এর মধ্যেই সীমাবদ্ধ থাকে। account-গুলোকে আলাদা রাখার অর্থ হলো, traffic পরিবেশনকারী process-টি node_modules rewrite করতে পারবে না। ফলে runtime-এ কোনো compromise হলে তা পরবর্তী restart-এর সাথে সাথেই মুছে যাবে, স্থায়ী হবে না।