সরাসরি পর্বে যান
পর্ব ৫৭ · ৭০-এর মধ্যে · মডিউল ৯

Git-এ নিরাপত্তা — signed commit, secret ফাঁস ঠেকানো আর history পরিষ্কার

Git Security — Signed Commits and Tags, Preventing Secret Leaks, Cleaning History and Safe Credentials

Taught in Bangla, this lesson treats Git history as a security surface. It shows why commit author identity is unverified, signs and verifies commits and tags with SSH, GPG and X.509, explains how secret scanners use regex and entropy, gives the right order after a leak (revoke, rotate, filter-repo, force push, notify), and covers credential helpers, tokens, SSH keys and supply-chain trust.

এই পর্বে যা শিখবেন

  • Git history কেন নিরাপত্তার জায়গা, আর commit-এর author পরিচয় কেন যাচাই ছাড়া বিশ্বাসযোগ্য নয়
  • SSH (Git ২.৩৪+), GPG আর X.509 দিয়ে commit ও tag sign করা, allowed signers ফাইল দিয়ে যাচাই
  • Secret scanner কীভাবে regex আর Shannon entropy দিয়ে কাজ করে, আর তার false positive
  • Secret commit হয়ে গেলে সঠিক ক্রম: বাতিল, নতুন key, filter-repo, force push, দলকে জানানো
  • Credential helper, token-এর scope, SSH key, deploy key; SHA-1 collision, safe.directory আর software supply chain

১ · History একটি নিরাপত্তার জায়গা — কেন commit প্রায় মোছা যায় না

মডিউল ৯-এর আগের পর্বগুলোতে Git-এর ভেতরের কাজ দেখেছেন: object store (পর্ব ৫২ — Git-এর ভেতরে), reflog আর garbage collection (পর্ব ৫৩ — Ref, reflog আর উদ্ধার), log -S দিয়ে খোঁজা (পর্ব ৫৫ — Git দিয়ে bug খোঁজা) আর hook (পর্ব ৫৬ — Git-এর শক্তিশালী হাতিয়ার). এই সব গুণ — কিছুই সহজে না হারানো, পুরো history সবার কাছে — দৈনন্দিন কাজে আশীর্বাদ। কিন্তু নিরাপত্তার চোখে একই গুণ ঝুঁকি।

জমির দলিল রেজিস্ট্রি অফিসের কথা ভাবুন। একবার দলিল রেজিস্ট্রি হলে বালাম বইয়ে লেখা থাকে, আর তার সত্যায়িত কপি চলে যায় ক্রেতা, বিক্রেতা, ব্যাংক — অনেকের কাছে। পরে ভুল ধরা পড়লে সংশোধনী দলিল করা যায়, কিন্তু আগের কপিগুলো সবার আলমারিতে থেকেই যায়। আবার বালাম বইয়ে “বিক্রেতা: অমুক” লেখা থাকলেই সেটা সত্য হয় না — সাক্ষী, সই আর টিপসই যাচাই করেই তা মানা হয়। Git-এর commit ঠিক এমন: একবার push হলে কপি ছড়িয়ে পড়ে, আর author-এর নাম কেবল লেখা — সই না থাকলে প্রমাণ নয়।

এই পর্বের ঝুঁকিগুলো পাঁচ ভাগে ভাবতে পারেন:

  • Secret ফাঁস: password, API key, personal access token বা private key commit হয়ে যাওয়া। Secret একবার history-তে ঢুকলে পরের commit-এ মুছলেও পুরোনো commit থেকে পড়া যায়।
  • জাল পরিচয়: কেউ অন্যের নাম-email বসিয়ে commit বা release tag বানাতে পারেন।
  • History-তে হাত দেওয়া: server বা account দখল করে কোড বদলানো, পুরোনো tag সরানো।
  • চাবি রাখার দুর্বলতা: token খোলা লেখায় ফাইলে, passphrase ছাড়া SSH key, সব repository-তে লেখার ক্ষমতাওয়ালা একটি token.
  • Tool-এর নিজের দুর্বলতা: Git-এর bug, ভুল জায়গায় রাখা .git folder, অজানা submodule.

Public repository-তে push হওয়া key খুব দ্রুত স্বয়ংক্রিয় scanner-এর চোখে পড়তে পারে — নিরাপত্তা-গবেষক আর platform-গুলোর বিভিন্ন প্রতিবেদন এটি বারবার দেখিয়েছে; কতটা দ্রুত, তা পরিস্থিতিভেদে আলাদা। তাই এই পর্বের মূল সুর: secret কখনো commit হতে দেবেন না; হয়ে গেলে ধরে নিন ফাঁস হয়ে গেছে।

এই পর্বের সীমা: এখানে সবকিছু রক্ষার দিক থেকে — কীভাবে নিজের repository আর দলকে সুরক্ষিত রাখবেন, ঘটনা ঘটলে কীভাবে সামলাবেন। আক্রমণের কৌশল শেখানো হবে না; ঝুঁকির কথা আসবে কেবল যতটুকু রক্ষার জন্য জানা দরকার। এই পর্বের সব key, token আর password বানানো — FAKE লেখা দেখলেই বুঝবেন।

এই পর্বের সব Git আউটপুট আসল — Git ২.৫৩ (Windows) আর OpenSSH ১০.২ দিয়ে scratchpad-এর একটি খালি পরীক্ষার folder-এ চালানো। Commit hash যেন আপনার কাছেও একই আসে, তাই নাম “Rahim”, email rahim@example.com, init.defaultBranch=main, core.autocrlf=false, আর প্রতিটি commit-এর সময় GIT_AUTHOR_DATE ও GIT_COMMITTER_DATE দিয়ে এক মিনিট করে বাড়িয়ে ঠিক করা। একটি দরকারি অভ্যাস: পরীক্ষার সময় নিজের আসল config ছুঁইনি — GIT_CONFIG_GLOBAL একটি খালি পরীক্ষার ফাইলের দিকে আর GIT_CONFIG_NOSYSTEM=1. নিরাপত্তার কিছু চেষ্টা করতে চাইলে আপনিও এভাবে আলাদা জায়গায় করুন। গল্পটি একটি কাল্পনিক অনলাইন বাজার অ্যাপের।

২ · Commit-এর পরিচয় শুধু লেখা — আর স্বাক্ষর কী প্রমাণ করে

পর্ব ৮-এ (প্রথম Git repository) git config user.name আর user.email সেট করেছিলেন। Git এগুলো যাচাই করে না — কোনো server-কে জিজ্ঞেস করে না, কোনো password চায় না। যা লেখা আছে, তা-ই commit-এর author আর committer হিসেবে বসে। নিচে রহিম নিজের কম্পিউটারে, এক command-এর জন্য অন্য নাম বসিয়ে commit করলেন:

$ git config user.name
Rahim
$ git -c user.name="Nasrin" -c user.email="nasrin@example.com" commit -q -m "Add shop config"
$ git log --format="%h %an <%ae> %s"
f777349 Nasrin <nasrin@example.com> Add shop config
7f51f18 Rahim <rahim@example.com> Add README

দ্বিতীয় commit-টি আসলে রহিমের হাতে তৈরি, অথচ history বলছে নাসরিন। -c দিয়ে শুধু এই একটি command-এর জন্য config বদলানো যায় — কোনো বাধা নেই। Push করার সময় hosting platform আপনার account যাচাই করে (পর্ব ১৪ — Remote repository-র HTTPS token বা SSH key), কিন্তু সেটা শুধু “কে push করছেন”; commit-এর ভেতরে লেখা নাম-email-এর সাথে মেলায় না। তাই একজন নিজের account দিয়ে এমন commit push করতে পারেন যার author অন্য কেউ। পর্ব ১৮-এর (ভালো commit, tag আর release) Signed-off-by trailer বা DCO-ও একই রকম লেখা মাত্র — সম্মতির ঘোষণা, প্রমাণ নয়।

২০২১ সালের মার্চে PHP project জানায়, তাদের নিজস্ব Git server-এ দুটি ক্ষতিকর commit ঢোকানো হয়েছিল, author হিসেবে বসানো ছিল project-এর দুই পরিচিত রক্ষণাবেক্ষণকারীর নাম। Review-এ দ্রুত ধরা পড়ে সরানো হয়, আর project তাদের মূল repository GitHub-এ সরিয়ে নেয় — project-এর ঘোষণা অনুযায়ী, server-টি কীভাবে দখল হয়েছিল তা পুরোপুরি নিশ্চিত হয়নি। শিক্ষা: নাম দেখে বিশ্বাস করা যায় না; review আর যাচাইযোগ্য স্বাক্ষরই ভরসা।

Digital signature — এক মিনিটে

Digital signature দাঁড়িয়ে আছে public-key cryptography-র ওপর: প্রত্যেকের একজোড়া key — private key শুধু নিজের কাছে, public key সবার জন্য। Private key দিয়ে কোনো লেখার স্বাক্ষর বানানো যায়; public key দিয়ে যে কেউ যাচাই করতে পারেন যে (১) স্বাক্ষরটি ওই জোড়ার private key দিয়েই বানানো, আর (২) লেখার একটি অক্ষরও পরে বদলায়নি। গণিত আর algorithm-এর বিস্তারিত Computer Networks কোর্সের Cryptography-র মৌলিক পর্বে।

Git-এ signed commit মানে commit object-এর লেখার (tree, parent, author, committer, বার্তা) ওপর একটি স্বাক্ষর, যা commit-এর ভেতরেই gpgsig header-এ রাখা হয়। স্বাক্ষর commit-এর অংশ, তাই commit hash-ও তার ওপর নির্ভর করে। Tree আর parent hash দিয়ে যুক্ত থাকায় স্বাক্ষর পরোক্ষে পুরো snapshot আর তার আগের পুরো history-কেও ঢাকে (hash কতটা শক্ত, সে প্রশ্ন অংশ ৮-এ)।

মূল ধারণা

স্বাক্ষর প্রমাণ করে “এই commit ওই key-র মালিক বানিয়েছেন আর পরে বদলায়নি” — এর বেশি নয়। Key কার, সেটা আলাদাভাবে জানতে হয় (allowed signers ফাইল, platform-এ তোলা key, keyring). আর স্বাক্ষর কখনো বলে না যে কোডটি নিরাপদ বা ভালো — সেটা review আর test-এর কাজ।

৩ · Signed commit আর tag — SSH, GPG আর X.509

Git তিন ধরনের স্বাক্ষর বোঝে, বেছে নেওয়া হয় gpg.format config দিয়ে:

ধরনgpg.formatKey কোথা থেকেকখন সুবিধা
GPG (OpenPGP)openpgp (default)GnuPG-র keyring; key-এর ID দিয়ে user.signingkeyপুরোনো ও প্রতিষ্ঠিত; বহু open-source project-এর release tag এতে sign হয়। শেখা আর key সামলানো তুলনায় কঠিন।
SSH signingsshআগে থেকেই থাকা SSH key (ssh-keygen -Y sign ব্যবহার হয়)Git ২.৩৪ (২০২১) থেকে; নতুন tool লাগে না, তাই শুরু করা সহজ।
X.509 (S/MIME)x509প্রতিষ্ঠানের certificate; সাধারণত gpgsm দিয়েযেসব প্রতিষ্ঠান আগে থেকেই certificate-ব্যবস্থা চালায়।

Command সব ক্ষেত্রেই একই: commit-এ বড় হাতের -S (ছোট -s শুধু Signed-off-by লাইন যোগ করে — পর্ব ১৮), tag-এ git tag -s (এটি নিজেই একটি annotated tag বানায়), দেখতে git log --show-signature, যাচাই করতে git verify-commit আর git verify-tag. প্রতিবার -S লিখতে না চাইলে commit.gpgSign true আর tag.gpgSign true.

SSH দিয়ে sign — আসল transcript

পরীক্ষার জন্য scratchpad-এ একটি ফেলে দেওয়ার মতো ed25519 key বানানো হয়েছে, passphrase ছাড়া (ssh-keygen -t ed25519 -N "" -f keys/rahim_demo). পরীক্ষা শেষে মুছে ফেলা হয়েছে। আসল key-তে অবশ্যই passphrase দেবেন (অংশ ৭)। Allowed signers ফাইলের প্রতিটি লাইন বলে “এই principal (সাধারণত email)-এর এই public key, এই কাজের (namespace) জন্য”:

$ git config gpg.format ssh
$ git config user.signingkey ../keys/rahim_demo
$ git config gpg.ssh.allowedSignersFile ../keys/allowed_signers
$ cut -c1-60 ../keys/allowed_signers
rahim@example.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZ
$ git commit -S -m "Lower delivery fee to 50"
[main a0eb0bc] Lower delivery fee to 50
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --show-signature -1
commit a0eb0bca05ed84e4c9a1e1737231f3778fc5e60b
Good "git" signature for rahim@example.com with ED25519 key SHA256:izkxYOtIHXekFPtahvkTxshbwIVtxZER6z3D8Y80ujs
Author: Rahim <rahim@example.com>
Date:   Tue Feb 10 10:02:00 2026 +0600

    Lower delivery fee to 50
