এই পর্বে যা শিখবেন
- Secret scan থেকে deploy-এর সময় সই যাচাই পর্যন্ত কোন পরীক্ষা কোথায় বসে, আর কেন
- False positive সামলে warn আর block-এর নিয়ম বানানো
- CI system-এর threat model, SHA pinning আর সীমিত job token
- OIDC federation, token-এর claim আর repository ও branch বেঁধে দেওয়া trust policy
- SLSA-র স্তর, provenance, Sigstore আর SBOM-এর আসল ব্যবহার
- আক্রান্ত dependency-তে সাড়া দেওয়া, Dependabot বা Renovate আর MTTR মাপা
১ · নিরাপত্তা পথের অংশ, শেষের গেট নয়
বিয়েবাড়ির ক্যাটারিং ভাবুন। বাবুর্চি যদি শুধু পরিবেশনের ঠিক আগে খাবার চেখে দেখেন, আর তখন বোঝেন মাছ পচা, তাহলে কিছুই করার থাকে না। ভালো বাবুর্চি বাজারে মাছ দেখেন, রান্নার সময় লবণ চাখেন, পরিবেশনের আগে আরেকবার দেখেন। প্রতিটি পরীক্ষা ছোট, কিন্তু একসাথে তারা বড় বিপদ ঠেকায়।
পুরোনো পদ্ধতিতে software শেষ হওয়ার পর একটি security দল release-এর আগে কয়েক সপ্তাহ ধরে পরীক্ষা করত। দিনে কয়েকবার deploy হলে সেই পদ্ধতি চলে না। DevSecOps মানে নিরাপত্তার কাজ দৈনন্দিন delivery-র পথের ভেতরে বসানো, যাতে প্রতিটি পরিবর্তন নিজে থেকেই পরীক্ষা হয়। একে বলে shift left: পরীক্ষা কাজের শুরুর দিকে আনা।
DevSecOps-এ security দল সব পরীক্ষা নিজে করে না। তারা নিয়ম, tool আর নিরাপদ default বানায়। পরীক্ষা চলে যন্ত্রে, আর ফল পৌঁছায় যিনি কোড লিখেছেন তাঁর কাছে, তখনই। দায় ভাগ হয়, নিরাপত্তা বিশেষজ্ঞের প্রয়োজন কমে না।
দুটি ভুল ধারণা আগেই সরিয়ে রাখা ভালো। প্রথমত, DevSecOps কোনো tool নয়, scanner কিনলেই তা হয় না। দ্বিতীয়ত, shift left মানে ডানদিক খালি করা নয়। কিছু সমস্যা শুধু চলন্ত সিস্টেমেই দেখা যায়, তাই production-এ পাহারা, audit log আর detection আগের মতোই লাগে। নামটি নিয়েও মতভেদ আছে। কেউ SecDevOps বলেন, কেউ বলেন DevOps-এর মধ্যেই নিরাপত্তা থাকার কথা, আলাদা নামের দরকার নেই। পর্ব ৭-এ DevOps culture দেখেছেন। এটি তারই সম্প্রসারণ।
“DevSecOps” শব্দটি ২০১০-এর দশকে ছড়ায়। মান হিসেবে দুটি নথি কাজে লাগে: NIST SP 800-218, Secure Software Development Framework (২০২২), আর CI/CD-তে supply chain নিরাপত্তা নিয়ে NIST SP 800-204D (২০২৪)। আর OWASP ২০২২ সালের দিকে CI/CD-র ঝুঁকির একটি Top 10 তালিকা প্রকাশ করে।
২ · Pipeline-এর কোন ধাপে কোন পরীক্ষা
পর্ব ৫৭-এ pipeline-এর গঠন আর গতি দেখেছেন। এবার তার ওপর নিরাপত্তার মানচিত্র বসাই। নিয়ম সহজ: যে পরীক্ষা যত দ্রুত আর সস্তা, সেটি তত আগে। যা চলন্ত সিস্টেম ছাড়া সম্ভব নয়, সেটি পরে।
Pipeline-এর প্রতিটি ধাপে নিরাপত্তা পরীক্ষা
Security checks at each pipeline stage
| পরীক্ষা | কী দেখে | কী ধরে না | উদাহরণ tool |
|---|---|---|---|
| Secret scan | Commit-এ key আর password-এর ছাঁচ | আগে push হওয়া বা অচেনা ধরনের secret | gitleaks, TruffleHog |
| SCA | Dependency-র জানা CVE আর লাইসেন্স | আপনার নিজের কোডের ভুল | Trivy, Grype, osv-scanner |
| SAST | না চালিয়ে source code পড়ে ঝুঁকির ছাঁচ | Config আর runtime-এর সমস্যা | Semgrep, CodeQL |
| IaC scan | Terraform বা Kubernetes YAML-এ ঝুঁকিপূর্ণ setting | হাতে করা পরিবর্তন (drift) | Checkov, Trivy, Conftest |
| Image scan | Image-এর OS package আর library | Scan-এর পরে আসা CVE | Trivy, Grype |
| DAST | চলন্ত app-কে বাইরে থেকে নিরাপদ অনুরোধ পাঠিয়ে পরীক্ষা | যে পথে অনুরোধ পৌঁছায় না | ZAP |
| সই যাচাই | Deploy-এর image কার সই করা | কোডের মান | cosign, admission policy |
এবার একটি কাল্পনিক অনলাইন বাজার অ্যাপের একটি পরিবর্তনকে পুরো পথে হাঁটিয়ে দেখি। নিচের আউটপুটগুলো নমুনা, কোনো আসল tool-এর হুবহু ফল নয়।
- Commithook
- PRSCA
- MergeSAST, IaC
- Buildimage
- Signdigest
- StagingDAST
- Deployverify
secret-scan config/dev.env:3 possible key
commit stopped, nothing left the laptopimage-lib 2.1.0 HIGH fix: 2.1.4 new in this PR
check failed: 1 blocking findingSCA pass
SAST WARN weak hash in report.py:41
IaC pass
all required checks passed, review approvedimage shop-api@sha256:aaaa…
scan 0 critical, 0 high with a fix
sbom sbom.spdx.json attachedsigned shop-api@sha256:aaaa…
provenance repo, commit, builder recorded
identity release workflow on mainWARN missing security header on /login
ticket opened, owner: shop team, due 14 daysverify signature identity matches policy
credential short-lived, from OIDC
rollout started- ল্যাপটপেই থামল। Pre-commit hook একটি key-এর মতো লেখা পেল। Commit হলোই না, তাই history-তে কিছু গেল না। এটিই সবচেয়ে সস্তা ধরা। তবে hook এড়ানো যায়, তাই server-এও একই পরীক্ষা চলে।
- PR-এ block. নতুন যোগ করা
image-lib 2.1.0-এ একটি HIGH CVE, আর 2.1.4-এ fix আছে। Finding এই PR-এই নতুন, তাই check ব্যর্থ। লেখক কয়েক মিনিটেই জানলেন। - Warn, block নয়। Version বাড়ানোর পর SCA পাস। SAST একটি medium সতর্কতা দিল, যা PR থামাল না কিন্তু রেকর্ডে থাকল। Review শেষে merge.
- Build-এ image scan. Base image-এর package-ও এখানে ধরা পড়ে। Fix আছে এমন critical বা high নেই। SBOM তৈরি হয়ে digest-এর সাথে যুক্ত হলো।
- সই। সই বসে digest-এ। Provenance-এ লেখা থাকে কোন repository, কোন commit, কোন builder. পর্ব ৫৮-এর “একবার build” নীতির কারণে এই একটি digest-ই সামনে যাবে।
- Staging-এ DAST. চলন্ত app-এ একটি header নেই। এটি ticket হলো, মালিক আর সময়সীমাসহ। মালিক ছাড়া finding মানে কাজ না হওয়া finding.
- Deploy-এ যাচাই। সই আর পরিচয় policy-র সাথে মিলল, আর deploy চলল OIDC-র স্বল্পমেয়াদি credential দিয়ে। সাতটি ধাপে দুবার থামলেও মোট দেরি ছিল সামান্য।
Scanner-গুলো দেখে কোড আর package. CI-র অনুমতি, deploy-এর পরিচয় আর build-এর প্রমাণ তারা দেখে না। এই পর্বের বাকি অংশ মূলত সেই ফাঁকগুলো নিয়ে। IaC-র পরীক্ষার বিস্তারিত আছে পর্ব ৫৫-এ, আর image scan-এর ফল পড়া পর্ব ৪১-এ।
৩ · False positive আর gate-এর নকশা
লঞ্চঘাটের টিকিট চেকার যদি প্রতি পাঁচজনে চারজনকে ভুল করে থামান, মানুষ অন্য পথ খুঁজবে। Security tool-এও তাই। False positive বেশি হলে দল alert পড়া ছেড়ে দেয়, যাকে বলে alert fatigue. তখন আসল বিপদও ভিড়ে হারায়। আর বিপরীত ভুল, false negative, মানে আসল সমস্যা ধরা পড়ল না।
হিসাব: একটি SAST rule-সেট ১২০টি finding দিল। যাচাইয়ে ১৮টি আসল, ১০২টি ভুল। Precision = ১৮ ÷ ১২০ = ১৫%. শব্দবহুল rule বাদ দিলে ৩০টি finding এলো, যার ১৫টি আসল। Precision এবার ১৫ ÷ ৩০ = ৫০%, কিন্তু ৩টি আসল finding হারাল। এই লেনদেন ইচ্ছাকৃত হওয়া উচিত। Block-এর জন্য উঁচু precision-এর rule রাখুন, আর বাকিগুলো warn হিসেবে চালান।
ভালো security gate-এর চার নিয়ম
- শুধু নতুন finding-এ block. পুরোনো ঋণের জন্য আজকের PR আটকালে সবাই gate-কে শত্রু ভাবে। পুরোনোগুলো baseline-এ রেখে আলাদা কাজ হিসেবে কমান।
- শুধু CVSS দেখবেন না। CVSS তীব্রতা বলে, আপনার ঝুঁকি নয়। কোডটি reachability-র হিসাবে পৌঁছানো যায় কি না, fix আছে কি না, service internet-মুখী কি না, আর CISA-র KEV তালিকায় আছে কি না, সব মেলান।
- ছাড়ের মেয়াদ থাকবে। Waiver-এ মালিক, কারণ আর শেষ তারিখ থাকে। মেয়াদহীন ছাড় মানে স্থায়ী ভুলে যাওয়া।
- কিছু জিনিসে আপস নেই। যাচাই করা live secret সবসময় block, score যা-ই হোক।
নিচের simulator-এ এই নিয়মগুলো কোড হিসেবে আছে। Weight-গুলো আমাদের অনুমান, কোনো মান নয়। আপনার দল নিজের ঝুঁকি অনুযায়ী ঠিক করবে।
// Security gate: score each finding, then decide. All weights are our assumption.
const WARN_AT = 30, BLOCK_AT = 70; // change these two and run again
const TODAY = "2026-11-01"; // simulated clock
const SEV = { critical: 50, high: 30, medium: 12, low: 3 };
function score(f) {
let s = SEV[f.sev];
s += f.reachable ? 20 : -15; // does our code really reach it?
if (f.exploited) s += 30; // known to be exploited in the wild
if (f.internet) s += 10; // service faces the internet
if (!f.fixAvailable) s -= 10; // nothing to upgrade to yet
return Math.max(0, Math.min(100, s));
}
function decide(f) {
if (f.verifiedSecret) return ["BLOCK", 100, "live secret, score ignored"];
if (f.waiverUntil && f.waiverUntil >= TODAY) return ["WAIVED", 0, "until " + f.waiverUntil];
if (f.existing) return ["WARN", score(f), "old finding, tracked, not this PR"];
const s = score(f);
return [s >= BLOCK_AT ? "BLOCK" : s >= WARN_AT ? "WARN" : "INFO", s, "new finding"];
}
const findings = [
{ id: "secret-scan: cloud key", verifiedSecret: true },
{ id: "CVE in web framework", sev: "critical", reachable: true, exploited: true, internet: true, fixAvailable: true },
{ id: "CVE in test-only tool", sev: "critical", reachable: false, fixAvailable: true },
{ id: "SAST: weak hash", sev: "medium", reachable: true, internet: true, fixAvailable: true },
{ id: "old CVE in base image", sev: "high", reachable: true, fixAvailable: true, existing: true },
{ id: "CVE, no fix yet", sev: "high", reachable: true, internet: true, fixAvailable: false, waiverUntil: "2026-11-20" },
{ id: "CVE, waiver expired", sev: "high", reachable: true, internet: true, fixAvailable: true, waiverUntil: "2026-10-01" },
];
let blocked = 0;
for (const f of findings) {
const [verdict, s, why] = decide(f);
blocked += verdict === "BLOCK";
console.log(verdict.padEnd(7), String(s).padStart(3), f.id + " (" + why + ")");
}
console.log(blocked ? "PR status: FAILED, " + blocked + " blocking" : "PR status: PASSED");আউটপুট:
BLOCK 100 secret-scan: cloud key (live secret, score ignored)
BLOCK 100 CVE in web framework (new finding)
WARN 35 CVE in test-only tool (new finding)
WARN 42 SAST: weak hash (new finding)
WARN 50 old CVE in base image (old finding, tracked, not this PR)
WAIVED 0 CVE, no fix yet (until 2026-11-20)
WARN 60 CVE, waiver expired (new finding)
PR status: FAILED, 2 blocking
Web framework-এর CVE পেল 50 + 20 + 30 + 10 = 110, যা 100-তে আটকে BLOCK. Test tool-এর একই critical CVE পেল 50 − 15 = 35, তাই শুধু WARN: তীব্রতা এক, ঝুঁকি আলাদা। মেয়াদোত্তীর্ণ waiver আর ঢাল নয়, finding আবার ৬০ নম্বর পেল। BLOCK_AT-এর মান 60 করে চালালে সেটিও block হবে, মোট ৩টি। এই একটি সংখ্যাই আপনার দলের ঝুঁকি-সহনশীলতা।
নতুন gate প্রথম দিনেই block mode-এ চালু করবেন না। আগে কয়েক সপ্তাহ শুধু report mode-এ চালান, কতটা শব্দ হয় দেখুন, rule ছাঁটুন, তারপর ধীরে block করুন। ধরুন দিনে ৪০টি PR আর ৫% ভুল block, প্রতিটিতে ২০ মিনিট নষ্ট: দিনে ৪০ মিনিট। ছোট শোনায়, কিন্তু gate-এর ওপর আস্থা ক্ষয় হয় এখান থেকেই।
৪ · CI system-এর threat model
এতক্ষণ pipeline দিয়ে কোড পাহারা দিলাম। এবার প্রশ্নটি উল্টে দিন: pipeline-কে কে পাহারা দেয়? Threat model মানে আগে থেকে ভাবা: কী রক্ষা করছি, কোথায় দুর্বল, ভুল হলে কতটা ক্ষতি। CI নিয়ে ভাবলে দেখা যায়, এটি একসাথে দুটি জিনিস: বাইরের কোড চালানোর যন্ত্র, আর production-এ লেখার ক্ষমতাধারী। এই দুটির মিলনই ঝুঁকি। CI runner তাই গুদামঘরের সেই দারোয়ানের মতো, যাঁর হাতে সব চাবি, আর সারাদিন অচেনা মানুষ তাঁর ঘরে আসা-যাওয়া করে।
অতিরিক্ত ক্ষমতার token
স্থায়ী cloud key বা repository-তে লেখার ক্ষমতা সব job-এ। একটি ধাপ গোলমাল করলেই পুরো ক্ষমতা ঝুঁকিতে।
প্রতিকার: job ধরে অনুমতি, OIDCতৃতীয় পক্ষের action
Third-party action অন্যের কোড, যা আপনার secret-এর পাশে চলে। Tag সরলে পরের run-এই ভিন্ন কোড চলে।
প্রতিকার: SHA pin, অনুমোদিত তালিকাঅবিশ্বস্ত pull request
Fork থেকে আসা PR-এর কোড বা তার title-ও অবিশ্বস্ত input. একে secret-এর নাগালে চালালে বিপদ।
প্রতিকার: আলাদা job, secret ছাড়াGitHub Actions-এ সাধারণ pull_request event-এ fork-এর PR লেখার সময়ের নিয়মে secret পায় না, আর token শুধু পড়তে পারে। কিন্তু pull_request_target চলে মূল repository-র প্রেক্ষাপটে, secret-এর নাগালে। তাই এই event-এ PR-এর কোড checkout করে চালানো যাবে না। GitLab-সহ অন্য CI-তেও একই নীতি খাটে: বাইরের কোড আর সংরক্ষিত variable এক job-এ রাখবেন না। PR-এর লেবেল বসানোর মতো সহজ কাজে এই event ঠিক আছে, যদি PR-এর কোড চালানো না হয়।
আরও দুটি নিয়ন্ত্রণ মনে রাখুন। Self-hosted runner একই যন্ত্রে বারবার চললে আগের job-এর অবশিষ্ট পরের job পেতে পারে, তাই প্রতিটি job-এ নতুন, অস্থায়ী runner ভালো, আর public repository-তে এগুলো সাধারণত এড়িয়ে চলা হয়। আর CI ফাইল বদলানো মানে কোন কোড কোন secret দেখবে তা বদলানো, তাই .github/ ফোল্ডারে CODEOWNERS দিয়ে আলাদা review রাখুন। Secret রাখার বিস্তারিত আছে Continuous Integration পর্বে।
৫ · Action pin আর সীমিত job token
বাজারে “চালের বস্তা, ২৫ কেজি” বললে দোকানদার যেকোনো বস্তা দিতে পারেন। কিন্তু সিল নম্বর মিলিয়ে নিলে ঠিক সেই বস্তাই পাবেন। @v7 হলো দোকানের নাম-লেখা বস্তা, আর পুরো commit SHA হলো সিল নম্বর। Tag সরানো যায়। Commit-এর SHA তার পুরো বিষয়বস্তুর hash, তাই SHA pinning ঠিক একই কোড নিশ্চিত করে। ৪০ অক্ষরের পুরোটাই লাগে, আর পাশে comment-এ version লিখে রাখলে মানুষ পড়তে পারে।
# Valid YAML, parsed but not run on a real repository.
name: test
on:
pull_request:
permissions:
contents: read # default for every job: read only
jobs:
test:
runs-on: ubuntu-latest
steps:
# checked with git ls-remote in October 2026
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: npm ci
- run: npm testএখানে দুটি কাজ হয়েছে। প্রথমটি pin. দ্বিতীয়টি permissions: স্বয়ংক্রিয় job token (GitHub-এ GITHUB_TOKEN) শুধু পড়তে পারে। এটি least privilege-এর সরাসরি প্রয়োগ। কোনো একটি অনুমতি লিখলে, না-লেখা বাকিগুলো বন্ধ হয়ে যায় (GitHub-এর লেখার সময়ের আচরণ)। যে job-এর লিখতে হবে, শুধু সেই job-এ বাড়ান। Repository-র default-ও read-only রাখুন, কারণ পুরোনো repository-তে তা ভিন্ন হতে পারে।
Tag-এর ঝুঁকি কতটা ছড়িয়ে আছে, নিজের workflow-তে দেখুন। নিচের checker নমুনা লেখা পড়ে প্রতিটি uses: শ্রেণিতে ভাগ করে। সব SHA ভুয়া।
// SHA-pinning checker over sample workflow text. Every SHA here is fake.
const workflow = `
- uses: actions/checkout@1111111111111111111111111111111111111111 # v7.0.1
- uses: example-org/lint-action@v3
- uses: example-org/scan-action@v2.4.1
- uses: example-org/report-action@main
- uses: example-org/cache-action@abc1234
- uses: ./.github/actions/deploy
- uses: docker://example.com/tool:latest
- uses: docker://example.com/tool@sha256:${"a".repeat(64)}
`;
function classify(ref) {
if (ref.startsWith("./")) return ["OK ", "local, reviewed in this repository"];
if (ref.startsWith("docker://"))
return /@sha256:[0-9a-f]{64}$/.test(ref)
? ["OK ", "image pinned by digest"]
: ["FAIL", "image tag can move"];
const rev = ref.split("@")[1] || "";
if (/^[0-9a-f]{40}$/.test(rev)) return ["OK ", "full commit SHA"];
if (/^[0-9a-f]{7,39}$/.test(rev)) return ["FAIL", "short SHA is not a full pin"];
if (/^v?\d/.test(rev)) return ["FAIL", "tag can move"];
return ["FAIL", "branch moves on every push"];
}
let ok = 0, total = 0;
for (const line of workflow.split("\n")) {
const m = line.match(/uses:\s*(\S+)/);
if (!m) continue;
const [status, why] = classify(m[1]);
total++;
if (status === "OK ") ok++;
console.log(status, m[1].slice(0, 40).padEnd(40), why);
}
console.log("pinned " + ok + " of " + total + " = " + Math.round(100 * ok / total) + "%");আউটপুট:
OK actions/checkout@11111111111111111111111 full commit SHA
FAIL example-org/lint-action@v3 tag can move
FAIL example-org/scan-action@v2.4.1 tag can move
FAIL example-org/report-action@main branch moves on every push
FAIL example-org/cache-action@abc1234 short SHA is not a full pin
OK ./.github/actions/deploy local, reviewed in this repository
FAIL docker://example.com/tool:latest image tag can move
OK docker://example.com/tool@sha256:aaaaaaa image pinned by digest
pinned 3 of 8 = 38%
৮টির ৩টি pin করা, মানে 3 ÷ 8 ≈ 38%. @main সবচেয়ে ঝুঁকিপূর্ণ, কারণ প্রতি push-এ কোড বদলায়। @abc1234 SHA দেখতে হলেও পুরো নয়, তাই FAIL. নিজের repository-র ./ action review-এর আওতায় থাকে। Docker image-এর নিয়মও একই: digest পিন, tag নয়।
এবার একই ধারণা shell-এ, যা CI-র একটি ধাপ হিসেবেই চলতে পারে। এই block অনুমতি আর ঝুঁকিপূর্ণ trigger-ও দেখে।
# Audit a sample workflow for unpinned actions and broad permissions.
set -euo pipefail
cat > ci.yml <<'YAML'
on:
pull_request_target:
permissions: write-all
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: example-org/lint-action@main
- uses: example-org/scan-action@1111111111111111111111111111111111111111
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: ./.github/actions/deploy
YAML
awk '
/^ [a-z_-]+:$/ { job = $1 }
/^ *- uses:/ {
if ($3 ~ /^\.\//) next
split($3, part, "@")
if (length(part[2]) != 40 || part[2] ~ /[^0-9a-f]/) {
print "line " NR ": unpinned action: " $3; bad++
}
}
/permissions: *write-all/ { print "line " NR ": broad token: write-all"; bad++ }
/^ *(pull_request_target|workflow_run):/ { print "line " NR ": review trigger: " $1; bad++ }
/id-token: *write/ { print "line " NR ": note: OIDC allowed in job " job }
END { print "findings: " bad + 0 }
' ci.ymlআউটপুট:
line 2: review trigger: pull_request_target:
line 3: broad token: write-all
line 8: unpinned action: actions/checkout@v7
line 9: unpinned action: example-org/lint-action@main
line 15: note: OIDC allowed in job deploy:
findings: 4
awk প্রতিটি লাইন দেখে। @-এর পরের অংশ ঠিক ৪০ অক্ষরের hex না হলে unpinned. Pin করা scan-action আর local action তাই তালিকায় নেই। সবচেয়ে বিপজ্জনক জুটি ci.yml-এর ২ আর ৩ নম্বর লাইনে: pull_request_target আর write-all একসাথে। ১৫ নম্বর লাইনের বার্তা finding নয়, তথ্য: OIDC শুধু deploy job-এ খোলা। CI-তে gate বানাতে শেষে findings শূন্য না হলে exit 1 যোগ করবেন। এটি শেখার জন্য একটি ছোট script, আসল কাজে zizmor বা actionlint-এর মতো YAML-বোঝা tool ভালো।
SHA pin সব ঢাকে না। Action নিজে ভেতরে অন্য কিছু নামালে সেটি pin-এর বাইরে। আর pin করার আগে SHA-টি upstream-এর release tag থেকে এসেছে কি না মিলিয়ে নিন, কারণ fork-এর commit-ও একই পথে দেখা যেতে পারে। প্রতিষ্ঠানের জন্য অনুমোদিত action-এর তালিকাও রাখা যায় (platform-ভেদে আলাদা)।
৬ · OIDC federation: জমা key ছাড়া cloud-এ ঢোকা
আগে deploy করতে একটি স্থায়ী access key CI-র secret-এ রাখা হতো। সেটি বাড়ির মূল চাবির মতো: হারালে কেউ জানে না, মেয়াদও শেষ হয় না। OIDC ভিত্তিক identity federation অফিসের গেটপাসের মতো। গেটে পরিচয়পত্র দেখালেন, প্রহরী যাচাই করে সেদিনের একটি pass দিলেন, সন্ধ্যায় তার মেয়াদ শেষ। পকেটে কোনো চাবি নেই, তাই হারানোরও কিছু নেই।
এখানে পরিচয়পত্র হলো একটি JWT: CI platform-এর স্বাক্ষর করা ছোট JSON, যার প্রতিটি ফিল্ডকে বলে claim. প্রহরী হলো cloud-এর trust policy, যা পর্ব ৩১-এর Cloud IAM-এ দেখেছেন। নিচে পুরো পথটি হাঁটুন।
- CI jobকোনো key নেই
- CI issuerJWT সই
- Cloud STSযাচাই
- Cloud APIdeploy
permissions:
id-token: write
stored cloud key: noneiss https://token.actions.githubusercontent.com
aud sts.amazonaws.com
sub repo:example-org/shop-api:ref:refs/heads/main
exp a few minutes from nowAssumeRoleWithWebIdentity
RoleArn …role/shop-api-deploy
WebIdentityToken eyJhbGciOi… (the JWT)issuer registered? yes
signature with JWKS? yes
expired? no
token is genuineaud = sts.amazonaws.com match
sub = repo:example-org/shop-api:
ref:refs/heads/main match
role may be assumedAccessKeyId ASIAEXAMPLE…
SessionToken …
Expiration 15 minutes
deploy runs with the role's own policycredential expires by itself
key to rotate: none
audit log keeps role and session name- শুরুতে কিছুই নেই। Job-এর হাতে কোনো cloud key নেই। আছে শুধু
id-token: write, মানে সে নিজের পরিচয়পত্র চাইতে পারে। - Issuer JWT দিল। CI platform জানে এই job কোন repository, কোন branch থেকে এসেছে। Job এই তথ্য নিজে লিখতে পারে না, তাই token বিশ্বাসযোগ্য।
subফিল্ডে সেই পরিচয়। - Cloud-এর কাছে বিনিময়। Job token-টি দেখিয়ে একটি নির্দিষ্ট role চাইল। AWS-এ এই সেবা STS, আর call-এর নাম
AssumeRoleWithWebIdentity. - Token আসল তো? Issuer আগে থেকে নিবন্ধিত থাকতে হয়। Cloud issuer-এর public key (JWKS) দিয়ে সই আর মেয়াদ দেখে। Token জাল হলে এখানেই শেষ।
- এই token কি এই role পাবে? আসল token হলেই হবে না। aud আর sub trust policy-র শর্তে মিলতে হবে। অন্য repository-র আসল token এখানে আটকায়। এটিই সবচেয়ে গুরুত্বপূর্ণ ধাপ।
- স্বল্পমেয়াদি credential. STS মেয়াদসহ তিন অংশের temporary credential দিল। এখানে ধরে নেওয়া মেয়াদ ১৫ মিনিট। Job এখন শুধু role-এর permission policy-তে যা আছে, তা-ই করতে পারে।
- শেষে কিছু নেই। মেয়াদ শেষে credential নিজেই অকেজো। ঘোরানোর মতো স্থায়ী key নেই, আর audit log-এ role ব্যবহারের ছাপ থাকে।
তিনটি অংশ: job, token আর trust policy
নিচের তিনটি উদাহরণ কোনো আসল account-এ চালানো হয়নি। সব id ভুয়া, আর claim-এর তালিকা ছাঁটা। YAML আর JSON parse করে যাচাই করা হয়েছে।
name: deploy
on:
push:
branches: [main]
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # may request the OIDC token
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
with:
role-to-assume: arn:aws:iam::123456789012:role/shop-api-deploy
aws-region: ap-south-1
role-duration-seconds: 900
- run: ./deploy.sh{
"iss": "https://token.actions.githubusercontent.com",
"aud": "sts.amazonaws.com",
"sub": "repo:example-org/shop-api:ref:refs/heads/main",
"repository": "example-org/shop-api",
"ref": "refs/heads/main",
"sha": "1111111111111111111111111111111111111111",
"event_name": "push",
"run_id": "1234567890",
"iat": 1800000000,
"exp": 1800000300
}{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:example-org/shop-api:ref:refs/heads/main"
}
}
}
]
}sub-এর শর্ত না থাকলে বা repo:example-org/* লিখলে দরজা অনেক চওড়া হয়। Job-এ environment: production যোগ করলে GitHub-এ sub-এর রূপও বদলে হয় repo:example-org/shop-api:environment:production, তাই policy মেলাতে হয়। আর main-এর শর্ত তখনই অর্থবহ, যখন branch protection আছে। যে কেউ main-এ push করতে পারলে এই শর্ত নামমাত্র।
| Cloud | বিশ্বাস কোথায় লেখা | কী মেলে |
|---|---|---|
| AWS | IAM OIDC provider আর role-এর trust policy | Condition-এ aud, sub |
| Azure | Entra app বা managed identity-র federated credential | Issuer, subject, audience |
| Google Cloud | Workload Identity Federation pool আর provider | Attribute condition |
GitLab CI-তেও id_tokens দিয়ে একই কাজ হয়, শুধু sub-এর গড়ন আলাদা। নাম আর ফিল্ড platform-ভেদে বদলায়, তাই docs মিলিয়ে নিন। এবার trust policy নিয়ে পরীক্ষা করুন: strict আর loose নীতিতে সাতটি ভুয়া token.
// OIDC trust-policy matcher: a small model, not any provider's real engine.
const NOW = 1000; // simulated clock, in seconds
const REPO = "repo:example-org/shop-api";
const glob = p => new RegExp("^" + p.split("*").map(s =>
s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$");
const strict = { iss: "https://ci.example.com", aud: "sts.example.com",
sub: [REPO + ":ref:refs/heads/main"] };
const loose = { iss: "https://ci.example.com", aud: "sts.example.com",
sub: ["repo:example-org/*"] };
function check(policy, t) {
if (t.exp <= NOW) return "DENY token expired";
if (t.iss !== policy.iss) return "DENY unknown issuer";
if (t.aud !== policy.aud) return "DENY wrong audience";
if (!policy.sub.some(p => glob(p).test(t.sub))) return "DENY subject not allowed";
return "ALLOW short-lived credential";
}
const base = { iss: "https://ci.example.com", aud: "sts.example.com", exp: 1300 };
const tokens = [
["main branch", { ...base, sub: REPO + ":ref:refs/heads/main" }],
["feature branch", { ...base, sub: REPO + ":ref:refs/heads/feature-x" }],
["pull request", { ...base, sub: REPO + ":pull_request" }],
["other repo, same org", { ...base, sub: "repo:example-org/old-demo:ref:refs/heads/main" }],
["other org", { ...base, sub: "repo:someone-else/shop-api:ref:refs/heads/main" }],
["wrong audience", { ...base, aud: "other.example.com", sub: REPO + ":ref:refs/heads/main" }],
["expired", { ...base, exp: 900, sub: REPO + ":ref:refs/heads/main" }],
];
for (const [name, t] of tokens) {
console.log(name.padEnd(21), "strict:", check(strict, t).slice(0, 5),
" loose:", check(loose, t).slice(0, 5));
}
console.log("why feature branch failed:", check(strict, tokens[1][1]));আউটপুট:
main branch strict: ALLOW loose: ALLOW
feature branch strict: DENY loose: ALLOW
pull request strict: DENY loose: ALLOW
other repo, same org strict: DENY loose: ALLOW
other org strict: DENY loose: DENY
wrong audience strict: DENY loose: DENY
expired strict: DENY loose: DENY
why feature branch failed: DENY subject not allowed
strict নীতিতে শুধু main ঢোকে। loose নীতিতে একই প্রতিষ্ঠানের feature branch, PR আর পুরোনো একটি demo repository-ও ঢোকে। মানে প্রতিষ্ঠানের সবচেয়ে অবহেলিত repository-ই আপনার production-এর দুর্বলতম দরজা। Audience ভুল হলে বা মেয়াদ গেলে দুই নীতিতেই DENY, কারণ এগুলো সব token-এর জন্য বাধ্যতামূলক পরীক্ষা। এটি একটি ছোট মডেল, কোনো provider-এর আসল engine নয়।
৭ · Build-এ যা থেকে যায়: দুটি আসল পরীক্ষা
Secret CI-তে নিরাপদে থাকলেও build-এর ভুলে image-এ ঢুকে যেতে পারে। নিচের transcript আসল run থেকে নেওয়া, Docker Engine 29 আর BuildKit-এ। Id, digest, সময় আর আকার ভিন্ন হবে, কিছু লাইন ছাঁটা। Token-এর মান ভুয়া।
$ cat Dockerfile.bad
FROM alpine:3.20
ARG API_TOKEN
RUN test -n "$API_TOKEN" && echo "token received"
$ docker build --progress=plain --no-cache \
--build-arg API_TOKEN=fake-token-abc123 -f Dockerfile.bad -t abcl-ds2-bad:1 .
…
#5 0.200 token received
…
1 warning found (use docker --debug to expand):
- SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "API_TOKEN") (line 2)
$ docker inspect --format '{{json .Config.Env}}' abcl-ds2-bad:1
["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]
$ docker history --no-trunc --format '{{.CreatedBy}}' abcl-ds2-bad:1
RUN |1 API_TOKEN=fake-token-abc123 /bin/sh -c test -n "$API_TOKEN" && echo "token received" # buildkit
ARG API_TOKEN=fake-token-abc123
…ARG চলন্ত container-এর environment-এ নেই, তাই inspect-এ নিরাপদ দেখায়। কিন্তু docker history মান খোলাখুলি দেখাল। Image যাঁর হাতে যায়, token-ও তাঁর হাতে যায়। BuildKit নিজেও warning দিয়েছিল, CI log-এ warning পড়া তাই জরুরি।
$ cat Dockerfile.good
FROM alpine:3.20
RUN --mount=type=secret,id=api_token \
test -s /run/secrets/api_token && echo "token received"
$ docker build --progress=plain --no-cache \
--secret id=api_token,src=token.txt -f Dockerfile.good -t abcl-ds2-good:1 .
…
#5 0.215 token received
…
$ docker history --no-trunc --format '{{.CreatedBy}}' abcl-ds2-good:1
RUN /bin/sh -c test -s /run/secrets/api_token && echo "token received" # buildkit
…একই কাজ হলো, token received ছাপা হলো, কিন্তু history-তে মান নেই। Build secret শুধু ওই একটি RUN-এর সময় ফাইল হিসেবে থাকে, কোনো layer-এ জমে না।
দ্বিতীয় পরীক্ষা non-root. পর্ব ৩৮-এ এর কারণ দেখেছেন। এখানে শুধু তথ্যটি:
$ cat Dockerfile.user
FROM alpine:3.20
RUN adduser -D -u 10001 app
USER 10001
CMD ["id"]
$ docker run --rm --hostname demo abcl-ds2-user:1
uid=10001(app) gid=10001(app) groups=10001(app)
$ docker run --rm --hostname demo abcl-ds2-user:1 sh -c 'touch /etc/x; echo exit=$?'
touch: /etc/x: Permission denied
exit=1
$ docker run --rm --hostname demo alpine:3.20 sh -c 'id; touch /etc/x; echo exit=$?'
uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),…
exit=0Dockerfile-এ USER না লিখলে alpine:3.20 root হিসেবে চলে আর /etc-এ লিখতে পারে। USER 10001-এ একই কাজ ব্যর্থ। IaC scan আর admission policy এই নিয়মটিই যন্ত্রে পরীক্ষা করে।
৮ · SLSA, provenance, Sigstore আর SBOM
Scan বলে “জানা ত্রুটি আছে কি না”. কিন্তু ডেলিভারি হওয়া জিনিসটি যে আপনার source থেকে আপনার build-এই তৈরি, তা কে বলবে? Provenance সেই নথি, ওষুধের প্যাকেটের ব্যাচ নম্বর আর কারখানার ঠিকানার মতো। SLSA (উচ্চারণ “সালসা”) সেই নথির ওপর কতটা ভরসা করা যায়, তার স্তর ঠিক করে। Google-এর ভেতরের চর্চা থেকে ২০২১ সালে এটি সামনে আসে, এখন OpenSSF-এর অধীনে। নিচের ভাষা SLSA v1.0 (২০২৩) আর পরের version-এর Build track-এর সরল রূপ। মূল specification বদলাতে পারে, তাই মিলিয়ে নিন। আগের v0.1-এ চারটি স্তর ছিল, আর v1.2-তে Build track-এর পাশে Source track-ও এসেছে।
| স্তর | সহজ কথায় | কী ঠেকায় |
|---|---|---|
| Build L1 | Provenance আছে: কে, কী থেকে, কীভাবে বানাল | ভুল আর হারানো হিসাব। জালিয়াতি ঠেকায় না |
| Build L2 | Hosted build platform বানায় আর provenance-এ সই করে | Build-এর পরে নথি বদল |
| Build L3 | Hardened builder: run-গুলো আলাদা, আর সই করার উপকরণ build ধাপের নাগালের বাইরে | Build চলাকালে নথি জাল করা |
স্তর বেশি মানে কোড ভালো নয়। ভেতরের কোড খারাপ হলে L3-তেও খারাপই থাকে। SLSA শুধু বলে জিনিসটি কোন পথে এসেছে। বেশিরভাগ দলের জন্য বাস্তব লক্ষ্য: আগে L1, তারপর hosted CI-তে L2. L3-এর জন্য platform-এর সমর্থন লাগে, যেমন SLSA project-এর GitHub Actions generator.
Sigstore: সই, চাবি ছাড়াই
পর্ব ৪১-এ cosign দেখেছেন। এই পর্বের OIDC-র সাথে এবার ছবিটি মিলে যায়। Keyless signing-এ job তার OIDC পরিচয় Fulcio-কে দেখায়, কয়েক মিনিটের একটি certificate পায়, আর সইয়ের ঘটনা Rekor-এর transparency log-এ জমা হয়। ফলে সই করার জন্য স্থায়ী গোপন key পাহারা দিতে হয় না। Deploy-এর সময় admission control দেখে পরিচয়টি ঠিক সেই release workflow কি না। “সই আছে” যথেষ্ট নয়, “কার সই” দেখতে হবে। Attestation একই পথে provenance বা SBOM-কে digest-এর সাথে বাঁধে।
SBOM-এর আসল কাজ
SBOM আলমারিতে রাখা নথি হলে লাভ নেই। এর কাজ শুরু হয় নতুন CVE আসার দিন। প্রতিটি build-এর SBOM এক জায়গায় জমা থাকলে “কোন চলন্ত image-এ এই library আছে?” প্রশ্নের উত্তর মিনিটে আসে। Tool যা চিনতে পারে না তা তালিকায় থাকে না, তাই এটি সম্পূর্ণ সত্য নয়। VEX নথি সাথে থাকলে বলা যায় “আছে, কিন্তু আমাদের এতে ঝুঁকি নেই”.
আরও গভীরে: SLSA provenance সাধারণত in-toto attestation ফরম্যাটে লেখা হয়: একটি subject (artifact-এর digest) আর একটি predicate (কীভাবে বানানো)। SLSA নিজে reproducible build চায় না, তবে reproducible build থাকলে তৃতীয় পক্ষ স্বাধীনভাবে ফল মিলিয়ে দেখতে পারে। প্রকাশিত package-এর ক্ষেত্রেও npm provenance আর PyPI-র trusted publishing একই OIDC ধারণা ব্যবহার করে (লেখার সময়ের অবস্থা)।
৯ · ঘটনা, হালনাগাদ আর মাপার সংখ্যা
তিনটি প্রকাশিত ঘটনা দেখি। বিবরণ কোম্পানিগুলোর প্রকাশিত তথ্য আর জনসমক্ষে আসা বিশ্লেষণের ভিত্তিতে সংক্ষেপ করা। এখানে উদ্দেশ্য আত্মরক্ষার শিক্ষা, আক্রমণের কৌশল নয়।
| ঘটনা | প্রকাশিত বিবরণ | শিক্ষা |
|---|---|---|
| SolarWinds Orion, ২০২০ | ডিসেম্বর ২০২০-এ প্রকাশ পায়, build প্রক্রিয়ায় ক্ষতিকর কোড ঢোকে। Update-এ বৈধ সই ছিল, আর কোম্পানির SEC-তে দাখিল করা তথ্য অনুযায়ী ১৮ হাজারের কম গ্রাহক সেই update বসিয়ে থাকতে পারেন | সই শুধু উৎস বলে। Build system-কেও রক্ষা করতে হয় |
| Codecov, ২০২১ | Codecov জানায়, Docker image তৈরির একটি ত্রুটি থেকে একটি credential বেরিয়ে যায়, আর তাদের Bash Uploader script বদলানো হয়। জানা গেছে বহু গ্রাহকের CI-র environment variable ঝুঁকিতে ছিল। একজন গ্রাহকের চোখে অসঙ্গতি ধরা পড়ে | Image-এ credential নয়। CI-তে নামানো script-এর hash মেলান। Secret ঘোরান |
| tj-actions/changed-files, ২০২৫ | মার্চ ২০২৫-এ এই জনপ্রিয় action-এর tag ক্ষতিকর commit-এ সরানো হয়, CVE-2025-30066 নামে নথিভুক্ত। প্রকাশিত বিশ্লেষণ অনুযায়ী কিছু public repository-র log-এ secret ফাঁসের ঝুঁকি হয়েছিল | SHA pin, সীমিত token, আর log-ও সংবেদনশীল |
তিনটি ঘটনার মিল একটিই: আক্রান্ত দল নিজে ভুল কোড লেখেনি, ভরসা করা একটি শিকল ভেঙেছিল। তাই প্রশ্ন শুধু “ঠেকাব কীভাবে” নয়, “ঘটলে কী করব” সেটিও। নিচের কাল্পনিক ঘটনায় ১২টি repository-র একটি প্রতিষ্ঠানে একটি CI dependency আক্রান্ত বলে খবর এলো। সময় আর সংখ্যাগুলো উদাহরণ।
| সময় | |
| আক্রান্ত repository | |
| বদলানো credential | |
| আক্রান্ত চলন্ত service | |
| Deploy |
- খবর এলো। একটি advisory বলছে একটি নির্দিষ্ট version ক্ষতিকর। সময় গোনা শুরু। ঘটনার একজন নেতা ঠিক করা হলো, কারণ সবাই সবকিছু করলে কিছুই হয় না। পর্ব ৬৭-এ এর কাঠামো আছে।
- ক্ষতি বাড়া থামান। Auto-merge বন্ধ, আর সাধারণ deploy আটকানো হলো, যাতে আক্রান্ত version নতুন করে না ছড়ায়। ঘাবড়ে কিছু মুছে ফেলা হলো না, কারণ log প্রমাণ।
- পরিধি মাপুন। SBOM আর lockfile খুঁজে ১২টির ৩টি repository মিলল, আর CI log-এ দেখা গেল কোন run সেই version চালিয়েছে। Production-এ ২টি service আক্রান্ত image-এ চলছে।
- Secret ফাঁস ধরে নিন। ওই job-গুলোর নাগালে যা ছিল, ৬টি credential-ই বদলানো হলো। প্রমাণের অপেক্ষা নয়। আগে বাতিল ও নতুন করা, পরে তদন্ত। OIDC থাকলে এখানে স্থায়ী key কম থাকত, কাজও কম হতো।
- পরিষ্কার version-এ pin. নিরাপদ বলে যাচাই করা version-এর SHA বা digest-এ pin করে image আবার build হলো, cache ছাড়া। আক্রান্ত সময়ের artifact বিশ্বাস করা হলো না।
- নতুন digest deploy. শুধু fix deploy-এর অনুমতি। সই যাচাই পার হয়ে ২টি service নতুন image-এ এলো, আর সংখ্যা ০. শনাক্ত থেকে এ পর্যন্ত ৫ ঘণ্টা।
- যাচাই আর শিক্ষা। Cloud-এর audit log-এ পুরোনো credential-এর অস্বাভাবিক ব্যবহার খোঁজা হলো। Postmortem-এ দোষ নয়, ব্যবস্থা বদলের সিদ্ধান্ত হলো: SHA pin, OIDC, আর হালনাগাদে দেরি।
Dependabot আর Renovate
Pin করলে হালনাগাদ নিজে থেকে আসে না। তাই pin-এর সাথে যন্ত্রও লাগে। Dependabot (GitHub-এর) আর Renovate (Mend-এর open source tool, অনেক platform-এ চলে) নতুন version দেখে PR খোলে, SHA-সহ, আর CI সেটি পরীক্ষা করে।
# .github/dependabot.yml (option names as of writing)
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
actions:
patterns: ["*"]
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 5cooldown নতুন release নিতে কয়েক দিন অপেক্ষা করে, কারণ অনেক ক্ষতিকর release অল্প সময়ে ধরা পড়ে (Renovate-এ minimumReleaseAge একই ধারণা)। তবে নিরাপত্তা-fix দ্রুত নেওয়ার পথ আলাদা রাখুন। ছোট PR, ভালো test আর গুচ্ছ করে হালনাগাদই এই tool-কে কার্যকর করে। নইলে PR-এর পাহাড় জমে।
মাপার মতো সংখ্যা
“এই মাসে ৪০০টি finding” কিছুই বলে না। Scanner বদলালেই সংখ্যা বদলায়। আসল প্রশ্ন, ধরা পড়ার পর ঠিক হতে কত সময় লাগে। একে বলে mean time to remediate. MTTR সংক্ষেপে অন্য অর্থেও (mean time to repair বা recover) ব্যবহার হয়, তাই পুরোটা বলুন।
হিসাব: ৫টি বন্ধ finding-এ লেগেছে ২, ৩, ৩০, ১ আর ১৪ দিন। গড় = (২ + ৩ + ৩০ + ১ + ১৪) ÷ ৫ = ৫০ ÷ ৫ = ১০ দিন। Median ৩ দিন। গড় একটি ৩০ দিনের finding-এ টেনে উঠেছে। আরও ফাঁদ: খোলা finding এই হিসাবে নেই। ধরুন আরও দুটি খোলা আছে, বয়স ৬০ আর ৭৫ দিন। সেগুলো ধরলে গড় অন্তত ১৮৫ ÷ ৭ ≈ ২৬.৪ দিন। তাই MTTR-এর সাথে সবসময় সবচেয়ে পুরোনো খোলা finding-এর বয়সও দেখুন।
সাথে আরও কয়েকটি দরকারি সংখ্যা: severity ধরে কতটি সময়সীমার ভেতরে ঠিক হলো, gate কতবার হাতে এড়ানো হলো, rule-এর precision, কত শতাংশ action pin করা, আর কত repository OIDC-তে এসেছে। Severity ধরে সময়সীমা ঠিক করুন, যেমন critical সাত দিনে। সংখ্যাটি উদাহরণ, মান নয়।
পরীক্ষা ও ইন্টারভিউতে:
- SAST আর DAST-এর পার্থক্য? SAST না চালিয়ে source পড়ে, DAST চলন্ত app-কে বাইরে থেকে পরীক্ষা করে।
- SCA কী ধরে? তৃতীয় পক্ষের dependency-র জানা ত্রুটি আর লাইসেন্স।
- Action-কে tag-এ নয়, SHA-তে pin কেন? Tag সরানো যায়, পুরো commit SHA নির্দিষ্ট কোড বোঝায়।
- OIDC-র trust policy-তে
subকেন? এটি বলে কোন repository আর branch role নিতে পারবে। না থাকলে অন্য repository-র token-ও ঢুকতে পারে। - SLSA L2 আর L3-এর পার্থক্য? L2-তে hosted platform সই করা provenance দেয়। L3-তে run আলাদা আর সইয়ের উপকরণ build ধাপের নাগালের বাইরে।
১০ · AI-এর যুগে DevSecOps
AI pipeline YAML দ্রুত লেখে, কিন্তু তার শেখা উদাহরণের বেশিরভাগই পুরোনো। ফলে সে প্রায়ই @v3 tag, permissions: write-all, স্থায়ী access key-এর secret, pull_request_target, sub-এ * আর scanner-এর continue-on-error: true বসায়। অনেক সময় কোনো action-এর ঠিকানা বা SHA বানিয়েও দেয়। ধরনটি হুবহু এই পর্বের ভুলের তালিকা। আর আপনি AI-কে যা পাঠান, সেটি নিজেই ঝুঁকি: log, token বা account id prompt-এ দেবেন না।
নিচের prompt-এ AI-কে যাচাইকারী বানানো হয়েছে, লেখক নয়। শেষ লাইনটি খেয়াল করুন: সে যা জানে না, তা বলতে হবে।
AI-এর লেখা pipeline merge করার আগে
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১SolarWinds-এর update-এ বৈধ সই ছিল। তাহলে সই কী প্রমাণ করে, আর কী করে না?
সই দুটি জিনিস প্রমাণ করে: এই নির্দিষ্ট byte-গুলো সই করার পরে বদলায়নি, আর সই করেছে এই key বা পরিচয়ের মালিক। এর বেশি কিছু নয়। ২০২০ সালের SolarWinds ঘটনায় প্রকাশিত বিশ্লেষণ অনুযায়ী ক্ষতিকর কোড ঢুকেছিল build প্রক্রিয়ার ভেতরে, সই হওয়ার আগে। তাই সই একদম সৎ ছিল: কোম্পানির build থেকেই জিনিসটি এসেছে। গ্রাহকের সব যাচাই পাস করেছে, কারণ যাচাইয়ের প্রশ্নটিই ভুল জায়গায় ছিল।
এই ঘটনার পরে শিল্পের দৃষ্টি বদলায়। প্রশ্ন “কে পাঠাল” থেকে সরে যায় “কীভাবে তৈরি হলো” পর্যন্ত। SLSA-র ভাবনা এখানেই। Build-কে আলাদা, স্বল্পমেয়াদি পরিবেশে চালানো, সইয়ের উপকরণ build ধাপের নাগালের বাইরে রাখা, আর provenance দিয়ে source থেকে artifact পর্যন্ত যোগসূত্র রাখা। Reproducible build আরও এগোয়: একই source থেকে দুটি স্বাধীন build একই ফল দিলে, একটি builder দখল হলেও ধরা পড়ে।
গ্রাহকের দিক থেকেও একটি শিক্ষা আছে। বিশ্বস্ত update-কেও production-এ সীমিত অনুমতি আর বাইরের network-এ সীমিত পথ দিন, যাতে ভেতরে কিছু ঢুকলেও ক্ষতি ছোট থাকে। তবু কোনোটিই পূর্ণ সমাধান নয়। ২০২৪ সালের xz ঘটনায় (পর্ব ৪১) সমস্যা ছিল release-এর উৎসেই, দীর্ঘদিনের একজন অবদানকারীর মাধ্যমে। সেখানে provenance সততার সাথেই ক্ষতিকর জিনিসের উৎস জানাত। তাই সই, provenance, review, detection, আর production-এ অস্বাভাবিক আচরণ ধরা, সব স্তর আলাদা ধরনের ব্যর্থতা ধরে।
মূল উপলব্ধি: সই পরিচয় আর অখণ্ডতা প্রমাণ করে, নিরাপত্তা নয়। প্রতিটি প্রমাণপত্রের কাছে শুধু সেই প্রশ্নই করুন, যার উত্তর সে সত্যিই দিতে পারে।
প্র ০২সব critical finding-এ pipeline block করলেই কি সবচেয়ে নিরাপদ হয়?
শুনতে নিরাপদ, বাস্তবে প্রায়ই উল্টো। ধরুন একটি base image-এ নতুন critical CVE প্রকাশ হলো, fix এখনো নেই। কঠোর gate হলে সেদিন সব দলের সব PR আটকে যাবে, যাঁরা সেই library ছুঁয়েও দেখেননি তাঁদেরও। আর বিপজ্জনক দিক হলো: এই gate সেই দিনটিতেও আটকাবে, যেদিন আপনার জরুরি একটি নিরাপত্তা-fix deploy করা দরকার। নিরাপত্তার নিয়মই তখন নিরাপত্তার পথ বন্ধ করে।
মানুষের আচরণও হিসাবে ধরতে হয়। Gate বারবার ভুল করলে দল পথ খোঁজে: continue-on-error, স্থায়ী ignore, বা admin হিসেবে merge. Software নিরাপত্তায় এই শিক্ষা পুরোনো। Microsoft-এর Security Development Lifecycle ২০০০-এর দশকে “bug bar” চালু করে: আগে থেকে ঠিক করা থাকে কোন শ্রেণির bug release আটকাবে, যাতে প্রতিবার তর্ক করতে না হয়। ভালো gate সেই ধারণারই আধুনিক রূপ। নিয়মটি আগেই লেখা থাকলে সংকটের দিন তর্ক কমে, আর সিদ্ধান্ত একজনের মেজাজের ওপর নির্ভর করে না।
তাই নিয়মে ব্যতিক্রমের পথ রাখুন, কিন্তু সেটি দৃশ্যমান করুন। জরুরি break-glass থাকবে, যেখানে কে, কেন এড়াল তা log হয়, আর পরে পর্যালোচনা হয়। Waiver-এর মেয়াদ থাকবে। Severity-র সাথে fix, reachability, KEV-এর মতো প্রমাণ মিলবে। EPSS-এর মতো সম্ভাবনার স্কোরও সাহায্য করতে পারে। আর “এই সংখ্যায় block” সিদ্ধান্তটি কোডে থাকবে, তাই সেটি review করা যাবে।
উল্টো ভুলও আছে। শুধু warn হলে কেউ দেখে না। তাই সর্বোচ্চ-আস্থার কিছু নিয়ম, যেমন যাচাই করা live secret আর fix থাকা সক্রিয়ভাবে ব্যবহৃত ত্রুটি, block-ই থাকা উচিত।
মূল উপলব্ধি: Gate-এর লক্ষ্য লাল বাতি জ্বালানো নয়, ঝুঁকি কমানো। মানুষ যে gate মানে, কঠোর কিন্তু এড়িয়ে যাওয়া gate-এর চেয়ে সেটি বেশি নিরাপদ।
প্র ০৩OIDC-তে কি সত্যিই কোনো secret নেই? তাহলে এখন বিশ্বাসটা কোথায় গেল?
জমা থাকা দীর্ঘমেয়াদি secret নেই, কিন্তু বিশ্বাস উধাও হয়নি, সরে গেছে। পর্ব ৩৩-এর “secret zero” সমস্যা মনে করুন: secret পেতে একটি প্রথম secret লাগে। OIDC এর উত্তর দেয় platform-কে সাক্ষী বানিয়ে। Job কিছু প্রমাণ করে না, CI platform ঘোষণা করে “এটি এই repository-র এই branch-এর job”, আর cloud সেই ঘোষণা মানে।
ফলে বিশ্বাস এখন তিন জায়গায়। প্রথমত, issuer: CI platform-এর signing key নিরাপদ, আর সে সঠিক claim লেখে। দ্বিতীয়ত, trust policy: আপনার লেখা শর্তগুলো ঠিকঠাক সংকীর্ণ। তৃতীয়ত, repository-র নিয়ন্ত্রণ: main-এ কে কোড ঢোকাতে পারে। এই তৃতীয়টিই প্রায়ই ভুলে যাওয়া হয়। Branch protection দুর্বল হলে, বা CI ফাইল কেউ review ছাড়া বদলাতে পারলে, sub মিলবে ঠিকই, কিন্তু চলবে অনাকাঙ্ক্ষিত কোড।
Credential পাওয়ার পরেও ঝুঁকি থাকে। Job চলাকালে একটি ক্ষতিকর ধাপ সেই স্বল্পমেয়াদি credential-এর নাগাল পেতে পারে। তাই মেয়াদ ছোট রাখুন, role-এর অনুমতি সীমিত রাখুন, আর deploy-এর job-এ বাইরের ধাপ কম রাখুন। Bearer token যার হাতে সে-ই ব্যবহার করতে পারে, এই সত্য বদলায়নি। তফাত হলো ক্ষতির জানালা মাসের বদলে মিনিটে নেমেছে। Trust policy-র পরিবর্তনও তাই IaC-তে রাখুন, যাতে সেটি review ছাড়া চওড়া না হয়।
লাভটি তবু বিশাল। ফাঁস হওয়ার মতো স্থায়ী কিছু নেই, ঘোরানোর কাজ নেই, আর প্রতিটি ব্যবহার একটি run-এর সাথে যুক্ত। পর্ব ৩১-এর temporary credential-এর ধারণাই এখানে machine পরিচয়ে এসেছে।
মূল উপলব্ধি: OIDC secret-এর ঝুঁকিকে নিয়মের ঝুঁকিতে বদলায়। ভালো বদল, কারণ নিয়ম পড়া, review আর পরীক্ষা করা যায়, ফাঁস হওয়া key-এর খোঁজ রাখা যায় না।
প্র ০৪Pin করলে নিরাপত্তা-fix নিজে থেকে আসে না। তাহলে pin ভালো, না স্বয়ংক্রিয় হালনাগাদ?
দুটি ঝুঁকি মুখোমুখি। ২০২১ সালের ডিসেম্বরে Log4Shell (CVE-2021-44228) প্রকাশের পর যাঁরা দ্রুত হালনাগাদ করতে পেরেছিলেন, তাঁরা এগিয়ে ছিলেন। উল্টো দিকে ২০২৫ সালের tj-actions ঘটনায় যাঁরা tag-এ চলছিলেন, তাঁদের কাছে ক্ষতিকর কোড নিজে থেকে পৌঁছায়। প্রথম ঘটনা বলে “দ্রুত নিন”, দ্বিতীয়টি বলে “যাচাই না করে নেবেন না”.
দ্বন্দ্বটি মেটে যখন আমরা দুটি কাজ আলাদা করি: কী চলছে তা নির্দিষ্ট থাকবে, আর বদলানো হবে একটি পরীক্ষিত পথে। Pin বলে আজ ঠিক কী চলছে। Dependabot বা Renovate PR খোলে। CI সেটি পরীক্ষা করে, মানুষ বা নিয়ম merge করে, আর lockfile ইতিহাস রাখে। তাই pin মানে হালনাগাদ বন্ধ নয়, হালনাগাদ দৃশ্যমান।
গতি ঠিক করতে ঝুঁকি ভাগ করুন। সাধারণ হালনাগাদে কয়েক দিনের cooldown ভালো, কারণ ক্ষতিকর release প্রায়ই দ্রুত ধরা পড়ে। কিন্তু জানা, সক্রিয়ভাবে ব্যবহৃত ত্রুটির fix-এ এই অপেক্ষা বাদ দেওয়ার পথ থাকবে। Patch হালনাগাদে test পাস করলে auto-merge কিছু দলের কাজে লাগে, তবে deploy-এর ক্ষমতাধর CI action-এ মানুষের চোখ রাখা ভালো।
এর পূর্বশর্ত ভালো test আর ছোট, ঘনঘন হালনাগাদ। ছয় মাস পরে একসাথে ৮০টি dependency বাড়ানো ভীতিকর, প্রতি সপ্তাহে পাঁচটি সহজ। এখানে DevOps-এর পুরোনো শিক্ষাই খাটে: যা কষ্টকর, তা বেশি বেশি করুন, তাহলে সেটি সহজ হয়ে যাবে। Security update যে দলের কাছে রুটিন, জরুরি দিনেও তাদের হাত কাঁপে না।
মূল উপলব্ধি: প্রশ্নটি pin বনাম update নয়। Pin ঠিক করে কী চলছে, আর স্বয়ংক্রিয় PR ঠিক করে বদল কীভাবে আসবে। দুটি একসাথেই লাগে।
প্র ০৫Log4Shell-এর দিন অনেক দল জানতই না log4j কোথায় আছে। SBOM কি এই সমস্যার সমাধান?
২০২১ সালের ডিসেম্বরে অনেক প্রতিষ্ঠানের প্রথম প্রশ্ন ছিল “আমরা কি আক্রান্ত?” উত্তর পেতে দিন বা সপ্তাহ লেগেছিল, কারণ log4j প্রায়ই সরাসরি dependency ছিল না, অন্য library-র ভেতরে কয়েক স্তর নিচে ছিল। একই বছরের মে মাসে মার্কিন Executive Order 14028 সরকারি সরবরাহে SBOM-কে সামনে এনেছিল, আর Log4Shell সেই যুক্তিকে সবার সামনে স্পষ্ট করে দেয়।
SBOM ঠিক এই প্রশ্নের দ্রুত উত্তর দিতে পারে, শর্ত সাপেক্ষে। প্রতিটি build-এ তৈরি হতে হবে, digest-এর সাথে যুক্ত থাকতে হবে, আর খোঁজা যায় এমন এক জায়গায় জমা থাকতে হবে। PDF হয়ে ফোল্ডারে পড়ে থাকলে লাভ নেই। আর জানতে হবে কোন digest এখন আসলে চলছে, তাই deploy-এর হিসাবের সাথে মেলাতে হয়।
সীমাও আছে। Tool যা চেনে না, তা তালিকায় নেই। Java-র fat JAR-এর ভেতরের জিনিস, হাতে কপি করা binary, বা build-এর পরে নামানো কিছু বাদ পড়তে পারে। “আছে” মানেই “ঝুঁকি আছে” নয়: কোডটি হয়তো চলেই না। এখানে VEX আসে, যা জানায় নির্দিষ্ট CVE-তে পণ্যটি আক্রান্ত কি না। তৃতীয়ত, SBOM নিজেও একটি দাবি, তাই সই না থাকলে তার ওজন কম। চতুর্থত, SPDX আর CycloneDX দুই ফরম্যাটের tool-এর মান এক রকম নয়।
আসল মূল্য আসে তালিকাকে প্রক্রিয়ায় জুড়লে: নতুন CVE এলে স্বয়ংক্রিয়ভাবে জমা SBOM-এর সাথে মিলিয়ে মালিক আর সময়সীমাসহ ticket খোলা। তখন SBOM আর কাগজ নয়, এটি একটি প্রশ্ন করার মতো ডেটাবেস। MTTR-এর “শনাক্ত” অংশ এখান থেকেই ছোট হয়।
মূল উপলব্ধি: SBOM সমাধান নয়, দ্রুত উত্তরের ভিত্তি। উপকরণের তালিকা থাকলেই খাবার নিরাপদ হয় না, কিন্তু নষ্ট উপকরণের খবর এলে কোন প্যাকেট ফেরত নিতে হবে তা জানা যায়।
অনুশীলন
এগুলোর জন্য কোনো cloud account লাগে না, তাই খরচও নেই।
-
Numerical: সময় আর precision. একটি দলের ৪টি বন্ধ finding-এ লেগেছে ৪, ৬, ১০ আর ৪০ দিন। আরও একটি খোলা আছে, বয়স ৫০ দিন। (ক) বন্ধগুলোর গড় আর median কত? (খ) খোলাটি ধরলে গড় অন্তত কত? (গ) একটি rule ৮০টি alert দিল, যার ২০টি আসল। Precision কত?
(ক) গড় = (৪ + ৬ + ১০ + ৪০) ÷ ৪ = ৬০ ÷ ৪ = ১৫ দিন। Median = (৬ + ১০) ÷ ২ = ৮ দিন।
(খ) অন্তত (৬০ + ৫০) ÷ ৫ = ১১০ ÷ ৫ = ২২ দিন। “অন্তত”, কারণ finding-টি এখনো চলছে।
(গ) ২০ ÷ ৮০ = ২৫%. এই rule block-এর জন্য দুর্বল, warn হিসেবে চালিয়ে ছাঁটাই ভালো।
-
Design বা critique: একটি workflow-তে আছে
on: pull_request_target,permissions: write-all, PR-এর head checkout,npm test, আরuses: example-org/deploy@mainযাতেAWS_SECRET_ACCESS_KEYদেওয়া। চারটি সমস্যা আর প্রতিকার লিখুন।- অবিশ্বস্ত কোড secret-এর নাগালে: test-এর জন্য সাধারণ
pull_requestব্যবহার করুন, secret ছাড়া। - বেশি অনুমতি: Default
contents: read, লেখার অনুমতি শুধু যে job-এ লাগে। - Branch-এ action: পুরো commit SHA-তে pin, আর Dependabot বা Renovate.
- স্থায়ী key আর PR-এ deploy: Deploy আলাদা workflow-তে, শুধু
main-এ push-এর পরে, OIDC দিয়ে, আরsub-এ repository ও branch-এর শর্ত।
- অবিশ্বস্ত কোড secret-এর নাগালে: test-এর জন্য সাধারণ
-
ফল অনুমান করুন: Trust policy-তে
aud=sts.example.comআরStringLikesub=repo:example-org/shop-*:ref:refs/heads/main. চারটি token, সবকটিরaudঠিক: (ক)repo:example-org/shop-api:ref:refs/heads/main(খ)repo:example-org/shop-api:ref:refs/heads/dev(গ)repo:example-org/shop-old:ref:refs/heads/main(ঘ)repo:example-org/shop-api:pull_request. কে ঢুকবে?(ক) ALLOW. (খ) DENY, branch মেলে না। (গ) ALLOW, আর এটিই লুকানো সমস্যা:
shop-*-এর মধ্যে পুরোনো repository-ও পড়ে। (ঘ) DENY,:ref:refs/heads/mainনেই। সমাধান: wildcard বাদ দিয়ে নির্দিষ্ট repository-র তালিকা। -
হাতে-কলমে:
audit_workflow.sh-এactions/checkout@v7বদলে ৪০ অক্ষরের একটি ভুয়া SHA লিখুন, আরwrite-allলাইনেpermissions: read-allদিন। আবার চালালে findings কত হবে? আগে অনুমান করে তারপর মেলান। Docker থাকলে নিজের একটি image-এdocker history --no-truncচালিয়েও দেখুন।Findings হবে ২। থাকবে
pull_request_targetআরlint-action@main. বাকি দুটি চলে যাবে।read-all-ও আদর্শ নয়, কারণ দরকারের চেয়ে বেশি পড়তে দেয়, যদিও script তা ধরে না। তাই findings শূন্য হওয়া মানে নিরাপদ হওয়া নয়, শুধু এই নিয়মগুলো পাস করা।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমCI job থেকে cloud credential পাওয়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Job-কে
id-token: writeঅনুমতি দেওয়া - CI issuer স্বাক্ষরিত JWT দেয়
- Job token দেখিয়ে cloud-এর STS-এ role চায়
- Cloud সই, মেয়াদ আর trust policy-র
audওsubযাচাই করে - Job স্বল্পমেয়াদি credential দিয়ে deploy করে
একটি ত্রুটি শুধু চলন্ত web app-এ বাইরে থেকে অনুরোধ পাঠালে দেখা যায়। কোন পরীক্ষা এটি ধরার উপযুক্ত?
- SAST
- DAST
- SCA
- Secret scan
GitHub Actions-এ uses: example-org/tool@v2.4.1 লেখা আছে। এর প্রধান সরবরাহ-শিকল ঝুঁকি কী?
- লেখাটি বৈধ নয়, workflow চলবে না
- Tag দিলে action ধীরে চলে
- Tag অন্য commit-এ সরানো যেতে পারে, ফলে না জেনেই ভিন্ন কোড চলতে পারে
- Tag দিলে সব secret log-এ ছাপা হয়
OIDC trust policy-তে aud মিলছে, কিন্তু sub-এর কোনো শর্ত নেই। সবচেয়ে বড় বিপদ কী?
- Token-এর মেয়াদ আর যাচাই হবে না
- Token-এর সই আর যাচাই হবে না
- Credential স্থায়ী হয়ে যাবে
- একই issuer-এর অন্য repository বা branch-এর job-ও role পেতে পারে
SLSA Build L3 আর L2-এর মূল পার্থক্য কোনটি?
- L3-তে build run আলাদা থাকে, আর সইয়ের উপকরণ build ধাপের নাগালের বাইরে থাকে
- L3 প্রমাণ করে কোডে কোনো নিরাপত্তা-ত্রুটি নেই
- L3-তে প্রথমবার provenance লাগে
- L3-তে ৯০% test coverage বাধ্যতামূলক
Dockerfile-এ ARG API_TOKEN আর --build-arg দিয়ে token দেওয়া হলো, আর একটি RUN সেটি ব্যবহার করল। Image নিয়ে কী সত্য?
- ARG-এর মান কোথাও থাকে না, তাই এটি নিরাপদ
docker history-তে মান দেখা যেতে পারে, তাই build secret ব্যবহার করা উচিত- Token চলন্ত container-এর environment-এ থেকে যায়
- BuildKit নিজে থেকে মানটি encrypt করে রাখে
একটি দলের বন্ধ finding-এ লেগেছে ২, ২, ৪ আর ৩২ দিন। এছাড়া একটি finding ৯০ দিন ধরে খোলা। কোন সিদ্ধান্তটি সবচেয়ে সঠিক?
- গড় ১০ দিন, তাই অবস্থা ভালো, আর কিছু দেখার নেই
- মাত্র ৫টি finding, তাই সংখ্যাই যথেষ্ট প্রমাণ
- গড় ১০ আর median ৩ দিন দেখার পাশাপাশি ৯০ দিনের খোলা finding-টিও আলাদা করে দেখতে হবে
- ৯০ দিনের finding বন্ধ দেখিয়ে দিলেই MTTR ঠিক হয়ে যাবে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- প্রতিটি ধাপে সস্তা পরীক্ষা: secret scan আগে, SCA ও SAST PR-এ, image scan build-এ, DAST staging-এ, সই যাচাই deploy-এ।
- Gate-এর নকশা: নতুন, পৌঁছানো যায়, fix আছে, তাহলে block. Waiver-এর মেয়াদ থাকবে। Live secret সবসময় block.
- CI নিজেই লক্ষ্য: Action পুরো SHA-তে pin, job token সীমিত, অবিশ্বস্ত PR secret থেকে দূরে।
- OIDC: জমা key নেই। Trust policy-তে
audআরsubদিয়ে repository ও branch বাঁধুন, branch protection-সহ। - প্রমাণ আর সংখ্যা: SLSA পথ বলে, সই পরিচয় বলে, SBOM উপকরণ বলে। কোনোটিই নিরাপত্তার নিশ্চয়তা নয়। MTTR আর সবচেয়ে পুরোনো খোলা finding মাপুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৬২ · Platform engineering — internal developer platform আর golden pathপরবর্তী পর্বনিরাপদ পথকে সবার জন্য সহজ পথ বানানো।
- ক্লাউড কম্পিউটিং ও DevOps · কোর্স হোম৮১টি পর্বপুরো কোর্সের রোডম্যাপ।
- পর্ব ৪১ · Registry আর supply chainআগের পর্বDigest, scan, SBOM আর cosign-এর হাতে-কলমে রূপ।
- পর্ব ৩১ · Cloud IAMআগের পর্বRole, trust policy আর temporary credential-এর ভিত্তি।
- Git-এ নিরাপত্তাপাশের ট্র্যাকSigned commit আর secret ফাঁসের পরে করণীয়।
- System Design: নিরাপত্তার নকশাপাশের ট্র্যাকArchitecture পর্যায়ে নিরাপত্তার সিদ্ধান্ত।