$ git verify-commit HEAD
Good "git" signature for rahim@example.com with ED25519 key SHA256:izkxYOtIHXekFPtahvkTxshbwIVtxZER6z3D8Y80ujs
$ git verify-commit HEAD~1; echo $?
1
$ git cat-file commit HEAD | head -6
tree 08c9e41301558f6bf3107ec547bd7ed701e666e3
parent f777349c7cd01d04f4b98efc245dce566263469d
author Rahim <rahim@example.com> 1770696120 +0600
committer Rahim <rahim@example.com> 1770696120 +0600
gpgsig -----BEGIN SSH SIGNATURE-----
 U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgXV3/Y/XNOa317c0PMiAxxr4ke2
…

Setup তিন লাইনের: gpg.format ssh বলে “GPG নয়, SSH”; user.signingkey-এ private key-এর path (ssh-agent ব্যবহার করলে public key-এর path বা key:: দিয়ে key-এর লেখাও দেওয়া যায় — git help config দেখুন); আর gpg.ssh.allowedSignersFile যাচাইয়ের সময় কোন key কার, তা জানায়। এখানে config repository-র নিজস্ব; নিজের সব repository-তে চাইলে --global.

Good "git" signature for rahim@example.com: “git” হলো namespace — SSH key দিয়ে অন্য কাজের স্বাক্ষর (যেমন ফাইল) যেন Git-এর স্বাক্ষর হিসেবে চালানো না যায়। Fingerprint SHA256:izkx… আপনার কাছে আলাদা হবে, কারণ key প্রতিবার নতুন করে বানানো হয়। একই কারণে signed commit-এর hash (a0eb0bc) আপনার কাছে মিলবে না — স্বাক্ষর commit-এর ভেতরে, তাই hash-এর অংশ। আগের দুটি unsigned commit (7f51f18, f777349) অবশ্য হুবহু মিলবে।

verify-commit HEAD~1 কিছুই ছাপেনি, শুধু exit code ১ — স্বাক্ষর নেই। Script বা CI-তে এই exit code দেখেই সিদ্ধান্ত নেওয়া হয়। git cat-file commit HEAD (পর্ব ৫২-এর command) দেখাচ্ছে স্বাক্ষর আসলে committer লাইনের পরে একটি gpgsig header — commit-এর লেখারই অংশ।

এবার একটি signed release tag, আর এমন একটি commit যা অচেনা key দিয়ে sign করা:

$ git tag -s v1.0 -m "Release 1.0"
$ git verify-tag v1.0
Good "git" signature for rahim@example.com with ED25519 key SHA256:izkxYOtIHXekFPtahvkTxshbwIVtxZER6z3D8Y80ujs
$ git -c user.signingkey=../keys/other_demo commit -q -S -m "Lower delivery fee to 40"
$ git verify-commit HEAD; echo $?
Good "git" signature with ED25519 key SHA256:YNtvQj73Y2JRyauX/FllEZb9k1C3ZnIPAOVFzhM4Ac8
No principal matched.
1
$ git log --format="%h %G? %s"
dcab560 U Lower delivery fee to 40
a0eb0bc G Lower delivery fee to 50
f777349 N Add shop config
7f51f18 N Add README

দ্বিতীয় key-টি (other_demo) allowed signers ফাইলে নেই। গণিতের দিক থেকে স্বাক্ষর ঠিক (“Good … signature”), কিন্তু No principal matched. — Git জানে না key-টি কার, তাই exit code ১. এটাই মূল কথা: স্বাক্ষর ঠিক হওয়া আর স্বাক্ষরকারী বিশ্বস্ত হওয়া আলাদা প্রশ্ন। %G? format এক অক্ষরে অবস্থা দেখায়: G = ভালো ও বিশ্বস্ত, U = ভালো কিন্তু বিশ্বস্ততা অজানা, N = স্বাক্ষর নেই; এছাড়া B (খারাপ স্বাক্ষর — লেখা বদলেছে), E (যাচাই করা যায়নি, যেমন key নেই), আর মেয়াদোত্তীর্ণ বা বাতিল key-এর জন্য X, Y, R. লক্ষ্য করুন, জাল-পরিচয়ের f777349 “N” — কেউ এর দায় প্রমাণসহ নেননি।

নিচের stepper-এ setup থেকে যাচাই পর্যন্ত প্রতিটি ধাপে কী তৈরি হয় আর কে কী জানে, তা দেখুন।

ধাপ ১ · key জোড়াআপনার কম্পিউটার
$ ssh-keygen -t ed25519 -C "rahim@example.com"
  → ~/.ssh/id_ed25519      (private, passphrase-সহ)
  → ~/.ssh/id_ed25519.pub  (public, শেয়ার করা যায়)
ধাপ ২ · Git-কে জানানোgit config --global
gpg.format      = ssh
user.signingkey = ~/.ssh/id_ed25519.pub   (agent থাকলে)
commit.gpgSign  = true                     (ঐচ্ছিক)
ধাপ ৩ · commit -Scommit object
tree 08c9e41…
parent f777349…
author Rahim <rahim@example.com> …
committer Rahim <rahim@example.com> …
gpgsig -----BEGIN SSH SIGNATURE-----  ← নতুন
message: Lower delivery fee to 50
ধাপ ৪ · কে কার keyallowed_signers
rahim@example.com namespaces="git" ssh-ed25519 AAAA…
nasrin@example.com namespaces="git" ssh-ed25519 AAAA…
(দলের repository-তে রাখা, review করে বদলানো)
ধাপ ৫ · যাচাইverify-commit
চেনা key   → Good "git" signature for rahim@…   exit 0  (%G? = G)
অচেনা key  → Good … No principal matched.        exit 1  (%G? = U)
স্বাক্ষর নেই → (কিছু ছাপে না)                       exit 1  (%G? = N)
  1. Key জোড়া। ssh-keygen দুটি ফাইল বানায়। Private ফাইলটি কোথাও পাঠানো হয় না; passphrase দিলে ফাইল চুরি হলেও সরাসরি কাজে লাগে না। আগে থেকে SSH key থাকলে সেটাই ব্যবহার করা যায়, তবে অনেক দল sign-এর জন্য আলাদা key পছন্দ করে।
  2. Git-কে জানানো। তিনটি config: format, কোন key, আর চাইলে সব commit-এ নিজে থেকে sign. Private key ssh-agent-এ থাকলে user.signingkey-এ public key-এর path দিলেই চলে।
  3. git commit -S. Git commit-এর লেখা তৈরি করে ssh-keygen -Y sign-কে দেয়, স্বাক্ষর ফেরত পেয়ে gpgsig header হিসেবে বসায়, তারপর পুরোটার hash নেয়। তাই স্বাক্ষর বদলালে commit hash-ও বদলায়।
  4. কে কার key. যাচাইকারীর কাছে principal → public key-এর তালিকা থাকতে হয়। দল এটি repository-তে রেখে review-সহ বদলাতে পারে; platform-এ “Verified” দেখাতে public key-টি নিজের account-এ “signing key” হিসেবে তুলতে হয় (platform-ভেদে নিয়ম আলাদা)।
  5. যাচাই। তিনটি ফল: চেনা key-তে exit ০; গাণিতিকভাবে ঠিক কিন্তু অচেনা key-তে exit ১ আর “No principal matched.”; স্বাক্ষর না থাকলে নীরব exit ১. CI-তে exit code দেখে merge আটকানো যায়।

Platform-এর “Verified” badge

বড় hosting platform-গুলো (যেমন GitHub, GitLab) commit-এর পাশে “Verified” জাতীয় চিহ্ন দেখায়, যখন স্বাক্ষর এমন একটি key-এর সাথে মেলে যা ওই commit-এর email-ওয়ালা account-এ তোলা আছে। কিছু platform-এ একটি কড়া মোড আছে (যেমন GitHub-এর “vigilant mode”), যেখানে আপনার নামে আসা unsigned commit-কে “Unverified” দেখায় — যাতে জাল-পরিচয়ের commit চোখে পড়ে। আর branch protection-এ “শুধু signed commit গ্রহণযোগ্য” নিয়ম বসানো যায়। নাম আর নিয়ম লেখার সময়ের, platform-ভেদে আলাদা — documentation মিলিয়ে নিন। একটি সূক্ষ্মতা: platform নিজে web interface-এ merge বা squash করলে commit-টি প্রায়ই platform-এর নিজের key-তে sign হয়।

৪ · Secret ফাঁস ঠেকানো — স্তরে স্তরে পাহারা

সবচেয়ে সস্তা ঘটনা হলো যা ঘটেই না। Secret যেন repository-তেই না ঢোকে, তার জন্য একটির ওপর আরেকটি স্তর বসানো হয় — নিরাপত্তায় একে বলে defense in depth. কোনো একটি স্তর নিখুঁত নয়; মিলে তারা ঝুঁকি কমায়।

  • Secret কোডের বাইরে: কোডে key না লিখে environment variable থেকে পড়া (os.environ["PAYMENT_API_KEY"]). Local কাজে মান থাকে একটি .env file-এ, যা .gitignore-এ রাখা আর তার বদলে খালি মানের .env.example commit করা — পর্ব ১০-এ (.gitignore, .gitattributes আর git config) দেখেছেন। মনে রাখুন, .gitignore আগে থেকে track হওয়া ফাইলকে থামায় না।
  • Secret manager: production-এ secret রাখা হয় একটি secret manager-এ (cloud provider-এর vault বা নিজস্ব ব্যবস্থা), যা প্রবেশাধিকার, log আর নিয়মিত বদল সামলায়। নকশার দিকটি System Design কোর্সের নিরাপত্তার নকশা পর্বে।
  • Commit-এর আগে scanner: একটি pre-commit hook staged পরিবর্তনে secret-এর ধাঁচ খোঁজে, পেলে commit থামায়। Hook কীভাবে কাজ করে, পর্ব ৫৬-এ। প্রচলিত open-source tool-এর মধ্যে আছে gitleaks, detect-secrets, TruffleHog — প্রত্যেকের নিয়ম, option আর license আলাদা, আর বদলায়; ব্যবহারের আগে নিজের documentation দেখুন।
  • Push protection: কিছু platform push গ্রহণের আগেই পরিচিত ধরনের secret খোঁজে আর push আটকায় (লেখার সময়ে GitHub-এ “push protection” নামে; অন্য platform-এ আলাদা নাম ও শর্ত)। Hook client-এ বন্ধ করা যায়, server-এর পাহারা নয় — তাই দুটোই দরকার।
  • CI আর পুরো history scan: pipeline-এ প্রতিটি pull request আর নির্দিষ্ট সময় পরপর পুরো history scan; CI-তে secret কীভাবে রাখবেন, পর্ব ৫৮-এ (Continuous integration). Code review-এর নিরাপত্তা-চোখ পর্ব ৪৪-এ (Code review).
  • শেষ ভরসা — বদলানো: সব স্তর ফসকালে secret বাতিল করে নতুন বানানো (secret rotation). অংশ ৫-এ।

Secret ফাঁস ঠেকানোর পাঁচ স্তর

Editor/IDE → pre-commit → push protection → CI scan → rotation; the earlier a leak is caught, the cheaper it is

Secret ফাঁস ঠেকানোর স্তর: editor, pre-commit, push protection, CI scan, rotation ১ · Editor / IDE লেখার সময়ই সতর্কতা খরচ প্রায় শূন্য ২ · Pre-commit hook staged diff scan history-তে এখনো কিছু নেই ৩ · Push protection server গ্রহণের আগে আটকায় commit শুধু local-এ: amend করুন ৪ · CI ও history scan push হয়ে গেছে, কপি ছড়িয়েছে বাতিল + পরিষ্কার লাগবে ৫ · Rotation key বাতিল, নতুন key শেষ ভরসা, সবসময় কাজ করে যত নিচে, তত দামি ওপরের স্তর ফসকালে নিচেরটি ধরে
ছবি ৫৭.১ — ওপরের তিন স্তরে secret তখনো দলের history-তে ঢোকেনি; চতুর্থ স্তর থেকে কপি ছড়িয়ে পড়েছে, তাই বাতিল করাই আসল সমাধান।

Scanner ভেতরে কীভাবে কাজ করে — একটি খেলনা scanner

বেশিরভাগ secret scanner দুটি কৌশল মেশায়। এক, নির্দিষ্ট ধাঁচ (regex): অনেক provider-এর key-এর শুরুতে পরিচিত prefix থাকে আর দৈর্ঘ্য নির্দিষ্ট; যেমন AWS-এর access key ID-র প্রচলিত রূপ AKIA + ১৬টি বড় হাতের অক্ষর বা অঙ্ক। দুই, এলোমেলোতা মাপা: মানুষের লেখা শব্দে কিছু অক্ষর বারবার আসে, কিন্তু এলোমেলো token-এ অক্ষর ছড়ানো থাকে। এই ছড়ানো-ভাব মাপে Shannon entropy:

H = − Σ p(c) · log₂ p(c)   bit / অক্ষর

এখানে p(c) হলো লেখায় অক্ষর c-এর অনুপাত। সব অক্ষর এক হলে H = ০; ১৬ রকম অক্ষর সমানভাবে থাকলে H = log₂16 = ৪. নিচের খেলনা scanner একটি diff-এর শুধু যোগ হওয়া (+) লাইন দেখে, দুটি regex নিয়ম আর ২০ অক্ষরের বেশি লম্বা টুকরোর entropy মাপে। সব মান বানানো।

import re, math
# Toy secret scanner. ALL values below are FAKE, made up for this lesson.
DIFF = """+DB_HOST=localhost
+AWS_ACCESS_KEY_ID=AKIAFAKEEXAMPLE00001
+PAYMENT_API_KEY=FAKE_sk_test_9fQ2xLm4Rt7Vb1Nz
+password = "changeme"
+COMMIT_ID=3f9a1c2e8b7d6a5f4e3c2b1a0f9e8d7c6b5a4f3e
+GREETING=assalamualaikum_bandhu
-OLD_KEY=removed_line_is_not_scanned"""
RULES = [("aws-style-key-id", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
         ("password-assignment", re.compile(r"(?i)password\s*=\s*\"[^\"]+\""))]
TOKEN = re.compile(r"[A-Za-z0-9_+/=-]{20,}")
def entropy(s):
    n = len(s)
    return -sum(s.count(c) / n * math.log2(s.count(c) / n) for c in set(s))
def scan(diff, threshold=4.0):
    found = []
    for no, line in enumerate(diff.splitlines(), 1):
        if not line.startswith("+"):
            continue                      # only added lines
        for name, rx in RULES:
            if rx.search(line):
                found.append((no, name, line[1:40]))
        for tok in TOKEN.findall(line.split("=", 1)[-1]):
            h = entropy(tok)
            if h >= threshold:
                found.append((no, f"high-entropy {h:.2f}", tok[:24]))
    return found
for no, rule, text in scan(DIFF):
    print(f"line {no}: {rule:22} {text}")

আউটপুট:

line 2: aws-style-key-id       AWS_ACCESS_KEY_ID=AKIAFAKEEXAMPLE00001
line 3: high-entropy 4.46      FAKE_sk_test_9fQ2xLm4Rt7
line 4: password-assignment    password = "changeme"

তিনটি লাইন ধরা পড়ল, তিনটি আলাদা কারণে। লাইন ২ ধরা পড়েছে ধাঁচে — এর entropy মাত্র প্রায় ৩.১৮ (“FAKE”, “EXAMPLE”, অনেক ০), শুধু entropy দেখলে এটি ফসকে যেত। লাইন ৩-এর token কোনো পরিচিত ধাঁচে মেলে না, কিন্তু entropy ৪.৪৬ ≥ ৪.০ — এলোমেলো দেখায়। লাইন ৪-এর “changeme” ছোট আর মোটেই এলোমেলো নয়; ধরা পড়েছে শুধু password = "…" ধাঁচে। লাইন ৫-এর commit id ৪০ অক্ষরের, তবু ধরা পড়েনি: hex-এ মাত্র ১৬ রকম অক্ষর, তাই entropy কখনো ৪-এর বেশি হতে পারে না (এখানে ৩.৯২)। লাইন ৬-এর সাধারণ শব্দ আর লাইন ৭-এর মুছে ফেলা (-) লাইনও বাদ। খেলনার সীমা: আসল tool শত শত নিয়ম রাখে, file-এর ধরন বোঝে, কিছু ক্ষেত্রে key আসল কি না provider-এর কাছে যাচাই করে (এবং সেটা নিজের অনুমতির মধ্যে করা উচিত), আর ভুল সতর্কতা চুপ করানোর নিয়ম দেয়। মনে রাখুন: scanner কম পারলে ঝুঁকি, বেশি চেঁচালে মানুষ শোনা ছেড়ে দেন।

Entropy-র সীমা নিজে মেপে দেখুন — কোন সীমায় কোনটি ধরা পড়ে, আর কোথায় false positive শুরু হয়:

// Shannon entropy in bits per character: H = -sum p(c) * log2 p(c)
// All "tokens" below are FAKE examples made up for this lesson.
function entropy(s) {
  const count = {};
  for (const c of s) count[c] = (count[c] || 0) + 1;
  let h = 0;
  for (const c in count) {
    const p = count[c] / s.length;
    h -= p * Math.log2(p);
  }
  return h;
}
const samples = [
  ["random-looking token", "FAKE_sk_test_9fQ2xLm4Rt7Vb1Nz"],
  ["git commit id (hex)", "3f9a1c2e8b7d6a5f4e3c2b1a0f9e8d7c6b5a4f3e"],
  ["uuid (not secret)", "7c9e6679-7425-40de-944b-e07fc1f90ae7"],
  ["normal words", "delivery_fee_for_dhaka_city"],
  ["weak password", "changeme"],
  ["same letter", "aaaaaaaaaaaaaaaaaaaa"],
];
const flag = (s, h, t) => (s.length >= 20 && h >= t ? "FLAG" : "ok  ");
console.log("T=4.0 T=3.5  H    len  sample");
for (const [label, s] of samples) {
  const h = entropy(s);
  console.log(`${flag(s, h, 4.0)}  ${flag(s, h, 3.5)}   ${h.toFixed(2)} ${String(s.length).padStart(3)}  ${label}`);
}
console.log("max for hex = log2(16) =", Math.log2(16));
console.log("max for base64 = log2(64) =", Math.log2(64));

আউটপুট:

T=4.0 T=3.5  H    len  sample
FLAG  FLAG   4.46  29  random-looking token
ok    FLAG   3.92  40  git commit id (hex)
ok    FLAG   3.66  36  uuid (not secret)
ok    FLAG   3.72  27  normal words
ok    ok     2.75   8  weak password
ok    ok     0.00  20  same letter
max for hex = log2(16) = 4
max for base64 = log2(64) = 6

সীমা ৪.০-এ শুধু আসল-দেখতে token ধরা পড়ে। সীমা ৩.৫-এ নামালে commit id, UUID আর এমনকি “delivery_fee_for_dhaka_city”-ও FLAG — তিনটিই false positive. Hex লেখার সর্বোচ্চ entropy ৪, base64-এর ৬; তাই অভিজ্ঞ tool অক্ষরের ধরন অনুযায়ী আলাদা সীমা রাখে (যেমন detect-secrets-এ hex আর base64-এর জন্য আলাদা সীমা — মান tool-ভেদে)। উল্টো দিকটিও দেখুন: “changeme” দুর্বল password, অথচ entropy আর দৈর্ঘ্য — দুই হিসাবেই ফসকে যায় (false negative). এজন্য entropy একা যথেষ্ট নয়; ধাঁচ, প্রসঙ্গ (“password =”, “secret:”) আর মানুষের review মিলিয়ে কাজ।

৫ · Secret commit হয়ে গেলে — আগে বাতিল, পরে history

ধরুন রহিম ভুল করে .env commit করেছেন, পরের commit-এ সেটি track থেকে সরিয়ে .gitignore-এ দিয়েছেন। Working tree আর সর্বশেষ commit-এ ফাইল নেই। কিন্তু history? পর্ব ৫৫-এর log -S (যে commit-এ লেখাটি যোগ বা বাদ হয়েছে) দিয়ে খুঁজলেন:

$ git ls-files
.gitignore
README.md
app.py
$ git log --oneline
b109503 Lower fee to 50
93fa95a Stop tracking .env
1799e4e Add app and settings
c6c1dc3 Add README
$ git log -p -S "PAYMENT_API_KEY" --format="%h %s" -- .env
93fa95a Stop tracking .env

diff --git a/.env b/.env
deleted file mode 100644
index 2113fdb..0000000
--- a/.env
+++ /dev/null
@@ -1,2 +0,0 @@
-DB_HOST=localhost
-PAYMENT_API_KEY=FAKE_sk_test_9fQ2xLm4Rt7Vb1Nz
1799e4e Add app and settings

diff --git a/.env b/.env
new file mode 100644
index 0000000..2113fdb
--- /dev/null
+++ b/.env
@@ -0,0 +1,2 @@
+DB_HOST=localhost
+PAYMENT_API_KEY=FAKE_sk_test_9fQ2xLm4Rt7Vb1Nz
$ git grep -n "PAYMENT_API_KEY" $(git rev-list --all)
1799e4e1869584595b12ccb62aa861ff7da3ffbf:.env:2:PAYMENT_API_KEY=FAKE_sk_test_9fQ2xLm4Rt7Vb1Nz

git ls-files-এ .env নেই, অথচ log -p -S দুটি commit দেখাল: 1799e4e-এ লেখাটি এসেছে, 93fa95a-এ গেছে। git grep … $(git rev-list --all) সব commit-এর snapshot-এ খোঁজে — 1799e4e-এর .env-এর ২ নম্বর লাইনে key আছে। যে কেউ clone করে git show 1799e4e:.env দিলেই পড়তে পারবেন। File-এর blob (2113fdb) object store-এ আছে, আর commit-গুলো এটিকে পৌঁছানোর পথ রাখে (পর্ব ৫২)। “Stop tracking .env” commit secret মোছেনি, শুধু ভবিষ্যতে যোগ হওয়া থামিয়েছে।

সঠিক ক্রম — কেন আগে বাতিল

প্রথম প্রবৃত্তি হয় history পরিষ্কার করা। কিন্তু push হয়ে থাকলে ধরে নিন secret ফাঁস হয়ে গেছে: কেউ clone করে থাকতে পারেন, কোনো bot দেখে থাকতে পারে। History পরিষ্কারে সময় লাগে আর সব কপি মোছা যায় না (অংশ ৬)। তাই incident response-এর ক্রম:

  1. Revokekey বাতিলprovider-এ
  2. Rotateনতুন keysecret manager-এ
  3. Cleanfilter-repoনতুন clone-এ
  4. Force push--forceসমন্বয় করে
  5. Notifyre-cloneদল, host
  6. Verifylog -Slog দেখা
অবস্থা
পুরোনো key কাজ করে?
App চলছে?
আপনার history-তে secret
Server-এর branch-এ
সহকর্মীদের clone-এ
বাকি ঝুঁকি
  1. Revoke — সবার আগে। যে service-এর key, সেখানে গিয়ে key বাতিল। এক মুহূর্তে ফাঁস হওয়া string মূল্যহীন — history-তে থাকুক বা না থাকুক। App থেমে যেতে পারে; তাই অনেক দল ধাপ ১ আর ২ প্রায় একসাথে করে, কিন্তু বাতিল পেছানো যায় না।
  2. Rotate. নতুন key বানিয়ে secret manager বা environment variable-এ বসানো, কোডে নয়। এবার app চলে। Key কোথায় কোথায় ব্যবহার হতো (CI, অন্য server), সব জায়গায় বদলাতে হয়।
  3. Clean. একটি নতুন clone-এ git filter-repo দিয়ে secret বা ফাইলটি পুরো history থেকে সরানো। প্রতিটি পরের commit-এর hash বদলায়। এখনো server পুরোনো history রাখে।
  4. Force push. সব branch আর tag-এ নতুন history পাঠানো — আগে দলকে জানিয়ে, push-এর সময় কাজ থামিয়ে। Protected branch-এ সাময়িক অনুমতি লাগতে পারে। Server-এর branch এখন পরিষ্কার, কিন্তু fork আর platform-এর cache-এ পুরোনো commit থাকতে পারে।
  5. Notify. সহকর্মীরা পুরোনো clone মুছে নতুন clone করবেন — পুরোনো clone থেকে pull বা push করলে পুরোনো commit আবার ঢুকতে পারে। Hosting platform-এর support-কে cache, pull request ref বা fork নিয়ে জানানো (নিয়ম platform-ভেদে)। প্রতিষ্ঠানে নিরাপত্তা দলকেও।
  6. Verify. নতুন clone-এ git log -p -S আর scanner দিয়ে পুরো history খুঁজে কিছু না পাওয়া; provider-এর log-এ পুরোনো key দিয়ে কোনো অপব্যবহার হয়েছিল কি না দেখা; কীভাবে ঘটল, তা নিয়ে দোষারোপহীন পর্যালোচনা আর একটি স্তর (যেমন pre-commit scanner) যোগ।

Push হয়নি এখনো? তাহলে কাজ অনেক সহজ। Secret শুধু সর্বশেষ commit-এ থাকলে ফাইল ঠিক করে git commit --amend; কয়েকটি commit আগে হলে interactive rebase (পর্ব ১৫ — Rebase). পুরোনো commit তখনো reflog-এ কিছুদিন থাকে, তবে শুধু আপনার কম্পিউটারে (পর্ব ৫৩)। তবু কোথাও কপি গেছে কি না নিশ্চিত না হলে key বদলানোই নিরাপদ।

History পরিষ্কার — git filter-repo

Git-এর নিজের documentation (git help filter-branch) পুরোনো git filter-branch-এর বদলে git filter-repo ব্যবহারের পরামর্শ দেয়। filter-repo একটি আলাদা Python tool — Git-এর সাথে আসে না, আলাদা install করতে হয় (package manager বা project-এর নির্দেশনা দেখুন)। এই পরীক্ষার কম্পিউটারে এটি নেই:

$ git filter-repo --version
git: 'filter-repo' is not a git command. See 'git --help'.

তাই নিচের command-গুলো চালানো হয়নি, শুধু documentation-অনুযায়ী উদাহরণ; চালানোর আগে নিজের সংস্করণের git filter-repo --help পড়ুন:

# 1) work on a FRESH clone (filter-repo refuses a non-fresh repo by default)
git clone https://github.com/example/bazar-app.git bazar-clean
cd bazar-clean

# 2a) remove a whole file from every commit
git filter-repo --invert-paths --path .env

# 2b) OR keep files, replace the secret text everywhere
#     replacements.txt — one rule per line; a line with no "==>"
#     is replaced by ***REMOVED***
#       FAKE_sk_test_9fQ2xLm4Rt7Vb1Nz
#       regex:FAKE_sk_test_[A-Za-z0-9]+==>REDACTED
git filter-repo --replace-text ../replacements.txt

# 3) check, then push every branch and tag (coordinate with the team first)
git log -p -S "FAKE_sk_test" --all        # expect: nothing
git remote add origin https://github.com/example/bazar-app.git   # filter-repo removes origin
git push --force --all origin
git push --force --tags origin

--path .env --invert-paths মানে “.env ছাড়া সব রাখা হোক” — প্রতিটি commit থেকে ফাইলটি বাদ। --replace-text ফাইল রেখে নির্দিষ্ট লেখা বদলায় — যখন secret কোনো দরকারি ফাইলের (যেমন config) ভেতরে। filter-repo কাজ শেষে নিজের থেকে পুরোনো ref আর reflog পরিষ্কার করে, আর ভুল করে পুরোনো জায়গায় push না হয় তাই origin remote সরিয়ে দেয় (documentation অনুযায়ী; সংস্করণভেদে খুঁটিনাটি আলাদা হতে পারে)। আরেকটি প্রচলিত tool, BFG Repo-Cleaner (Java-তে লেখা), --delete-files আর --replace-text দিয়ে একই কাজ দ্রুত করে; default-এ এটি সর্বশেষ commit-এর বিষয়বস্তু ছোঁয় না, তাই আগে নিজে secret সরিয়ে commit করতে হয় — তার documentation দেখুন।

পুরোনো পথ: filter-branch — আর তার সতর্কবার্তা

filter-branch Git-এর সাথেই আসে, তাই কোন সমস্যার কথা Git নিজে বলে, দেখা যাক। একই repository-তে প্রতিটি commit-এর index থেকে .env সরানো হলো:

$ git filter-branch --index-filter "git rm -q --cached --ignore-unmatch .env" --prune-empty -- --all
WARNING: git-filter-branch has a glut of gotchas generating mangled history
	 rewrites.  Hit Ctrl-C before proceeding to abort, then use an
	 alternative filtering tool such as 'git filter-repo'
	 (https://github.com/newren/git-filter-repo/) instead.  See the
	 filter-branch manual page for more details; to squelch this warning,
	 set FILTER_BRANCH_SQUELCH_WARNING=1.
Proceeding with filter-branch...

Rewrite c6c1dc3… (1/4) … Rewrite b109503… (4/4) …
Ref 'refs/heads/main' was rewritten
Ref 'refs/remotes/origin/main' was rewritten
$ git log --oneline
1664c3e Lower fee to 50
d6873dc Stop tracking .env
d1af7cd Add app and settings
c6c1dc3 Add README
$ git for-each-ref --format="%(refname)"
refs/heads/main
refs/original/refs/heads/main
refs/original/refs/remotes/origin/main
refs/remotes/origin/main
$ git grep -n "PAYMENT_API_KEY" $(git rev-list --all)
1799e4e1869584595b12ccb62aa861ff7da3ffbf:.env:2:PAYMENT_API_KEY=FAKE_sk_test_9fQ2xLm4Rt7Vb1Nz
$ git cat-file -t 2113fdb
blob
$ git for-each-ref --format="delete %(refname)" refs/original/ | git update-ref --stdin
$ git reflog expire --expire=now --all
$ git gc -q --prune=now
$ git cat-file -t 2113fdb
fatal: Not a valid object name 2113fdb

Git নিজেই সতর্ক করল: filter-branch-এ “a glut of gotchas” — ভুল history তৈরির অনেক ফাঁদ — আর filter-repo ব্যবহারের পরামর্শ; শুরুর আগে কয়েক সেকেন্ড অপেক্ষা করে, যাতে Ctrl-C চাপা যায়। (Progress লাইনগুলো ছোট করা হয়েছে।) c6c1dc3-এ .env ছিল না, তাই hash একই রইল; তার পরের তিনটি commit নতুন hash পেল (d1af7cd, d6873dc, 1664c3e), কারণ parent বদলালে child-এর hash-ও বদলায়। --prune-empty খালি হয়ে যাওয়া commit বাদ দিত, তবে এখানে কোনোটি খালি হয়নি — প্রতিটিতে .env ছাড়াও অন্য কিছু ছিল।

সবচেয়ে বড় ফাঁদটি দেখুন: rewrite-এর পরেও git grep key খুঁজে পেল! filter-branch পুরোনো ref refs/original/…-এ backup রাখে, আর reflog-ও পুরোনো commit চেনে। Local-এ সত্যিই মুছতে backup ref মোছা, reflog খালি করা আর gc --prune=now — তিনটিই লাগে (পর্ব ৫৩-এর garbage collection). শেষে blob 2113fdb আর নেই। filter-repo এই পরিষ্কার নিজেই করে — এ কারণেও এটি সুপারিশকৃত।

filter-repo-র --replace-text আসলে কী করে, আর কেন সেটা অন্য কপিতে পৌঁছায় না — একটি ছোট মডেলে দেখুন। এখানে commit id-কে Git-এর মতো বিষয়বস্তু আর parent-এর hash দিয়ে বানানো হয়েছে (Python-এর hashlib SHA-1, প্রথম ৭ অক্ষর; আসল Git object-এর format আলাদা, তাই hash Git-এর সাথে মিলবে না):

import hashlib
# Model of what `git filter-repo --replace-text` does. The secret is FAKE.
SECRET = "FAKE_sk_test_9fQ2xLm4Rt7Vb1Nz"
history = [("Add README", "# Bazar app"),
           ("Add settings", "DB_HOST=localhost\nPAYMENT_API_KEY=" + SECRET),
           ("Stop tracking key", "DB_HOST=localhost"),
           ("Lower fee to 50", "DB_HOST=localhost\nFEE=50")]
def build(hist):
    commits, parent = [], ""
    for msg, text in hist:   # id depends on content AND parent, like Git
        cid = hashlib.sha1((parent + msg + text).encode()).hexdigest()[:7]
        commits.append({"id": cid, "msg": msg, "text": text})
        parent = cid
    return commits
def replace_text(hist, old, new="***REMOVED***"):
    return [(m, t.replace(old, new)) for m, t in hist]
server = build(history)
old_clone = [dict(c) for c in server]          # a teammate cloned earlier
server = build(replace_text(history, SECRET))  # rewrite + force push
for before, after in zip(old_clone, server):
    same = "same" if before["id"] == after["id"] else "NEW "
    print(f"{before['id']} -> {after['id']} {same} {after['msg']}")
print("server, Add settings:", server[1]["text"].splitlines()[1])
leak = [c["id"] for c in old_clone if SECRET in c["text"]]
print("old clone still has secret in:", leak)
print("ids that changed:", sum(a["id"] != b["id"] for a, b in zip(old_clone, server)))

আউটপুট:

323be1a -> 323be1a same Add README
eb89f0c -> 5fc9e36 NEW  Add settings
58b7a8c -> 5512840 NEW  Stop tracking key
d596dca -> a1afbb1 NEW  Lower fee to 50
server, Add settings: PAYMENT_API_KEY=***REMOVED***
old clone still has secret in: ['eb89f0c']
ids that changed: 3

প্রথম commit-এ secret ছিল না, তাই id একই। “Add settings”-এর লেখা বদলেছে, তাই নতুন id; আর পরের দুটির লেখা না বদলালেও parent বদলেছে, তাই তাদেরও নতুন id — ঠিক উপরের filter-branch transcript-এর মতো। Server-এ এখন ***REMOVED***. কিন্তু old_clone তালিকাটি rewrite-এর আগে কপি করা; সেখানে eb89f0c-এ secret অক্ষত। Rewrite শুধু আপনার repository বদলায়; অন্য কারও কম্পিউটারে পৌঁছানোর কোনো উপায় Git-এর নেই। তিন commit-এর id বদলেছে মানে যাঁরা এগুলোর ওপর কাজ করছিলেন, তাঁদের branch এখন “অন্য history”-র ওপর — এজন্যই re-clone.

৬ · Push-এর পরে secret কোথায় কোথায় থেকে যায়

আসল পরীক্ষায় একটি bare “server” আর rewrite-এর আগে বানানো একটি সহকর্মীর clone (oldclone) রাখা হয়েছিল। পরিষ্কার history force push করার পরে:

$ git push --force origin main
To ../server.git
 + b109503...1664c3e main -> main (forced update)
$ git -C ../server.git cat-file -t 2113fdb
blob
$ git -C ../oldclone fetch
From C:/…/t57/leak/../server
 + b109503...1664c3e main       -> origin/main  (forced update)
$ git -C ../oldclone log --oneline --all
b109503 Lower fee to 50
1664c3e Lower fee to 50
93fa95a Stop tracking .env
d6873dc Stop tracking .env
1799e4e Add app and settings
d1af7cd Add app and settings
c6c1dc3 Add README
$ git -C ../oldclone show 1799e4e:.env
DB_HOST=localhost
PAYMENT_API_KEY=FAKE_sk_test_9fQ2xLm4Rt7Vb1Nz

তিনটি শিক্ষা। এক, server-এর main সরেছে (forced update), কিন্তু server-এর object store-এ blob 2113fdb এখনো আছে — কোনো ref থেকে পৌঁছানো যায় না, তবে server-এ garbage collection না চলা পর্যন্ত থাকে। Hosting platform-এ এই gc কখন চলে, আপনার নিয়ন্ত্রণে নেই। দুই, সহকর্মীর clone fetch করলেও তাঁর local main পুরোনো history-তেই; --all-এ দুই history পাশাপাশি — একই বার্তার দুটি করে commit. তিন, git show 1799e4e:.env এখনো key দেখায়। তিনি সাধারণ git pull দিলে দুই history merge হয়ে পুরোনো commit আবার ফিরে আসতে পারে, আর পরের push-এ server-এও। তাই নির্দেশনা স্পষ্ট: “পুরোনো clone মুছে নতুন clone; অসমাপ্ত কাজ থাকলে patch বানিয়ে নতুন clone-এ বসান।”

কোন কপিতে secret আছে
আপনার repository
Server-এর branch
Server-এর object store
নাসরিনের clone
Fork, PR ref, CI cache
Key কি কাজের?
  1. Commit. Secret শুধু আপনার কম্পিউটারে। এখনই ধরা পড়লে amend বা rebase-ই যথেষ্ট; কোনো কপি ছড়ায়নি।
  2. Push. Server-এর branch আর object store — দুই জায়গায় এল। Public repository হলে এই মুহূর্ত থেকেই যে কেউ পড়তে পারেন।
  3. অন্যরা clone/fetch করলেন। নাসরিনের কম্পিউটার, কারও fork, pull request-এর ref, CI-এর cache — প্রতিটি একটি স্বাধীন পূর্ণ কপি। কারও কাছে অনুমতি না নিয়েই এগুলো তৈরি হয়।
  4. আপনি rewrite করলেন। শুধু আপনার repository পরিষ্কার। Git-এর এমন কোনো command নেই যা অন্য কম্পিউটারের history মুছতে পারে।
  5. Force push. Server-এর branch পরিষ্কার, কিন্তু object তখনো server-এ থাকতে পারে। নাসরিনের কপি, fork, cache — সব আগের মতো।
  6. শেষ অবস্থা। Re-clone করা কপি পরিষ্কার; fork বা cache-এ কিছু থেকে যেতে পারে, আর তা মোছাতে host-এর সাহায্য লাগে। একমাত্র নিশ্চিত সুরক্ষা শেষ সারিতে: key বাতিল হলে সব কপির string মূল্যহীন।

Force push-এর সমন্বয়: history rewrite প্রত্যেকের চলমান কাজে প্রভাব ফেলে (পর্ব ১৪-এর force push). আগে দলকে সময় জানান, সবাইকে push থামাতে বলুন, protected branch-এ সাময়িক অনুমতি নিন, push-এর পরে আবার সুরক্ষা চালু করুন, আর নতুন hash-গুলো জানিয়ে দিন। পুরোনো hash উল্লেখ করা issue বা release note থাকলে সেগুলোর link ভাঙবে। Hosting platform-এ pull request-এর ref বা cache করা পুরোনো commit-এর পাতা মোছাতে সাধারণত support-এর সাহায্য লাগে — নিজের platform-এর “sensitive data সরানো” documentation দেখুন।

৭ · চাবি নিরাপদে রাখা — credential helper, token আর SSH key

পর্ব ১৪-এ দেখেছেন, HTTPS-এ token আর SSH-এ key দিয়ে remote-এ ঢুকতে হয়। এই চাবিগুলো কোথায় আর কতটা ক্ষমতা নিয়ে থাকে, সেটাই এখানে। HTTPS-এর token মনে রাখে credential helper:

Helperকোথায় রাখেমন্তব্য
managerOS-এর সুরক্ষিত ভান্ডার (Windows Credential Manager ইত্যাদি)Git Credential Manager; Windows-এর Git-এর সাথে সাধারণত আসে, অন্য OS-এও চলে। Browser-এ login করিয়ে token নিতে পারে।
osxkeychainmacOS KeychainmacOS-এ প্রচলিত।
libsecretLinux desktop-এর secret service (যেমন GNOME Keyring)কিছু distribution-এ আলাদা করে build বা install করতে হয়।
cacheMemory-তে, নির্দিষ্ট সময়ের জন্য (default ৯০০ সেকেন্ড)Disk-এ লেখে না; Unix-ধাঁচের system-এ।
store~/.git-credentials ফাইলে খোলা লেখায়ফাইল পড়তে পারলেই token পাওয়া যায় — backup, ভাগ করা কম্পিউটার বা ভুল permission-এ ঝুঁকি। Git-এর documentation-ও এই সতর্কতা দেয়।

Token নিজেও least privilege মেনে বানাতে হয়: শুধু দরকারি repository, শুধু দরকারি অনুমতি (পড়া, নাকি লেখাও), আর মেয়াদসহ। অনেক platform এখন repository-ভিত্তিক সূক্ষ্ম অনুমতির (fine-grained) token দেয় — নাম আর সুবিধা platform-ভেদে আলাদা।

SSH-এর দিকে কয়েকটি অভ্যাস:

  • নতুন key ed25519: ssh-keygen -t ed25519 -C "you@example.com". Ed25519 ছোট, দ্রুত আর আধুনিক; খুব পুরোনো server-এর জন্য RSA লাগলে অন্তত ৩০৭২ বা ৪০৯৬ bit (বর্তমান প্রচলিত পরামর্শ)।
  • Passphrase: private key ফাইলকে এনক্রিপ্ট করে; ssh-agent একবার খুলে memory-তে রাখে, তাই বারবার লিখতে হয় না।
  • ~/.ssh/config: host অনুযায়ী কোন key, তা ঠিক করা যায় — যেমন কাজের আর ব্যক্তিগত account-এর আলাদা key.
  • Deploy key: server বা automation-এর জন্য একটি repository-তে বাঁধা SSH key, সাধারণত শুধু পড়ার অনুমতি। ব্যক্তিগত key server-এ রাখার চেয়ে অনেক নিরাপদ, কারণ ফাঁস হলে ক্ষতি এক repository-তে সীমিত।
# ~/.ssh/config  (example; host names and paths are made up)
Host github-work
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_work
    IdentitiesOnly yes

# then use:  git clone git@github-work:example/bazar-app.git

Host github-work একটি ডাকনাম; Git যখন git@github-work:… দেখে, SSH আসল HostName-এ যায় কিন্তু শুধু IdentityFile-এর key ব্যবহার করে (IdentitiesOnly yes অন্য key চেষ্টা করা বন্ধ রাখে)। এটি config ফাইলের নমুনা, কোনো চালানো transcript নয়।

নিচের checker এই পর্বের নিয়মগুলো (কোনো আনুষ্ঠানিক মান নয়) কয়েকটি কাল্পনিক setup-এ প্রয়োগ করে:

// Credential setup checker. The RULES are this lesson's own teaching rules,
// not an official standard. Each setup is a made-up example.
function check(s) {
  const notes = [];
  if (s.helper === "store") notes.push("HIGH: 'store' keeps the token as plain text in ~/.git-credentials");
  if (/^https:\/\/[^\/@]+:[^\/@]+@/.test(s.remoteUrl)) notes.push("HIGH: token written inside the remote URL");
  if (s.helper === "cache") notes.push("info: 'cache' keeps it in memory only, for a timeout");
  if (["manager", "osxkeychain", "libsecret"].includes(s.helper)) notes.push("ok: OS-protected store");
  if (s.tokenScopes && s.tokenScopes.includes("admin")) notes.push("MEDIUM: token has admin scope; least privilege says no");
  if (s.tokenDays === null) notes.push("MEDIUM: token never expires");
  if (s.sshKey && !s.sshPassphrase) notes.push("MEDIUM: SSH private key has no passphrase");
  if (s.sshKey === "rsa-1024") notes.push("HIGH: 1024-bit RSA is too weak; use ed25519");
  return notes;
}
const setups = {
  "laptop A": { helper: "manager", remoteUrl: "https://github.com/example/bazar.git",
                tokenScopes: ["repo:read", "repo:write"], tokenDays: 90 },
  "laptop B": { helper: "store", remoteUrl: "https://rahim:FAKE_token_123@github.com/example/bazar.git",
                tokenScopes: ["admin"], tokenDays: null },
  "server C": { helper: "cache", remoteUrl: "git@github.com:example/bazar.git",
                sshKey: "ed25519", sshPassphrase: false },
  "old box D": { helper: "", remoteUrl: "git@github.com:example/bazar.git",
                 sshKey: "rsa-1024", sshPassphrase: true },
};
for (const [name, s] of Object.entries(setups)) {
  const notes = check(s);
  const worst = notes.some(n => n.startsWith("HIGH")) ? "FIX NOW" :
                notes.some(n => n.startsWith("MEDIUM")) ? "improve" : "fine";
  console.log(`${name}: ${worst}`);
  for (const n of notes) console.log("   " + n);
}

আউটপুট:

laptop A: fine
   ok: OS-protected store
laptop B: FIX NOW
   HIGH: 'store' keeps the token as plain text in ~/.git-credentials
   HIGH: token written inside the remote URL
   MEDIUM: token has admin scope; least privilege says no
   MEDIUM: token never expires
server C: improve
   info: 'cache' keeps it in memory only, for a timeout
   MEDIUM: SSH private key has no passphrase
old box D: FIX NOW
   HIGH: 1024-bit RSA is too weak; use ed25519

Laptop B চারটি নিয়মেই আটকেছে: token খোলা ফাইলে, আবার URL-এর ভেতরেও (git remote -v, screenshot বা log-এ দেখা যাবে), admin ক্ষমতা আর মেয়াদ নেই — ফাঁস হলে সবচেয়ে বড় ক্ষতি। Server C-র নোটটি প্রসঙ্গ-নির্ভর: automation-এর key-তে প্রায়ই passphrase রাখা যায় না; তখন সমাধান হলো সেটিকে শুধু-পড়ার deploy key বানানো আর server-এর ফাইল permission কড়া রাখা। Old box D-র passphrase আছে, কিন্তু ১০২৪-bit RSA দুর্বল বলে বিবেচিত, আর অনেক নতুন server ও tool এমন key গ্রহণই করে না। মনে রাখুন, নিয়মগুলো এই পর্বের শেখানোর জন্য বানানো; প্রতিষ্ঠানের নিজস্ব নীতি এর চেয়ে কড়া হতে পারে।

৮ · Git নিজেও শক্ত হচ্ছে — SHA-1, safe.directory আর খোলা .git folder

SHA-1 collision আর hardened SHA-1

Git প্রতিটি object-এর নাম দেয় তার বিষয়বস্তুর SHA-1 hash দিয়ে (পর্ব ৫২)। দুটি আলাদা বিষয়বস্তুর একই hash হলে তাকে বলে hash collision. ২০১৭ সালের ফেব্রুয়ারিতে Google আর CWI Amsterdam-এর গবেষকেরা “SHAttered” নামে প্রথম ব্যবহারিক SHA-1 collision প্রকাশ করেন — একই hash-এর দুটি আলাদা PDF. বিপুল computing লেগেছিল, তবু বোঝা গেল SHA-1-কে আর নিরাপদ ধরা যায় না।

Git-এর সাড়া দুই ধাপে। প্রথমত, Git ২.১৩ (২০১৭) থেকে default-এ “hardened SHA-1” (SHA-1DC নামের একটি library) ব্যবহার হয়, যা SHAttered-ধাঁচের collision আক্রমণের চিহ্ন hash করার সময়ই ধরতে পারে আর তখন object গ্রহণ করে না; সাধারণ data-র hash অপরিবর্তিত থাকে। দ্বিতীয়ত, দীর্ঘমেয়াদি সমাধান হিসেবে SHA-256 object format — git init --object-format=sha256 — যা কয়েক বছর ধরে ধাপে ধাপে পরিণত হচ্ছে; hosting platform আর অন্য tool-এ সমর্থন এখনো সব জায়গায় সমান নয়। বিস্তারিত পর্ব ৫২-এ। স্বাক্ষরের জন্য এর অর্থ: signed commit tree আর parent-কে hash দিয়ে ঢাকে, তাই স্বাক্ষরের ভরসা hash-এর শক্তির ওপরও নির্ভর করে।

safe.directory আর submodule-এর file protocol

Git-এর নিজের নিরাপত্তা-ত্রুটি (প্রকাশ্য ত্রুটির তালিকায় নম্বর পায়, যাকে বলে CVE) নিয়মিত সংশোধন হয়। দুটি উদাহরণ, যা আপনার দৈনন্দিন আচরণ বদলেছে:

  • CVE-2022-24765 → safe.directory: ভাগ করা কম্পিউটারে অন্য কোনো ব্যবহারকারী একটি ওপরের folder-এ .git বানিয়ে রাখলে আপনার Git সেটিকে নিজের repository ভেবে তার config মানতে পারত — সেই config-এ চালানোর মতো command থাকতে পারে। Git ২.৩৫.২ (এপ্রিল ২০২২) থেকে অন্য ব্যবহারকারীর মালিকানার repository-তে Git “dubious ownership” বলে কাজ থামায়, যতক্ষণ না আপনি নিজে git config --global --add safe.directory <path> দিয়ে বিশ্বাস জানান। Error এলে সবখানে '*' বসিয়ে দেওয়া সহজ সমাধান, কিন্তু সুরক্ষাটাই তাতে বন্ধ হয়ে যায় — শুধু নির্দিষ্ট, চেনা path যোগ করুন।
  • Git ২.৩৮.১ (অক্টোবর ২০২২) → protocol.file.allow: submodule-এ local file protocol default-এ আর অনুমোদিত নয় (ব্যবহারকারী নিজে সরাসরি চালালে চলে)। অচেনা repository-র submodule যেন নীরবে আপনার কম্পিউটারের অন্য repository টেনে না আনে। Submodule-এর কাজ পর্ব ১৯-এ (বড় repository).

শিক্ষা একটাই: Git হালনাগাদ রাখুন (git --version), আর release note-এর নিরাপত্তা-অংশ পড়ুন। পুরোনো টিউটোরিয়ালের “এই error এলে এই config দিন” পরামর্শ অনেক সময় একটি সুরক্ষা বন্ধ করে।

Web server-এ ভুল করে .git folder

একটি পুরোনো কিন্তু এখনো দেখা যাওয়া ভুল: website deploy করতে পুরো repository folder server-এর public folder-এ কপি, সাথে .git. তখন https://example.com/.git/… থেকে object ফাইল নামিয়ে পুরো history — পুরোনো secret-সহ — আবার জোড়া লাগানো সম্ভব হতে পারে। রক্ষার তিনটি অভ্যাস:

  • Repository নয়, artifact deploy করুন: build-এর ফল (HTML, bundle, binary) কপি করুন; .git, .env, notes কখনো public folder-এ নয়। Deployment pipeline পর্ব ৫৯-এ।
  • Server config-এ dot-file বন্ধ: ভুল হলেও যেন পরিবেশন না হয়।
  • যাচাই: deploy-এর পরে নিজের site-এ /.git/HEAD খুলে দেখুন — 404 বা 403 আসা উচিত।
# Apache (.htaccess or vhost): answer 404 for anything under /.git
RedirectMatch 404 /\.git

# nginx (inside the server block): refuse every dot-file except .well-known
location ~ /\.(?!well-known) {
    deny all;
}

দুটিই config-এর নমুনা, এখানে চালানো হয়নি — নিজের server-এর সংস্করণ অনুযায়ী documentation মিলিয়ে নিন। Apache-র লাইনটি যে path-এ কোথাও /.git আছে (যেমন /.git/HEAD, আবার /.gitignore-ও), সেখানে “নেই” (404) উত্তর দেয়, ফলে folder-এর অস্তিত্বও বোঝা যায় না। nginx-এর location সব dot-file (.git, .env, .htpasswd) আটকায়, কিন্তু .well-known ছাড় দেয়, যা TLS certificate নবায়নের মতো কাজে লাগে (TLS আর HTTPS). এটা দ্বিতীয় স্তর; প্রথম স্তর হলো .git server-এ না পাঠানো।

৯ · Supply chain — release-এর ওপর ভরসা গড়া

আপনার software শুধু আপনার কোড নয়: dependency, build tool, CI, release ফাইল — পুরো পথকে বলে software supply chain. Git-এর দিক থেকে এই পথে কয়েকটি তালা:

🏷️ Signed release tag

প্রতিটি release একটি signed tag (git tag -s v1.4.0); build বা deploy-এর আগে git verify-tag. পর্ব ১৮-এর release-ধারার সাথে মেলান। ব্যবহারকারীরাও যাচাই করতে পারেন।

🛡️ Branch protection

main-এ সরাসরি push আর force push নিষেধ, review আর সফল CI বাধ্যতামূলক, চাইলে signed commit বাধ্যতামূলক। Tag-এর জন্যও সুরক্ষা-নিয়ম (platform-ভেদে)।

👥 CODEOWNERS

CODEOWNERS দিয়ে সংবেদনশীল path (CI ফাইল, deploy script, auth কোড, CODEOWNERS নিজেই) বদলাতে নির্দিষ্ট মানুষের review বাধ্যতামূলক। পর্ব ১৬ — Pull request আর fork.

⚙️ CI আর hook

CI ফাইল বদলানো মানে কোন কোড কোন secret দেখবে, তা বদলানো — তাই সেটিও review-এর আওতায়। CI-এর secret আর অনুমতি পর্ব ৫৮-এ; local hook clone-এর সাথে আসে না, পর্ব ৫৬।

আরও গভীরে: Meli, McNiece ও Reaves-এর গবেষণা “How Bad Can It Git? Characterizing Secret Leakage in Public GitHub Repositories” (NDSS ২০১৯) public repository-তে ফাঁস হওয়া key-এর ব্যাপকতা আর দীর্ঘস্থায়িত্ব মেপেছে। SHAttered-এর বিবরণ Stevens ও সহকর্মীদের “The first collision for full SHA-1” (CRYPTO ২০১৭)-তে। Git-এর Documentation/technical/hash-function-transition নথি SHA-256-এ যাওয়ার নকশা ব্যাখ্যা করে। Supply chain-এর কাঠামো হিসেবে SLSA (Supply-chain Levels for Software Artifacts) আর keyless স্বাক্ষরের জন্য Sigstore প্রকল্প দেখার মতো — দুটিই চলমান উদ্যোগ, খুঁটিনাটি বদলায়।

পরীক্ষা ও ইন্টারভিউতে:

  • git commit -s আর -S-এর পার্থক্য? — -s শুধু Signed-off-by লেখা যোগ করে (সম্মতি, যাচাইযোগ্য নয়); -S GPG/SSH/X.509 key দিয়ে cryptographic স্বাক্ষর দেয়।
  • ভুল করে API key push করেছেন — প্রথম কাজ? — key বাতিল ও নতুন key; তারপর history পরিষ্কার, force push, দলকে re-clone, host-কে জানানো, log যাচাই।
  • পরের commit-এ ফাইল মুছলে কি secret চলে যায়? — না; পুরোনো commit-এ থাকে, git log -p -S বা git show <commit>:file দিয়ে পড়া যায়।
  • filter-branch না filter-repo? — Git-এর documentation filter-repo সুপারিশ করে: দ্রুত, কম ফাঁদ, নিজে backup ref আর reflog পরিষ্কার করে; তবে আলাদা install লাগে।
  • Credential helper store কেন ঝুঁকিপূর্ণ? — token খোলা লেখায় ~/.git-credentials-এ থাকে; OS-এর ভান্ডার (manager, osxkeychain, libsecret) ভালো।

১০ · AI-এর যুগে — নিরাপত্তার প্রশ্নে AI-কে যাচাই

“Secret push করে ফেলেছি, কী করব?” — AI assistant কয়েক সেকেন্ডে উত্তর দেবে। কিন্তু এই প্রশ্নে ভুল উত্তরের দাম বেশি, আর AI-এর কিছু চেনা ভুল আছে:

AI-এর ভুলকীভাবে ধরবেন
শুধু history পরিষ্কারের command দেয়, key বাতিলের কথা বলে নাউত্তরে revoke/rotate সবার আগে আছে কি না দেখুন; না থাকলে উত্তরটি অসম্পূর্ণ
“git rm করে commit দিন” — যেন তাতেই secret মোছেনিজে git log -p -S চালিয়ে দেখুন পুরোনো commit-এ আছে কি না
সতর্কতা ছাড়া push --force, reset --hard, বা shared branch-এ rewriteআগে একটি throwaway clone বা mirror-এ চালান; দলকে জানানোর ধাপ আছে কি না দেখুন
পুরোনো filter-branch recipe, বা filter-repo-র বানানো flaggit help filter-branch-এর সতর্কতা আর git filter-repo --help মিলিয়ে দেখুন; অজানা option-এ tool নিজেই error দেয়
“dubious ownership” error-এ safe.directory '*'শুধু নির্দিষ্ট চেনা path যোগ করুন; কেন error এল, আগে বুঝুন
SSH signing-এর ভুল config নাম বা সংস্করণ (যেমন “Git ২.২০ থেকে”)git --version, git help config-এ gpg.ssh অংশ
সমস্যা বোঝাতে আপনি নিজেই prompt-এ আসল .env, token বা private key পেস্ট করলেনপাঠানোর আগে সব secret REDACTED দিয়ে বদলান; ভুলে পাঠালে সেটিও ফাঁস — বাতিল করুন

নিচের prompt-এ আসল অবস্থা দেওয়া আছে, কিন্তু কোনো secret নয়, আর ক্রম ও যাচাই চাওয়া হয়েছে:

আমি ভুল করে একটি API key একটি Git repository-তে commit করে push করেছি। কোনো secret-এর আসল মান আমি এখানে দিচ্ছি না। Git version: [git --version-এর ফল] Repository public না private: [লেখো] কোন ফাইলে, কোন commit-এ এসেছে (git log -p -S "KEY_NAME" --format="%h %s"-এর শুধু hash আর বার্তা): [এখানে] কতজন সহকর্মী clone করেছেন, fork আছে কি না: [লেখো] Hosting platform: [নাম] ১. সঠিক ক্রমে ধাপগুলো বলো — key বাতিল আর নতুন key সবার আগে, তারপর history. ২. History পরিষ্কারের জন্য git filter-repo-র command দাও, প্রতিটি option কী করে ব্যাখ্যা করো; কোন option-গুলো তুমি নিশ্চিত, কোনগুলো documentation-এ মিলিয়ে নিতে হবে, তা আলাদা করো। ৩. Force push-এর আগে-পরে দলের জন্য কী নির্দেশনা দিতে হবে লেখো। ৪. কোথায় কোথায় কপি থেকে যেতে পারে (clone, fork, cache), আর host-কে কী জানাতে হবে বলো। ৫. শেষে কোন command দিয়ে যাচাই করব যে history-তে আর কিছু নেই, তা দাও।

AI-এর দেওয়া নিরাপত্তা-সমাধান হাতে পেলে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১সব commit sign করলেই কি একটি repository নিরাপদ? Signed commit আসলে কোন প্রশ্নের উত্তর দেয়, আর কোনটির দেয় না?

Signed commit একটি নির্দিষ্ট প্রশ্নের উত্তর দেয়: “এই commit-এর লেখা কি ওই key-র মালিক বানিয়েছেন, আর তারপর কি কিছু বদলেছে?” এটা মূল্যবান। ২০২১ সালে PHP-র Git server-এ পরিচিত রক্ষণাবেক্ষণকারীদের নামে ক্ষতিকর commit ঢোকানো হয়েছিল; বাধ্যতামূলক যাচাই থাকলে সেই commit-গুলো “স্বাক্ষর নেই” বা “অচেনা key” হিসেবে আলাদা করে চোখে পড়ত। ২০১১ সালে kernel.org-এর server-গুলোতে অনুপ্রবেশের পরে Linux kernel সম্প্রদায় রক্ষণাবেক্ষণকারীদের PGP key আর signed tag-এর ওপর জোর বাড়ায় বলে জানা যায় — কারণ Git-এর hash আর স্বাক্ষর মিলে নিশ্চিত করে যে server দখল হলেও যাচাই করা history চুপিচুপি বদলানো যায় না।

কিন্তু স্বাক্ষর অনেক প্রশ্নের উত্তর দেয় না। প্রথমত, key-এর মালিক কে — সেটা স্বাক্ষর জানে না; allowed signers ফাইল, platform-এ তোলা key বা web of trust সেই সম্পর্ক বানায়, আর সেই তালিকাই তখন আক্রমণের লক্ষ্য। এজন্য allowed signers ফাইল review-সহ বদলানো আর CODEOWNERS-এ রাখা যুক্তিসংগত। দ্বিতীয়ত, key চুরি হলে চোর নিখুঁত স্বাক্ষর বানাতে পারেন — passphrase, hardware key আর দ্রুত বাতিলের ব্যবস্থা তাই দরকারি। তৃতীয়ত, স্বাক্ষর কোডের মান বা উদ্দেশ্য সম্পর্কে কিছু বলে না: একজন বিশ্বস্ত মানুষও ভুল বা ক্ষতিকর কোড sign করতে পারেন। ২০২৪ সালে xz-utils-এর ঘটনায় প্রকাশ্যে জানা গেছে, একজন অবদানকারী বছরের পর বছর আস্থা অর্জন করে রক্ষণাবেক্ষণের অধিকার পেয়েছিলেন; তাঁর পরিচয় “সঠিক” হলেও কাজটি ক্ষতিকর ছিল।

আরেকটি বাস্তব দিক: সবাই sign করলে কিন্তু কেউ যাচাই না করলে লাভ প্রায় শূন্য। যাচাই হতে হবে স্বয়ংক্রিয় — CI-তে git verify-commit বা verify-tag-এর exit code, বা platform-এর “signed commit বাধ্যতামূলক” নিয়ম। Release-এর tag যাচাই না করে build করলে স্বাক্ষর শুধু সাজসজ্জা।

মূল উপলব্ধি: স্বাক্ষর পরিচয় আর অখণ্ডতার প্রমাণ, বিশ্বাসযোগ্যতার নয়। এর সাথে key-এর সুরক্ষা, স্বয়ংক্রিয় যাচাই আর মানুষের review মিলেই নিরাপত্তা।

প্র ০২“Secret ফাঁস হলে আগে বাতিল, পরে history পরিষ্কার” — অনেকে উল্টোটা করেন কেন, আর উল্টো ক্রমে কী ঝুঁকি?

মানুষের স্বাভাবিক প্রবৃত্তি হলো ভুলটি লুকানো: “কেউ দেখার আগেই মুছে ফেলি।” Key বাতিল করা মানে স্বীকার করা, অন্য দলকে জানানো, হয়তো production-এ কিছুক্ষণের বিভ্রাট — অস্বস্তিকর। History পরিষ্কার মনে হয় নিজের হাতে, চুপচাপ করা যায়। কিন্তু এই ক্রম ধরে নেয় যে secret এখনো কেউ দেখেনি, আর সেই অনুমান যাচাই করার কোনো উপায় আপনার নেই।

Public repository-তে push হওয়া key খুব দ্রুত স্বয়ংক্রিয় scanner-এর হাতে পড়তে পারে। Meli ও সহকর্মীদের NDSS ২০১৯-এর গবেষণা public repository-তে বিপুল সংখ্যক ফাঁস হওয়া key পেয়েছিল, আর দেখিয়েছিল অনেক key দীর্ঘদিন কাজের অবস্থায় থাকে। কিছু platform আর provider মিলে স্বয়ংক্রিয়ভাবে কিছু ধরনের key খুঁজে provider-কে জানায় বা বাতিল করে — কোনো কোনো সময় আপনার আগেই। Private repository-ও নিরাপদ ধরা যায় না: account দখল, প্রাক্তন সহকর্মী, ভুল অনুমতি, CI log বা backup দিয়ে কপি বেরোতে পারে। যুক্তরাষ্ট্রের FTC-র ২০১৮ সালের একটি পদক্ষেপের বিবরণে বলা হয়েছিল, ২০১৬ সালে Uber-এর একটি data breach-এ অনুপ্রবেশকারীরা একটি private repository-তে থাকা cloud access key ব্যবহার করেছিল (খুঁটিনাটি সরকারি নথিতে)।

উল্টো ক্রমের আরেকটি বিপদ সময়। History পরিষ্কার, force push আর দলের সবার re-clone — কয়েক ঘণ্টা বা দিন লাগে, আর অংশ ৬-এ দেখেছেন, fork আর cache-এ কপি থেকেই যেতে পারে। এই পুরো সময় key কাজের অবস্থায়। বাতিল করতে লাগে কয়েক মিনিট, আর তার পরে সব কপি একসাথে মূল্যহীন।

History পরিষ্কার তবু দরকারি: scanner বারবার সতর্ক করবে, নতুন যোগ দেওয়া মানুষ পুরোনো pattern শিখবেন, আর কখনো কখনো ফাঁস হওয়া তথ্য বাতিলযোগ্য নয় (যেমন ব্যক্তিগত data). তখন পরিষ্কার আর host-এর সাহায্যই ভরসা।

মূল উপলব্ধি: নিরাপত্তায় “কেউ দেখেনি” অনুমান নয়, “সবাই দেখেছে” ধরে পরিকল্পনা করতে হয়। বাতিল সেই অনুমানেও কাজ করে; পরিষ্কার করে না।

প্র ০৩Git-এর নকশায় SHA-1 নিরাপত্তার জন্য ছিল, না অখণ্ডতার জন্য? SHAttered-এর পরে Git কেন একবারে SHA-256-এ চলে গেল না?

২০০৫ সালে Linus Torvalds যখন Git লেখেন, SHA-1 তখনো ব্যাপকভাবে ব্যবহৃত ছিল। তিনি বিভিন্ন সময় বলেছেন, Git-এ hash-এর মূল কাজ হলো object-কে নাম দেওয়া আর দুর্ঘটনাজনিত নষ্ট হওয়া (disk-এর ভুল, network-এর গোলমাল) ধরা — অর্থাৎ অখণ্ডতা; নিরাপত্তা আসবে মানুষের আস্থা আর signed tag থেকে। কিন্তু বাস্তবে hash-ই signed tag-এর ভিত্তি: স্বাক্ষর একটি commit hash-কে ঢাকে, আর সেই hash পুরো tree আর history-কে। Hash দুর্বল হলে স্বাক্ষরের অর্থও দুর্বল হয়।

২০০৫ সালেই গবেষকেরা SHA-1-এর তাত্ত্বিক দুর্বলতা দেখান, আর ২০১৭ সালের SHAttered প্রথম ব্যবহারিক collision দেয়; ২০২০ সালের দিকে “chosen-prefix” collision আরও সস্তা হয় বলে গবেষণায় জানানো হয়। Git-এর তাৎক্ষণিক সাড়া ছিল hardened SHA-1: collision বানানোর পরিচিত কৌশলের চিহ্ন খুঁজে সন্দেহজনক object বাতিল করা, সাধারণ data-র hash না বদলে। এতে একটি কোডও না বদলে সব পুরোনো repository একই hash রেখে সুরক্ষা পায়।

একবারে SHA-256-এ যাওয়া কঠিন কারণ hash Git-এর সব জায়গায়: object-এর নাম, pack format, network protocol, commit বার্তায় উল্লেখ করা hash, CI-র cache, hosting platform-এর database, অসংখ্য tool. লাখো repository আর তাদের সব clone একদিনে বদলানো অসম্ভব। তাই Git-এর transition নথি একটি ধাপে ধাপে পথ বর্ণনা করে, যেখানে SHA-256 repository পুরোনো SHA-1 পক্ষের সাথেও কাজ করতে পারবে, প্রয়োজনে দুই hash-এর মানচিত্র রেখে। SHA-256 repository বানানো Git ২.২৯ থেকে পরীক্ষামূলকভাবে সম্ভব হয়, পরে আরও পরিণত হয়েছে; তবে platform-গুলোর সমর্থন লেখার সময়ে সমান নয় (বিস্তারিত পর্ব ৫২)।

মূল উপলব্ধি: একটি মৌলিক নকশা-সিদ্ধান্ত (hash function) বদলানো ব্যবহারকারী-ভিত্তি যত বড়, তত ধীর। তাই প্রথমে ক্ষতি ঠেকানোর তাৎক্ষণিক বর্ম, পরে ধীর কিন্তু সামঞ্জস্যপূর্ণ রূপান্তর।

প্র ০৪Token খোলা লেখায় রাখা store helper এত ঝুঁকিপূর্ণ হলে Git সেটা রেখেছে কেন? “নিরাপদ” আর “ব্যবহারযোগ্য”-র টানাপোড়েন কীভাবে মেটাবেন?

Git সব ধরনের পরিবেশে চলে: ব্যক্তিগত ল্যাপটপ, desktop ছাড়া Linux server, container, পুরোনো embedded system. সবখানে OS-এর সুরক্ষিত ভান্ডার নেই। Server-এ বা container-এ libsecret-এর মতো desktop service থাকে না, আর cache restart-এ হারায়। Git-এর রক্ষণাবেক্ষণকারীরা তাই একটি সরল, সর্বত্র চলা বিকল্প রেখেছেন — আর documentation-এ স্পষ্ট লিখেছেন যে এটি খোলা লেখায় রাখে। একটি tool সবার হাতে সিদ্ধান্ত ছেড়ে দিলে সতর্কবার্তা দেওয়াই তার দায়িত্ব।

ঝুঁকি বুঝতে threat model ভাবুন: কার কাছ থেকে রক্ষা করছেন? যিনি আপনার user account দিয়ে কম্পিউটারে ঢুকতে পারেন, তিনি সম্ভবত অন্য পথেও token পাবেন — browser-এর session, ssh-agent. কিন্তু ঝুঁকি তার বাইরেও: ফাইলটি backup-এ চলে যায়, dotfiles repository-তে ভুলে commit হয় (পর্ব ১০-এর global ignore মনে করুন), ভাগ করা কম্পিউটারে ভুল permission থাকে, বা ল্যাপটপ চুরি হয় এনক্রিপ্ট না করা disk-সহ। OS-এর ভান্ডার এসব ক্ষেত্রে আরেকটি স্তর যোগ করে।

টানাপোড়েন মেটানোর পথ হলো ক্ষতির পরিধি ছোট করা। Automation-এ ব্যক্তিগত token নয়, শুধু-পড়ার deploy key বা CI platform-এর দেওয়া স্বল্পমেয়াদি token. সত্যিই store লাগলে সেই token-এর ক্ষমতা একটি repository আর কম মেয়াদে সীমিত, ফাইলের permission শুধু নিজের জন্য, disk এনক্রিপ্ট। আর নিরাপদ পথকে সহজ করা: Git Credential Manager-এর মতো tool browser-এ login করিয়ে token নিজেই সামলায় — মানুষ যখন নিরাপদ পথটাকেই সহজ পান, তখন অনিরাপদ পথে যান না। নিরাপত্তা-গবেষণায় বারবার দেখা গেছে, কঠিন নিয়ম মানুষ এড়িয়ে চলার উপায় খোঁজেন।

মূল উপলব্ধি: “নিখুঁত নিরাপদ” প্রায়ই অব্যবহারযোগ্য। সঠিক প্রশ্ন: ফাঁস হলে কতটা ক্ষতি, আর নিরাপদ পথটি কি সবচেয়ে সহজ পথও?

প্র ০৫২০২৪ সালের xz-utils ঘটনা থেকে Git-ভিত্তিক release-প্রক্রিয়ার জন্য কী শেখার আছে? Repository আর release ফাইল এক না হলে কী হয়?

২০২৪ সালের মার্চে একজন developer কিছু system-এ SSH-এর অস্বাভাবিক ধীরগতি লক্ষ করে খুঁজতে গিয়ে xz-utils compression library-তে একটি গোপন backdoor আবিষ্কার করেন (CVE-2024-3094). প্রকাশ্য বিশ্লেষণ অনুযায়ী, একজন অবদানকারী কয়েক বছর ধরে সাহায্য করে আস্থা অর্জন করেছিলেন আর রক্ষণাবেক্ষণের অধিকার পেয়েছিলেন। ক্ষতিকর অংশের একটি বড় দিক ছিল: release হিসেবে প্রকাশিত tarball-এ build-এর একটি ধাপ বদলানো ছিল, যা repository-র সংশ্লিষ্ট ফাইলে ছিল না; আর আসল payload লুকানো ছিল “test data” নামের binary ফাইলে। কয়েকটি distribution-এর পরীক্ষামূলক সংস্করণে পৌঁছালেও ব্যাপক ছড়ানোর আগেই ধরা পড়ে (বিস্তারিত বিভিন্ন বিশ্লেষণে আলাদা)।

Git-এর দিক থেকে কয়েকটি শিক্ষা। প্রথমত, repository আর release ফাইলের পার্থক্য: অনেকে tarball বিশ্বাস করেন কারণ সেটা “অফিসিয়াল”, কিন্তু তা review করা history থেকে আসেনি। রক্ষার উপায় হলো release ফাইল signed tag থেকে পুনরুৎপাদনযোগ্যভাবে (reproducible build) বানানো, আর distribution-গুলোর পক্ষে tag থেকে নিজে build করে মিলিয়ে দেখা। দ্বিতীয়ত, review-এর অন্ধ জায়গা: binary test ফাইল, build script আর CI config মানুষ কম পড়েন — এগুলোর জন্য CODEOWNERS আর বাড়তি সতর্কতা। তৃতীয়ত, স্বাক্ষরের সীমা: প্রশ্ন ০১-এ দেখেছেন, বিশ্বস্ত পরিচয় ক্ষতিকর উদ্দেশ্য ঠেকায় না। চতুর্থত, মানবিক দিক: একা, ক্লান্ত রক্ষণাবেক্ষণকারীর ওপর চাপ সৃষ্টি করে অধিকার হস্তান্তরের ঝুঁকি — তাই গুরুত্বপূর্ণ project-এ একাধিক রক্ষণাবেক্ষণকারী আর অর্থায়নের প্রশ্ন।

Ken Thompson তাঁর Turing Award বক্তৃতা “Reflections on Trusting Trust”-এ (১৯৮৩ সালে দেওয়া, ১৯৮৪-তে প্রকাশিত) দেখিয়েছিলেন, compiler নিজেই দূষিত হলে source code পড়ে কিছু ধরা যায় না — শেষ পর্যন্ত কাউকে না কাউকে বিশ্বাস করতেই হয়। Supply chain নিরাপত্তা তাই বিশ্বাস দূর করা নয়, বিশ্বাসকে ছোট, যাচাইযোগ্য টুকরোয় ভাগ করা: signed tag, পুনরুৎপাদনযোগ্য build, সংরক্ষিত branch, একাধিক reviewer.

মূল উপলব্ধি: যা ব্যবহারকারীর হাতে পৌঁছায়, তা যেন যাচাই করা history থেকেই আসে — আর সেই যোগসূত্র যেকোনো কেউ মিলিয়ে দেখতে পারেন।

অনুশীলন

প্রথমে নিজে উত্তর লিখুন, তারপর মিলিয়ে নিন। Git-এর কাজ চেষ্টা করলে একটি খালি পরীক্ষার folder আর বানানো secret ব্যবহার করুন।

  1. আউটপুট অনুমান করুন: একটি খেলনা scanner diff-এর শুধু + দিয়ে শুরু লাইন দেখে। নিয়ম: (ক) AKIA + ১৬টি বড় হাতের অক্ষর/অঙ্ক হলে “aws-style-key-id”; (খ) password = "…" হলে “password-assignment”; (গ) =-এর পরের অংশে ২০ বা তার বেশি অক্ষরের টুকরোর Shannon entropy সীমা ৩.৫-এর সমান বা বেশি হলে “high-entropy”. Diff-এর লাইনগুলো (entropy বন্ধনীতে): ১ +DB_HOST=localhost; ২ +AWS_ACCESS_KEY_ID=AKIAFAKEEXAMPLE00001 (৩.১৮); ৩ +PAYMENT_API_KEY=FAKE_sk_test_9fQ2xLm4Rt7Vb1Nz (৪.৪৬); ৪ +password = "changeme"; ৫ +COMMIT_ID= একটি ৪০ অক্ষরের hex commit id (৩.৯২); ৬ +GREETING=assalamualaikum_bandhu (৩.২৭); ৭ -OLD_KEY=…. কোন কোন লাইন, কোন নিয়মে ছাপা হবে? কোনটি false positive?

    আসল প্রোগ্রাম সীমা ৩.৫ দিয়ে চালিয়ে পাওয়া ফল:

    line 2: aws-style-key-id       AWS_ACCESS_KEY_ID=AKIAFAKEEXAMPLE00001
    line 3: high-entropy 4.46      FAKE_sk_test_9fQ2xLm4Rt7
    line 4: password-assignment    password = "changeme"
    line 5: high-entropy 3.92      3f9a1c2e8b7d6a5f4e3c2b1a

    লাইন ১-এ localhost ছোট (২০-এর কম); লাইন ২ ধাঁচে ধরা পড়ে, entropy ৩.১৮ < ৩.৫ বলে entropy-নিয়মে নয়; লাইন ৩ entropy-তে; লাইন ৪ ধাঁচে (“changeme” ২০ অক্ষরের কম); লাইন ৫-এর commit id এবার ৩.৯২ ≥ ৩.৫ — false positive, কারণ commit id গোপন নয়; লাইন ৬ ৩.২৭ < ৩.৫ বলে বাদ; লাইন ৭ মুছে ফেলা লাইন, দেখাই হয় না। সীমা ৪.০-এ লাইন ৫ বাদ পড়ত। শিক্ষা: সীমা কমালে বেশি ধরা পড়ে, কিন্তু অপ্রয়োজনীয় সতর্কতাও বাড়ে।

  2. ক্রম সাজান আর command লিখুন: একটি কাল্পনিক দলের public repository-তে গতকাল একটি payment API-এর test key (config/payment.yml-এর ভেতরে, ফাইলটি দরকারি) push হয়েছে; তিনজন সহকর্মী clone করেছেন, একটি fork আছে। এলোমেলো তালিকা: (A) সবাইকে re-clone-এর নির্দেশ ও host-কে জানানো, (B) নতুন key বানিয়ে secret manager-এ বসানো, (C) নতুন clone-এ history থেকে key-এর লেখা সরানো, (D) provider-এর dashboard-এ key বাতিল, (E) নতুন clone-এ পুরো history খুঁজে যাচাই আর provider-এর log দেখা, (F) সব branch ও tag force push. সঠিক ক্রম কী? (C), (E) আর (F)-এর command লিখুন।

    ক্রম: D → B → C → F → A → E. বাতিল সবার আগে, কারণ গতকাল থেকে public — ধরে নিতে হবে কেউ দেখেছেন। নতুন key তার পরপরই (অনেক দল D আর B প্রায় একসাথে করে, যাতে app কম সময় থামে)। ফাইলটি দরকারি, তাই ফাইল মোছা নয়, লেখা বদলানো — --replace-text:

    # C: fresh clone, replace the secret text everywhere
    git clone https://github.com/example/bazar-app.git clean && cd clean
    printf 'FAKE_old_payment_key_value\n' > ../replacements.txt
    git filter-repo --replace-text ../replacements.txt
    git log -p -S "FAKE_old_payment_key_value" --all     # expect: nothing
    
    # F: after telling the team; re-add origin (filter-repo removes it)
    git remote add origin https://github.com/example/bazar-app.git
    git push --force --all origin
    git push --force --tags origin
    
    # E: in another brand-new clone
    git clone https://github.com/example/bazar-app.git verify && cd verify
    git log -p -S "FAKE_old_payment_key_value" --all
    git grep -n "FAKE_old_payment_key_value" $(git rev-list --all)

    বাস্তবে replacements.txt-এ আসল পুরোনো মান থাকবে, তাই ফাইলটি repository-র বাইরে রাখুন আর পরে মুছুন। Fork-এর মালিককে আলাদা জানাতে হবে, আর platform-এর cache বা pull request ref-এর জন্য support. Push-এর আগে protected branch-এর force push নিষেধাজ্ঞা সাময়িক তুলতে হতে পারে, পরে আবার চালু। এই command-গুলো উদাহরণ; filter-repo-র option নিজের সংস্করণের documentation-এ মিলিয়ে নিন।

  3. হিসাব করুন: (ক) “aaab” লেখাটির Shannon entropy কত bit/অক্ষর? (খ) “abcd”-র? (গ) ৪০ অক্ষরের একটি hex লেখার সর্বোচ্চ মোট entropy কত bit? (ঘ) ৬২ রকম অক্ষর (a–z, A–Z, 0–9) থেকে সম্পূর্ণ এলোমেলোভাবে বাছা ২০ অক্ষরের একটি token-এ কত bit? (ঙ) কেন একটি সাধারণ ইংরেজি বাক্যের entropy প্রতি অক্ষর সাধারণত কম?

    (ক) p(a) = ৩/৪, p(b) = ১/৪. H = −(০.৭৫ × log₂০.৭৫ + ০.২৫ × log₂০.২৫) = −(০.৭৫ × (−০.৪১৫) + ০.২৫ × (−২)) = ০.৩১১ + ০.৫ ≈ ০.৮১১ bit/অক্ষর.

    (খ) চার অক্ষর সমান ভাগে, প্রতিটি ১/৪: H = log₂৪ = ২ bit/অক্ষর — n রকম অক্ষর সমানভাবে থাকলে H = log₂n, যা সর্বোচ্চ।

    (গ) Hex-এ ১৬ রকম অক্ষর, সর্বোচ্চ ৪ bit/অক্ষর; ৪০ × ৪ = ১৬০ bit — ঠিক SHA-1-এর দৈর্ঘ্য। (একটি নির্দিষ্ট ৪০ অক্ষরের লেখায় মাপা entropy সাধারণত একটু কম আসে, যেমন খেলনা scanner-এ ৩.৯২, কারণ ৪০টি অক্ষরে ১৬টি সমানভাবে ছড়ানো সম্ভব নয়।)

    (ঘ) প্রতিটি অক্ষরে log₂৬২ ≈ ৫.৯৫৪ bit; ২০ × ৫.৯৫৪ ≈ ১১৯.১ bit. অনুমান করে মেলানো কার্যত অসম্ভব — তাই এমন token-এর নিরাপত্তা নির্ভর করে শুধু গোপন থাকার ওপর।

    (ঙ) ভাষায় কিছু অক্ষর (e, a, ফাঁকা জায়গা) অনেক বেশি আসে আর অনেক অক্ষর কম, তাই p(c) অসমান, H কম। এই পার্থক্যই scanner কাজে লাগায়; আবার “changeme”-এর মতো দুর্বল কিন্তু ছোট password-কে entropy ধরতে পারে না।

  4. সমালোচনা করুন: একটি কাল্পনিক startup-এর “Git নিরাপত্তা নীতি”: (১) “Repository private, তাই config-এ key রাখা চলবে।” (২) “ভুলে key commit হলে পরের commit-এ মুছে দেবেন।” (৩) “সবার ল্যাপটপে credential.helper store, যাতে password বারবার না চায়।” (৪) “Release tag সবাই বানাতে পারেন; sign করার ঝামেলা নেই।” (৫) “Deploy-এর জন্য পুরো repository server-এর public folder-এ git pull করা হবে।” (৬) “Pre-commit hook বিরক্ত করলে --no-verify দেবেন।” প্রতিটির সমস্যা আর ভালো বিকল্প লিখুন।
    • (১) Private মানে “কম মানুষ দেখে”, “কেউ দেখে না” নয় — প্রাক্তন কর্মী, ভুল অনুমতি, account দখল, CI log, backup. Repository একদিন public হতেও পারে। ভালো: secret কোডে নয়; environment variable আর secret manager, .env ignore, .env.example commit.
    • (২) পরের commit-এ মুছলে পুরোনো commit-এ থাকে (git log -p -S). ভালো: “key বাতিল ও নতুন key → push না হলে amend/rebase, push হলে filter-repo, force push, re-clone, host-কে জানানো।”
    • (৩) store token খোলা লেখায় রাখে। ভালো: OS-এর helper (manager, osxkeychain, libsecret), সীমিত scope ও মেয়াদের token, বা passphrase-সহ ed25519 SSH key.
    • (৪) যে কেউ release tag বানাতে পারলে জাল release চেনার উপায় নেই। ভালো: শুধু release-দায়িত্বপ্রাপ্তরা git tag -s, platform-এ tag সুরক্ষা, build-এর আগে CI-তে git verify-tag.
    • (৫) Public folder-এ .git থাকলে history (পুরোনো secret-সহ) নামানো সম্ভব হতে পারে। ভালো: build artifact deploy, server config-এ dot-file বন্ধ, deploy-এর পরে /.git/HEAD-এ 404/403 যাচাই; server-এ pull দরকার হলে public folder-এর বাইরে, শুধু-পড়ার deploy key দিয়ে।
    • (৬) একটি স্তর ইচ্ছে করে বন্ধ করা। ভালো: false positive হলে tool-এর allowlist বা baseline-এ কারণসহ যোগ; hook ধীর হলে শুধু staged ফাইল scan; আর server-এর push protection ও CI scan রাখা, কারণ client-এর hook সবসময় এড়ানো যায়।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমKey আগেই বাতিল আর বদলানো হয়েছে। এবার push হওয়া secret history থেকে সরানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. দলকে জানিয়ে push থামানো, তারপর একটি নতুন clone নেওয়া
  2. নতুন clone-এ git filter-repo --replace-text চালানো
  3. git log -p -S দিয়ে দেখা যে history-তে আর কিছু নেই
  4. সব branch আর tag force push করা
  5. সহকর্মীদের পুরোনো clone মুছে নতুন clone করানো

একটি repository-তে commit-এর author দেখাচ্ছে “Nasrin <nasrin@example.com>”. কোন তথ্যটি প্রমাণ করতে পারে যে commit-টি সত্যিই নাসরিনের key দিয়ে বানানো?

  • Commit-এর email নাসরিনের email-এর সাথে মেলে
  • বার্তার শেষে “Signed-off-by: Nasrin” লাইন
  • git verify-commit সফল, আর key-টি allowed signers ফাইলে নাসরিনের নামে আছে
  • Commit-টি push করা হয়েছে একটি যাচাই করা account থেকে

git log --format="%h %G? %s"-এ একটি commit-এর পাশে U দেখাচ্ছে, আর git verify-commit বলছে “Good … signature … No principal matched.” এর অর্থ কী?

  • Commit-এ কোনো স্বাক্ষর নেই
  • স্বাক্ষর গাণিতিকভাবে ঠিক, কিন্তু key-টি কার, তা বিশ্বস্ত তালিকায় নেই
  • Commit-এর লেখা স্বাক্ষরের পরে বদলানো হয়েছে
  • Key-টির মেয়াদ শেষ

একজন developer একটি public repository-তে ভুলে cloud API key push করেছেন। পরের ধাপগুলোর মধ্যে কোনটি সবার আগে করা উচিত?

  • Provider-এ key বাতিল (আর নতুন key বানানো)
  • git filter-repo দিয়ে history পরিষ্কার
  • ফাইলটি মুছে নতুন commit push করা
  • Repository private করে দেওয়া

কোন credential helper HTTPS token-কে disk-এ খোলা লেখায় (plain text) রাখে?

  • manager
  • osxkeychain
  • cache
  • store

git filter-branch --index-filter "git rm --cached --ignore-unmatch .env" -- --all চালানোর পরে git grep "API_KEY" $(git rev-list --all) এখনো পুরোনো commit-এ key খুঁজে পাচ্ছে। সবচেয়ে সম্ভাব্য কারণ কী?

  • filter-branch শুধু বর্তমান branch বদলায়
  • --index-filter ফাইল সরাতে পারে না
  • পুরোনো commit এখনো refs/original/ backup ref আর reflog থেকে পৌঁছানো যায়
  • Remote থেকে fetch করা হয়নি

একটি ভাগ করা Linux server-এ git status দিলে “detected dubious ownership in repository” error আসে। নিরাপত্তার দিক থেকে সবচেয়ে ভালো সাড়া কোনটি?

  • git config --global --add safe.directory '*'
  • মালিক কে আর কেন, তা যাচাই করে শুধু সেই নির্দিষ্ট path safe.directory-তে যোগ করা
  • Git-এর পুরোনো সংস্করণ install করা
  • প্রতিবার sudo git status চালানো

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. History প্রায় চিরস্থায়ী: পরের commit-এ মুছলেও পুরোনো commit-এ থাকে; push হলে প্রতিটি clone একটি পূর্ণ কপি।
  2. Author শুধু লেখা, স্বাক্ষর প্রমাণ: commit -S, tag -s; SSH-এ gpg.format ssh, user.signingkey, gpg.ssh.allowedSignersFile; যাচাই verify-commit/verify-tag আর %G?.
  3. ঠেকানো স্তরে স্তরে: .gitignore + environment variable + secret manager, pre-commit scanner, push protection, CI scan. Scanner = ধাঁচ + entropy, false positive ও negative দুটোই আছে।
  4. ফাঁস হলে: revoke → rotate → filter-repo → force push → re-clone ও host-কে জানানো → যাচাই। filter-branch-এর পরে backup ref, reflog আর gc না সামলালে secret থেকে যায়।
  5. চাবি আর Git হালনাগাদ: store নয়, OS-এর helper; সীমিত token; passphrase-সহ ed25519, deploy key. Git হালনাগাদ রাখুন, safe.directory '*' নয়, public folder-এ .git নয়।

পরের পর্বে: আজ বারবার “CI-তে scan”, “CI-তে verify-tag” এসেছে। পর্ব ৫৮-এ continuous integration: pipeline কীভাবে প্রতিটি push-এ build আর test চালায়, দ্রুত feedback কেন জরুরি, আর CI-তে secret কীভাবে নিরাপদে রাখবেন।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান