সরাসরি পর্বে যান
পর্ব ৪১ · ৮১-এর মধ্যে · মডিউল ৭

Registry আর supply chain — tag, digest, scanning, SBOM আর signing

Container Registries & Supply Chain — Tags, Digests, Scanning, SBOM and Signing

Taught in Bangla, this lesson reads a full image reference, separates moving tags from fixed digests, explains manifests, indexes and layers in push and pull, choosing a registry with short-lived tokens, immutable tags, safe retention and garbage collection, triaging vulnerability scans, rebuilding base images, and using SBOMs, signing and SLSA, with defence against dependency confusion and typosquatting, plus simulators and real output.

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

  • একটি image reference-এর প্রতিটি অংশ পড়া, আর tag ও digest-এর আসল পার্থক্য
  • Manifest, index আর layer কীভাবে push ও pull-কে দ্রুত করে
  • Registry বাছাই, স্বল্পমেয়াদি token, immutable tag আর নিরাপদ retention
  • Scan-এর ফল বাছাই করা, base image নিয়মিত rebuild করা আর false positive সামলানো
  • SBOM, signing, SLSA, dependency confusion আর typosquatting থেকে আত্মরক্ষা

১ · একটি image-এর পুরো ঠিকানা

আগের পর্বগুলোতে আপনি image বানিয়েছেন আর নিজের কম্পিউটারে চালিয়েছেন। কিন্তু production server আপনার ল্যাপটপ থেকে image নেয় না। মাঝখানে লাগে একটি container registry, মানে image রাখার আর দেওয়ার server. এই পর্বের প্রশ্ন দুটি: যে image চলছে, সেটি ঠিক কোনটি? আর সেটির ওপর ভরসা করবেন কেন? দ্বিতীয় প্রশ্নটিই software supply chain-এর বিষয়।

“সকাল ৮টার রাজশাহীর বাস” একটি নাম। আজ এক গাড়ি যাবে, কাল অন্য গাড়ি। আর “ঢাকা মেট্রো-ব ১১-২২৩৩” একটি নির্দিষ্ট গাড়ি। Tag হলো সময়সূচির নাম, digest হলো নম্বর প্লেট। টিকিট কাটতে নামই সুবিধার। কিন্তু দুর্ঘটনার তদন্তে নম্বর প্লেট লাগে।

পুরো ঠিকানাটি এরকম (image reference):

registry.example.com:5000/shop/api:1.4@sha256:a151f7de…
└──────── registry ─────┘└ repository ┘└tag┘└─ digest ─┘
অংশমানেনা লিখলে
Registryকোন server থেকে আসবেDocker ধরে নেয় docker.io
Repositoryএকই app-এর image-গুলোর ঘরDocker Hub-এ এক শব্দের নামের আগে library/ বসে
Tagমানুষের দেওয়া নাম, যা সরানো যায়Docker ধরে নেয় latest
DigestManifest-এর SHA-256 hashTag যেখানে দেখায়, সেটিই নেওয়া হয়

তাই alpine:3.20 আসলে docker.io/library/alpine:3.20. আমাদের build-এর লগেই পুরো রূপটি দেখা গেছে: FROM docker.io/library/alpine:3.20@sha256:d9e853e8…. আর latest কোনো বিশেষ অর্থ বহন করে না। এটি “সবচেয়ে নতুন” নয়, শুধু একটি default নাম। কেউ পুরোনো image-এ এই tag বসালে সেটিই latest.

মূল ধারণা

Digest হলো content addressing: নাম আসে বিষয়বস্তু থেকে। এক byte বদলালে hash বদলাবে, তাই একই digest-এ দুই রকম জিনিস থাকার ব্যবহারিক সুযোগ নেই। Reference-এ tag আর digest দুটোই থাকলে Docker digest মেনে চলে, tag তখন শুধু পড়ার সুবিধার জন্য।

এটি আমরা নিজেরা মিলিয়ে দেখেছি। নিচের সব transcript একটি আসল run-এর: Docker Engine 29, BuildKit, আর এই কম্পিউটারের 127.0.0.1-এ চালানো registry:2 (version 2.8.3). আপনার কম্পিউটারে id, digest, size আর সময় আলাদা হবে। কোনো দূরের registry-তে কিছু push করা হয়নি, আর কাজ শেষে registry ও image মুছে ফেলা হয়েছে।

$ curl -sI -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
    http://127.0.0.1:18411/v2/abcl-cr-api/manifests/v2 | grep -i docker-content
Docker-Content-Digest: sha256:f87d8bb69ec51c4ff4db64d56f76a586b0f2025c3629278634c17179382d1269

$ curl -s -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
    http://127.0.0.1:18411/v2/abcl-cr-api/manifests/v2 | sha256sum
f87d8bb69ec51c4ff4db64d56f76a586b0f2025c3629278634c17179382d1269  -

Registry যে digest জানাল, আর manifest-এর byte নিজে hash করে যা পাওয়া গেল, দুটি হুবহু এক। তাই client registry-কে অন্ধভাবে বিশ্বাস করতে বাধ্য নয়, নিজেই মিলিয়ে নিতে পারে। Manifest-এর ভেতরে প্রতিটি layer-এরও digest আছে, ফলে একটি hash পুরো image-কে বেঁধে রাখে।

২ · Tag সরে যায়, digest সরে না

আসল পরীক্ষাটি দেখুন। প্রথমে abcl-cr-shop:1.4 নামে build A push করা হলো। তারপর ফাইল বদলে একই tag-এ build B push করা হলো। Registry কোনো আপত্তি করেনি।

$ docker push localhost:18411/abcl-cr-shop:1.4     # build A
1.4: digest: sha256:a151f7de7c905758e747d0430a239f6d9807899351bb986c326b8f6725af63b7 size: 855

$ docker push localhost:18411/abcl-cr-shop:1.4     # build B, same tag
1.4: digest: sha256:f8acfc18b6009ff412b17c0f4b05e42f7e0287053eeb5e9edcc64fe5b211ac01 size: 855

$ curl -s http://127.0.0.1:18411/v2/abcl-cr-shop/tags/list
{"name":"abcl-cr-shop","tags":["1.4"]}

$ docker pull localhost:18411/abcl-cr-shop:1.4
Digest: sha256:f8acfc18b6009ff412b17c0f4b05e42f7e0287053eeb5e9edcc64fe5b211ac01
$ docker run --rm localhost:18411/abcl-cr-shop:1.4
shop build B: has a bug

$ docker pull localhost:18411/abcl-cr-shop@sha256:a151f7de7c905758e747d0430a239f6d9807899351bb986c326b8f6725af63b7
Digest: sha256:a151f7de7c905758e747d0430a239f6d9807899351bb986c326b8f6725af63b7
$ docker run --rm localhost:18411/abcl-cr-shop@sha256:a151f7de…
shop build A: stable

Tag তালিকায় একটিই নাম, 1.4. কিন্তু registry-তে দুটি image আছে। Tag দিয়ে নামালে আসে shop build B: has a bug, আর digest দিয়ে নামালে আসে shop build A: stable. পুরোনো image মুছে যায়নি, শুধু তার গায়ের নামটি সরে গেছে। Tag হারানো এমন manifest-কে বলে untagged manifest.

নিচের সিমুলেটরে registry হলো একটি মানচিত্র: tag থেকে digest. একই নাম ভিন্ন সময়ে ভিন্ন image দেয়। IMMUTABLE-কে true করে আবার চালিয়ে দেখুন, বা pushes-এ নতুন লাইন যোগ করুন। এখানকার hash খেলনা, আসল SHA-256 নয়।

// Tag vs digest: a registry as two maps. Change pushes or IMMUTABLE.
const IMMUTABLE = false;            // true: a tag can never point elsewhere
const pushes = [                    // t = simulated minutes
  { t: 10, tag: '1.4',    body: 'build A' },
  { t: 20, tag: 'latest', body: 'build A' },
  { t: 30, tag: '1.4',    body: 'build B' },
  { t: 40, tag: 'latest', body: 'build C' },
];
function digest(s) {                // toy FNV-1a hash, not real SHA-256
  let h = 2166136261;
  for (const c of s) h = Math.imul(h ^ c.charCodeAt(0), 16777619);
  return 'sha:' + (h >>> 0).toString(16).padStart(8, '0');
}
const blobs = new Map(), history = [];
for (const p of pushes) {           // replay the timeline, keep each tag state
  const d = digest(p.body), old = history.findLast(h => h.tag === p.tag);
  blobs.set(d, p.body);
  if (IMMUTABLE && old && old.d !== d) {
    console.log(`t=${p.t} push ${p.tag}: REJECTED, tag is immutable`); continue;
  }
  history.push({ t: p.t, tag: p.tag, d });
  console.log(`t=${p.t} push ${p.tag} -> ${d}` + (old && old.d !== d ? ` (moved from ${old.d})` : ''));
}
const pinned = digest('build A');   // the digest a careful deploy recorded
function resolve(ref, t) {
  if (ref.startsWith('@')) return ref.slice(1);
  return history.findLast(h => h.tag === ref && h.t <= t)?.d;
}
console.log('--- resolve the same names at different times');
for (const [ref, t] of [['1.4', 15], ['1.4', 35], ['latest', 25], ['latest', 45], ['@' + pinned, 45]]) {
  const d = resolve(ref, t);
  console.log(`t=${t} ${ref.padEnd(14)} -> ${d} = ${blobs.get(d)}`);
}

t=15 1.4 -> sha:228f48e8 = build A আর t=35 1.4 -> sha:258f4da1 = build B: একই নাম, ভিন্ন ফল। শেষ লাইন t=45 @sha:228f48e8 -> sha:228f48e8 = build A দেখায় digest সময়ে বদলায় না। t=20-এ latest আর t=10-এর 1.4 একই hash পেয়েছে, কারণ বিষয়বস্তু এক। একটি image-এর অনেক নাম থাকতে পারে।

এবার দেখুন এই আচরণ বাস্তবে কী ক্ষতি করে। একটি কাল্পনিক অনলাইন বাজার app-এর দল rollback করতে গিয়ে কী পেল:

অবস্থা (কাল্পনিক দল, digest সংক্ষেপে)
সময়
Registry: 1.4
Production চলছে
দল যা ভাবছে
  1. সব ঠিক আছে। Build A 1.4 নামে push হলো, পরীক্ষা হলো, production-এ গেল। Digest a151… কোথাও লিখে রাখা হয়নি, কারণ সবাই “১.৪” বলেই চেনে।
  2. নীরবে tag সরল। একটি ছোট সংশোধনের পর CI আবার 1.4 নামেই build B push করল। Production-এ কিছু বদলায়নি, কারণ চলমান container পুরোনো image নিয়েই চলে। তাই কেউ টের পায়নি।
  3. নতুন release. Build C 1.5 নামে production-এ গেল। দলের রানবুকে লেখা আছে, সমস্যা হলে 1.4-এ ফিরতে হবে।
  4. ত্রুটি ধরা পড়ল। 1.5-এ অর্ডার আটকে যাচ্ছে। দল দ্রুত rollback শুরু করল। এতক্ষণ সব পরিকল্পনা মতোই চলছিল।
  5. Rollback ভুল জিনিস আনল। Node-গুলো 1.4 নামাল, আর পেল B. এই build কেউ production-এর মতো পরীক্ষা করেনি। সমস্যা কমল না, নতুন উপসর্গ যোগ হলো। দল যা ভাবছে আর যা চলছে, দুটি আলাদা।
  6. Digest উদ্ধার করল। পুরোনো deployment log-এ পাওয়া গেল a151…. সেই digest দিয়ে deploy করতেই হুবহু A ফিরল। Registry থেকে A মুছে না যাওয়ায় এই পথ খোলা ছিল।
  7. স্থায়ী প্রতিকার। Release tag immutable করা হলো, প্রতিটি deploy-এর digest লগে রাখা হলো, আর rollback হলো digest দিয়ে। আগের ধাপের A আবার নিশ্চিত, আর tag-এর ওপর অন্ধ ভরসা শেষ।

নিয়ম: মানুষের জন্য tag, যন্ত্রের জন্য digest. CI যে digest push করে সেটি ধরে রাখুন, আর deployment-এ সেটিই ব্যবহার করুন। পর্ব ৫৮-এ “একবার build, সব জায়গায় deploy” এই ভিত্তির ওপরই দাঁড়াবে।

৩ · Registry-র ভেতরে: manifest, index আর layer

Registry মূলত একটি content-addressed গুদাম। তিন রকম জিনিস থাকে। Blob হলো layer আর config-এর ফাইল। Manifest হলো একটি ছোট JSON, যা বলে কোন config আর কোন কোন layer মিলে এই image. আর tag হলো একটি নির্দিষ্ট manifest-এর দিকে তাক করা নাম। এই registry-র কথোপকথনের নিয়ম খোলা মানদণ্ড, OCI Distribution Specification.

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:e86b0012a82bc854af136cd21b950eea5384c96750f87fd74afd986bf5f5d62c",
    "size": 1009
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:25f1d6b1951ac8eb3740558fe94cb83d377bdadf95fd9f98b50d2e1b96130471",
      "size": 3630321
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:2d1b1b7444681f44435079c0842f26d6fecce3cf86fb81152b4450c41c0de5b9",
      "size": 159
    }
  ]
}

প্রথম layer 25f1d6b1… হলো alpine, ৩৬ লাখ byte-এর মতো। দ্বিতীয়টি আমাদের ফাইল, মাত্র 159 byte. দুটি build-এই 25f1d6b1… একই, তাই registry সেটি একবারই রাখে। এখানকার সব size সংকুচিত অবস্থার মাপ।

এক নামে অনেক CPU: multi-architecture index

একই alpine:3.20 Intel server, ARM server আর Raspberry Pi-তে চলে। কারণ tag তাক করে একটি image index-এর দিকে, যা প্রতিটি CPU-র জন্য আলাদা manifest-এর তালিকা। Client নিজের platform মিলিয়ে ঠিকটি নেয়। এই সুবিধাকে বলে multi-architecture image.

$ docker buildx imagetools inspect alpine:3.20
Name:      docker.io/library/alpine:3.20
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc
  Platform:    linux/amd64
  Platform:    unknown/unknown
  Platform:    linux/arm/v6
  …
  Platform:    linux/arm64/v8
  …
  Platform:    linux/riscv64
  Platform:    linux/s390x

$ docker buildx imagetools inspect localhost:18411/abcl-cr-shop:1.4
MediaType: application/vnd.oci.image.index.v1+json
  Name:        …abcl-cr-shop:1.4@sha256:395d6ff2…
  Platform:    linux/amd64
  Name:        …abcl-cr-shop:1.4@sha256:fc01e148…
  Platform:    unknown/unknown
    vnd.docker.reference.type:   attestation-manifest

আমরা একটি মাত্র platform-এর জন্য build করলেও BuildKit একটি index বানিয়েছে। unknown/unknown কোনো CPU নয়। attestation-manifest মানে এতে image-টি কীভাবে তৈরি হলো তার তথ্য আছে (অংশ ৭-এ দেখবেন)। সতর্কতা: index-এর digest আর একটি platform-এর manifest-এর digest আলাদা। সব CPU-র জন্য এক digest চাইলে index-এর digest ব্যবহার করতে হবে। docker manifest inspect-ও একই তালিকা JSON আকারে দেখায়।

Push আর pull: শুধু যা নেই

ঈদের আগে পুরো সংসার গ্রামে না নিয়ে শুধু যা সেখানে নেই সেটি নিয়ে যাওয়ার মতো। Client আগে জিজ্ঞেস করে, “এই digest-এর blob আছে?” থাকলে পাঠায় না। এখানে একটি app নিন। এর layer তিনটি: alpine (৩৬,৩০,৩২১ byte), dependency (২,০৯,৭৮,০৯৭ byte) আর app ফাইল (১১০ byte). পরের version-এ শুধু app ফাইল বদলাবে।

Registry-র log আর ফল
কাজ
alpine layer
dependency layer
app layer
Blob upload
Blob download
  1. Build v1. তিনটি layer তৈরি। abcl-cr-api repository এখনো খালি।
  2. আগে জিজ্ঞেস। Client প্রতিটি blob-এর জন্য HEAD পাঠায়। Log-এ এই repository-তে চারটি HEAD blob অনুরোধ এসেছে। উত্তর: কিছুই নেই। তবে Docker জানে alpine layer পাশের repository-তে আছে।
  3. তোলা। Alpine layer-এর byte আসেনি, registry ভেতরেই জুড়ে নিয়েছে: Mounted from abcl-cr-shop. একে বলে blob mount. বাকি dependency, app আর config, মোট তিনটি blob ওঠায় log-এ PUT তিনটি।
  4. Build v2. শুধু app ফাইল বদলেছে। Dependency-র ধাপ CACHED, তাই তার digest আগের মতোই 7d2c0b96….
  5. আবার যাচাই। এবার HEAD জানাল alpine আর dependency আছে। পর্দায় এল Layer already exists.
  6. শুধু নতুনটুকু। ওঠে ১০৯ byte-এর app layer আর config, মোট দুটি blob. Registry-র ডিস্কে blob-এর মাপ 24264 KiB থেকে হলো 24296 KiB. প্রায় ২০ MB আবার ওঠেনি।
  7. Pull-এও একই নিয়ম। আরেকটি image নিন, যা alpine-এর ওপর ৮ MB-র নতুন layer যোগ করে। এই কম্পিউটারে নতুন layer ছিল না, alpine ছিল। Log-এ blob GET মাত্র দুটি: নতুন layer আর config. Alpine নামেনি।
  8. আবার pull. সব আগেই আছে। পর্দায় Image is up to date, আর কোনো layer-এর লাইন নেই।
$ docker push localhost:18411/abcl-cr-api:v1
edd22db6defa: Pushed
25f1d6b1951a: Mounted from abcl-cr-shop
7d2c0b965f82: Pushed
v1: digest: sha256:bd2de108de54422263f3f944ca11627e095671cb231c6832dfeb545a3e73d6e0 size: 896

$ docker push localhost:18411/abcl-cr-api:v2
8a4f2f418467: Pushed
25f1d6b1951a: Layer already exists
7d2c0b965f82: Layer already exists
v2: digest: sha256:f87d8bb69ec51c4ff4db64d56f76a586b0f2025c3629278634c17179382d1269 size: 896

$ docker pull localhost:18411/abcl-cr-report:v1     # alpine already on this machine
v1: Pulling from abcl-cr-report
c9d6c1cf503b: Pulling fs layer
c9d6c1cf503b: Download complete
c9d6c1cf503b: Pull complete
Digest: sha256:d37318293bfcf717b8d75725e217b071b57ce5c8e22b6f5d3794c794da5974dc
Status: Downloaded newer image for localhost:18411/abcl-cr-report:v1

Mounted from, Layer already exists আর Pushed: তিনটি শব্দে তিন আচরণ। Pull-এর transcript-এ শুধু c9d6c1cf503b-এর লাইন আছে। Alpine-এর লাইন এই run-এ আসেনি। এখানে image-টি API দিয়ে সরাসরি registry-তে রাখা হয়েছিল, যাতে নতুন layer-টি এই কম্পিউটারে সত্যিই না থাকে। Image store-ভেদে পর্দার লেখা আলাদা হতে পারে, কখনো Already exists লাইনও দেখবেন।

এজন্যই ভালো Dockerfile-এ যা কম বদলায় তা নিচে থাকে। পর্ব ৩৭-এর layer cache শুধু build-এর সময়ই বাঁচায় না, registry-র জায়গা আর node-এর pull-এর সময়ও বাঁচায়।

৪ · কোন registry, আর ঢোকার চাবি কী

সব registry একই API বলে, তাই image এক জায়গা থেকে অন্য জায়গায় নেওয়া যায়। পার্থক্য হয় পরিচয়, নিয়ন্ত্রণ আর খরচে। নিচের তথ্য লেখার সময়ের সাধারণ চিত্র। সুবিধা ও সীমা provider আর plan-ভেদে বদলায়, তাই অফিসিয়াল নথি দেখে নিন।

Registryকোথায় মানায়পরিচয়Tag ও পরিষ্কার
Docker HubPublic image, official imageAccount, access tokenPlan-ভেদে আলাদা
GHCR (GitHub)GitHub-কেন্দ্রিক projectActions-এর GITHUB_TOKENAPI বা workflow দিয়ে পরিষ্কার
Amazon ECRAWS workloadIAM, স্বল্পমেয়াদি login tokenTag immutability, lifecycle policy
Azure ACRAzure workloadEntra ID, managed identityTag ও repository lock, purge
Google Artifact RegistryGoogle Cloud workloadIAM, service accountImmutable tag, cleanup policy

Rate limit: Docker Hub অনেক দিন ধরে pull-এর সংখ্যায় rate limit রাখে। পরিচয় ছাড়া pull সাধারণত IP ধরে গোনা হয়, আর NAT-এর পেছনের অনেক node একটি IP ভাগ করে। তাই CI বা নতুন node হঠাৎ 429 Too Many Requests পেতে পারে। অঙ্ক বদলায়, তাই কোনো সংখ্যা মুখস্থ করবেন না। প্রতিকার হলো পরিচয় দিয়ে pull করা, আর একটি pull-through cache বা নিজের private registry-তে দরকারি base image-এর কপি রাখা।

Private registry আর স্বল্পমেয়াদি token

নিয়মটি পর্ব ৩১-এর temporary credential আর least privilege-এরই ধারাবাহিকতা। CI-তে চিরস্থায়ী password রাখলে তা ফাঁস হওয়ার ঝুঁকি দীর্ঘ। বরং CI নিজের OIDC পরিচয় দিয়ে cloud থেকে অল্প সময়ের token নেয়। Build job শুধু নিজের repository-তে push করতে পারে, আর production node শুধু pull করতে পারে। AWS-এর উদাহরণ:

# Token লেখার সময়ে প্রায় ১২ ঘণ্টা বৈধ; docker login-এ password-এর জায়গায় বসে
aws ecr get-login-password --region ap-south-1 \
  | docker login --username AWS --password-stdin \
    123456789012.dkr.ecr.ap-south-1.amazonaws.com

Cloud-এর ভেতরের VM বা managed Kubernetes-এ সাধারণত login লাগেই না। Node-এর role বা managed identity-তে pull-এর অনুমতি দিলেই চলে। Token কখনো Dockerfile-এ লিখবেন না, কারণ layer থেকে তা বের করা যায়। পর্ব ৬১-এ OIDC-র পুরো পথ দেখবেন।

খরচের একটি কথা: Image টানা মানেই network traffic. Registry অন্য region-এ বা public internet-এ থাকলে NAT gateway-র খরচ আর data transfer যোগ হতে পারে। তাই image-এর ঘর workload-এর একই region-এ রাখা সাধারণত সস্তা আর দ্রুত।

৫ · Immutable tag, retention আর cleanup

Immutable tag চালু থাকলে একবার push করা tag-এ অন্য image বসানো যায় না, push ব্যর্থ হয়। তখন 1.4 সবসময় একটিই image. এতে অংশ ২-এর দুর্ঘটনা বন্ধ হয়। তবে latest বা main-এর মতো চলমান নাম তখন আর ব্যবহার করা যাবে না, অথবা আলাদা repository-তে রাখতে হবে। Release-এ version বা commit SHA-ভিত্তিক tag ভালো।

aws ecr put-image-tag-mutability \
  --repository-name shop \
  --image-tag-mutability IMMUTABLE

এবার জমে থাকা image. প্রতিটি commit-এ একটি image মানে কয়েক মাসেই হাজার হাজার image, আর জমা রাখার বিল। তাই retention নিয়ম লাগে, ঠিক object storage-এর lifecycle rule-এর মতো। কিন্তু একটি ফাঁদ আছে। আমাদের registry-তে v1 মুছে দেখা গেল:

$ du -s …/blobs                       # KiB, inside the registry container
32144
$ curl -s -o /dev/null -w 'DELETE %{http_code}\n' -X DELETE …/v2/abcl-cr-api/manifests/sha256:bd2de108…
DELETE 202
$ curl -s …/v2/abcl-cr-api/tags/list
{"name":"abcl-cr-api","tags":["v2"]}
$ du -s …/blobs
32144                                 # nothing changed yet

$ registry garbage-collect --dry-run /etc/docker/registry/config.yml
…
22 blobs marked, 3 blobs and 0 manifests eligible for deletion
$ registry garbage-collect /etc/docker/registry/config.yml
$ du -s …/blobs
32120

DELETE-এর পরেও 32144 KiB. Manifest গেছে, blob রয়ে গেছে, কারণ অন্য image একই blob ব্যবহার করতে পারে। Garbage collection সব manifest ঘুরে যে blob-এ কেউ তাক করে না শুধু সেগুলো মোছে। এখানে মাত্র ৩টি blob গেল (v1-এর manifest, config আর ১১০ byte-এর layer), ফাঁকা হলো 24 KiB, কারণ ২০ MB-র dependency layer v2 ব্যবহার করছে। এই registry-তে GC চলার সময় নতুন push বন্ধ রাখতে হয়। Managed registry সাধারণত এটি নিজেরাই সামলায়, আর বিল অনুযায়ী জায়গা কমতে কিছুটা দেরি হতে পারে।

এবার আরও কড়া নিয়মের আসল ফল। --delete-untagged সব tag-বিহীন manifest মোছে। অংশ ২-এর build A ছিল tag-বিহীন। দেখুন কী হলো:

$ registry garbage-collect --delete-untagged /etc/docker/registry/config.yml
11 blobs marked, 11 blobs and 5 manifests eligible for deletion

$ docker pull localhost:18411/abcl-cr-shop@sha256:a151f7de…     # build A
Error response from daemon: failed to resolve reference … not found

# worse: the tag still answers, but its platform manifest is gone
$ curl … -w 'tag index %{http_code}\n' …/abcl-cr-shop/manifests/1.4
tag index 200
$ curl … -w 'amd64 child %{http_code}\n' …/abcl-cr-shop/manifests/sha256:5a6aab8a…
amd64 child 404
$ docker pull localhost:18411/abcl-cr-shop:1.4
short read: expected 1052 bytes but got 0: unexpected EOF

দুটি শিক্ষা। এক, digest কখনো বদলায় না, কিন্তু চিরকাল থাকার নিশ্চয়তা দেয় না। Rollback-এর digest যদি cleanup মুছে দেয়, পথ বন্ধ। দুই, এই run-এ 1.4-এর index-কে বাঁচানো হলেও তার ভেতরের platform manifest tag-বিহীন বলে মুছে গেছে, ফলে চালু tag-টিই pull করা যায়নি। এটি এই version-এ multi-architecture index-এর সাথে এই flag-এর আচরণ। অন্য version বা managed registry-তে ফল আলাদা হতে পারে। তাই যেকোনো cleanup আগে --dry-run-এ দেখুন, আর আসল কাজের পর একটি পরিষ্কার client থেকে pull করে যাচাই করুন।

নিচের সিমুলেটরে একটি নমুনা repository-র ওপর নিয়ম চালানো হচ্ছে। protect running images প্রথমে বন্ধ, পরে চালু। KEEP_LAST_RELEASES বদলে দেখুন কোন image টিকে যায়।

// Retention rule over a SAMPLE repository. Change the rule values.
const KEEP_LAST_RELEASES = 2, PR_MAX_AGE = 7, UNTAGGED_MAX_AGE = 14;
const LAYER_MB = { base: 8, deps: 120, a1: 5, a2: 5, a3: 5, a4: 5, p1: 5, p2: 5 };
const images = [        // age in days; running = used by production right now
  { ref: 'rel-1.1', age: 90, running: false, layers: ['base', 'deps', 'a1'] },
  { ref: 'rel-1.2', age: 60, running: true,  layers: ['base', 'deps', 'a2'] },
  { ref: 'rel-1.3', age: 20, running: false, layers: ['base', 'deps', 'a3'] },
  { ref: 'rel-1.4', age: 3,  running: false, layers: ['base', 'deps', 'a4'] },
  { ref: 'pr-77',   age: 12, running: false, layers: ['base', 'deps', 'p1'] },
  { ref: 'pr-81',   age: 2,  running: false, layers: ['base', 'deps', 'p2'] },
  { ref: 'untagged', age: 30, running: false, layers: ['base', 'deps', 'p1'] },
];
const releases = images.filter(i => i.ref.startsWith('rel-')).sort((a, b) => a.age - b.age);
function verdict(img, protect) {
  if (protect && img.running) return 'KEEP   running in production';
  if (img.ref === 'untagged') return img.age > UNTAGGED_MAX_AGE ? 'DELETE old and untagged' : 'KEEP   young untagged';
  if (img.ref.startsWith('pr-')) return img.age > PR_MAX_AGE ? 'DELETE old PR build' : 'KEEP   recent PR';
  return releases.indexOf(img) < KEEP_LAST_RELEASES ? 'KEEP   recent release' : 'DELETE old release';
}
const size = i => i.layers.reduce((t, l) => t + LAYER_MB[l], 0);
for (const protect of [false, true]) {
  console.log(`--- protect running images: ${protect}`);
  const out = images.map(i => ({ i, v: verdict(i, protect) }));
  const del = out.filter(x => x.v.startsWith('DELETE'));
  const kept = new Set(out.filter(x => !del.includes(x)).flatMap(x => x.i.layers));
  const gone = new Set(del.flatMap(x => x.i.layers).filter(l => !kept.has(l)));
  for (const { i, v } of out) {
    const warn = i.running && v.startsWith('DELETE') ? '  <-- OUTAGE RISK' : '';
    console.log(`${i.ref.padEnd(8)} ${String(i.age).padStart(2)}d ${v}${warn}`);
  }
  const freed = [...gone].reduce((s, l) => s + LAYER_MB[l], 0);
  console.log(`freed after GC: ${freed} MB, sum of image sizes: ${del.reduce((s, x) => s + size(x.i), 0)} MB`);
}

প্রথম ভাগে rel-1.2 60d DELETE old release <-- OUTAGE RISK: নিয়ম শুধু বয়স আর ক্রম দেখেছে, অথচ এই image production-এ চলছে। সেটি মুছলে নতুন node উঠতে পারবে না। দ্বিতীয় ভাগে KEEP running in production. শেষ লাইনটিও লক্ষ করুন: freed after GC: 10 MB, sum of image sizes: 399 MB. Layer ভাগাভাগির কারণে তিনটি image মুছলে ৩৯৯ MB নয়, ফেরে মাত্র ১০ MB. untagged আর pr-77 একই p1 layer ব্যবহার করে, তাই দুটি মুছলে তবেই সেটি ফাঁকা হয়।

নিরাপদ নিয়মের কাঠামো: চলমান digest-এর তালিকা আগে বের করুন, সেগুলো রক্ষা করুন, শেষ কয়েকটি release রাখুন, PR build দ্রুত মুছুন, আর নিয়ম প্রথমে শুধু রিপোর্ট আকারে চালান। AWS-এর lifecycle policy-র একটি নিয়ম এরকম:

{
  "rules": [
    {
      "rulePriority": 1,
      "description": "Expire untagged images after 14 days",
      "selection": {
        "tagStatus": "untagged",
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 14
      },
      "action": { "type": "expire" }
    }
  ]
}

এই নিয়ম জানে না কোন image চলছে। Deployment যদি digest দিয়ে tag-বিহীন image চালায়, ১৪ দিন পরে সেটি মুছে যেতে পারে। তাই release image-এ সবসময় একটি স্থায়ী tag রাখুন, deployment digest-এ হলেও।

৬ · Vulnerability scanning: তালিকা থেকে সিদ্ধান্ত

আপনার image-এ আপনার লেখা কোড সামান্য। বাকিটা base image, OS package আর library. এগুলোর জানা ত্রুটি প্রকাশিত হয় CVE নম্বরে। Vulnerability scanning image-এর package তালিকা বের করে সেটি ত্রুটির ডেটাবেসের সাথে মেলায়। এটি কোড চালিয়ে আক্রমণ করে দেখে না। তাই নতুন, অজানা ত্রুটি ধরে না, আর ফল ডেটাবেস যত টাটকা ততটাই ভালো।

Open-source দুটি জনপ্রিয় টুল Trivy (Aqua Security) আর Grype (Anchore). ECR, ACR আর Artifact Registry-র সাথেও scanning সেবা যুক্ত করা যায়। এই লেখার কম্পিউটারে Trivy বা Grype ইনস্টল করা নেই, তাই নিচের কমান্ড চালানো হয়নি। JSON-টি ফরম্যাট বোঝানোর নমুনা, id-গুলো বানানো, আসল scan-এর ফল নয়।

# HIGH/CRITICAL only, JSON report, non-zero exit if found
trivy image --severity HIGH,CRITICAL --exit-code 1 \
  --format json -o scan.json registry.example.com/shop/api@sha256:...

# Grype: fail the job at critical severity
grype registry.example.com/shop/api@sha256:... --fail-on critical
{
  "ArtifactName": "registry.example.com/shop/api",
  "Results": [
    {
      "Target": "registry.example.com/shop/api (alpine 3.20)",
      "Class": "os-pkgs",
      "Type": "alpine",
      "Vulnerabilities": [
        {
          "VulnerabilityID": "CVE-SAMPLE-0003",
          "PkgName": "zlib",
          "InstalledVersion": "1.3.1-r0",
          "FixedVersion": "1.3.1-r1",
          "Severity": "CRITICAL"
        }
      ]
    }
  ]
}

রিপোর্ট প্রায়ই লম্বা হয়, তাই আগে সংক্ষেপ করতে হয়। নিচের ব্লকে একটি নমুনা CSV রিপোর্ট awk দিয়ে ছাঁকা হয়েছে, আর শেষে একটি সহজ release gate. এটি Linux-এর আসল Bash-এ চলে।

set -euo pipefail
# SAMPLE scanner export. Fake IDs, not from a real scan.
cat > scan.csv <<'CSV'
id,package,installed,fixed,severity,cvss
CVE-SAMPLE-0001,libfoo,1.2.0,,CRITICAL,9.8
CVE-SAMPLE-0002,openssl,3.1.4-r5,3.1.4-r6,HIGH,7.5
CVE-SAMPLE-0003,zlib,1.3.1-r0,1.3.1-r1,CRITICAL,9.1
CVE-SAMPLE-0004,busybox,1.36.1-r29,1.36.1-r30,MEDIUM,5.5
CVE-SAMPLE-0005,webframe,4.2.0,,HIGH,8.2
CVE-SAMPLE-0006,gcc-libs,13.2.1-r3,,LOW,2.9
CSV

echo "== count by severity"
tail -n +2 scan.csv | cut -d, -f5 | sort | uniq -c | sort -rn

echo "== CRITICAL or HIGH with a fix (act first)"
awk -F, 'NR>1 && $4!="" && ($5=="CRITICAL" || $5=="HIGH") {
  printf "%-16s %-8s %s -> %s\n", $1, $2, $3, $4 }' scan.csv

echo "== CRITICAL or HIGH without a fix (needs a decision)"
awk -F, 'NR>1 && $4=="" && ($5=="CRITICAL" || $5=="HIGH") {
  printf "%-16s %-8s cvss %s\n", $1, $2, $6 }' scan.csv

fixable=$(awk -F, 'NR>1 && $4!="" && $5=="CRITICAL"' scan.csv | wc -l)
if [ "$fixable" -gt 0 ]; then
  echo "GATE: block release, $fixable fixable CRITICAL"
else
  echo "GATE: pass"
fi

দুই ভাগে আলাদা করার কারণ, দুটির কাজ আলাদা। CVE-SAMPLE-0003 zlib 1.3.1-r0 -> 1.3.1-r1-এর ক্ষেত্রে সমাধান তৈরি, শুধু rebuild দরকার। CVE-SAMPLE-0001 libfoo cvss 9.8-এর fix নেই, তাই মানুষের সিদ্ধান্ত লাগবে। Gate থামিয়েছে: GATE: block release, 1 fixable CRITICAL. এখানে শুধু লেখা ছাপা হচ্ছে। আসল pipeline-এ এই ধাপ non-zero exit দিয়ে job থামায়।

Triage: সবচেয়ে লাল সারিটি সবসময় আগে নয়

CVSS severity বলে ত্রুটিটি নিজে কতটা গুরুতর। সেটি বলে না আপনার system-এ ঝুঁকি কতটা। হাসপাতালের জরুরি বিভাগ ভাবুন: রোগীর অবস্থা, চিকিৎসা আছে কি না আর দেরি হলে কী হবে, সব মিলিয়ে ডাক্তার সিরিয়াল ঠিক করেন। একে বলে triage. Scan-এর ক্ষেত্রে প্রশ্নগুলো এরকম:

  • Reachability: ত্রুটির কোড কি আপনার app-এ আদৌ চলে, আর বাইরের অনুরোধ কি সেখানে পৌঁছায়?
  • Exploited: বাস্তবে আক্রমণ হচ্ছে কি না। CISA-র KEV তালিকা আর FIRST-এর EPSS score এখানে সাহায্য করে।
  • Fix available: নতুন version আছে, না অপেক্ষা করে অন্য প্রতিরক্ষা দিতে হবে?

নিচের ওজনগুলো একটি উদাহরণ, কোনো মানদণ্ড নয়। libfoo-র reachable বদলে দেখুন তালিকা কীভাবে নড়ে।

// Scan triage on a labelled SAMPLE. IDs are fake; change flags or weights.
const SEVERITY = { CRITICAL: 40, HIGH: 25, MEDIUM: 10, LOW: 3 };
const WEIGHT = { reachable: 30, exploited: 40, fix: 10 };
const findings = [
  { id: 'CVE-SAMPLE-0001', pkg: 'libfoo',   sev: 'CRITICAL', reachable: false, exploited: false, fix: false },
  { id: 'CVE-SAMPLE-0002', pkg: 'openssl',  sev: 'HIGH',     reachable: true,  exploited: true,  fix: true  },
  { id: 'CVE-SAMPLE-0003', pkg: 'zlib',     sev: 'CRITICAL', reachable: true,  exploited: false, fix: true  },
  { id: 'CVE-SAMPLE-0004', pkg: 'busybox',  sev: 'MEDIUM',   reachable: false, exploited: false, fix: true  },
  { id: 'CVE-SAMPLE-0005', pkg: 'webframe', sev: 'HIGH',     reachable: true,  exploited: false, fix: false },
  { id: 'CVE-SAMPLE-0006', pkg: 'gcc-libs', sev: 'LOW',      reachable: false, exploited: false, fix: false },
];
function score(f) {
  return SEVERITY[f.sev] + (f.reachable ? WEIGHT.reachable : 0)
    + (f.exploited ? WEIGHT.exploited : 0) + (f.fix ? WEIGHT.fix : 0);
}
function action(f) {
  if (f.reachable && f.exploited) return 'NOW: patch and redeploy today';
  if (f.reachable && f.fix) return 'NEXT: patch in this release';
  if (f.reachable) return 'MITIGATE: no fix yet, reduce exposure';
  if (f.fix) return 'ROUTINE: next base-image rebuild';
  return 'RECORD: note why not reachable, review later';
}
const ranked = [...findings].sort((a, b) => score(b) - score(a) || a.id.localeCompare(b.id));
for (const f of ranked) {
  console.log(`${String(score(f)).padStart(3)} ${f.sev.padEnd(8)} ${f.pkg.padEnd(8)} ${action(f)}`);
}
const bySeverity = [...findings].sort((a, b) => SEVERITY[b.sev] - SEVERITY[a.sev] || a.id.localeCompare(b.id));
console.log('first by severity only:', bySeverity[0].pkg);
console.log('first by triage       :', ranked[0].pkg);

শুধু severity ধরে সাজালে first by severity only: libfoo, যার কোড আপনার app-এ চলেই না আর fix-ও নেই। Triage-এ first by triage : openssl, কারণ severity কম হলেও এটি reachable, আক্রমণ চলছে আর fix আছে: ২৫ + ৩০ + ৪০ + ১০ = 105. RECORD মানে ভুলে যাওয়া নয়। কারণ লিখে রাখা, আর পরে আবার দেখা।

False positive আর backport

Scanner অনেক সময় version দেখে সতর্ক করে। অথচ Linux distribution প্রায়ই পুরোনো version নম্বর রেখেই সংশোধনটি যোগ করে, যাকে বলে backport. তখন রিপোর্ট ভুল সংকেত দিতে পারে। আবার package থাকলেও ত্রুটিপূর্ণ ফাংশন ব্যবহার না-ও হতে পারে। প্রতিকার হলো প্রমাণসহ একটি ignore নোট, যাতে কারণ, লেখকের নাম আর মেয়াদ থাকে। পুরো তালিকা চিরতরে চুপ করানো বিপজ্জনক। “এই পণ্যে এই ত্রুটি প্রযোজ্য নয়” কথাটি যন্ত্র-পাঠ্য আকারে জানানোর মানদণ্ডের নাম VEX.

Base image rebuild-এর অভ্যাস

আজ পরিষ্কার image কাল পরিষ্কার না-ও থাকতে পারে, কারণ নতুন CVE প্রকাশিত হয়। Image নিজে বদলায় না, জ্ঞান বদলায়। তাই দুটি অভ্যাস লাগে। এক, registry-র image নিয়মিত আবার scan করা, শুধু build-এর সময় নয়। দুই, code না বদলালেও সময় ধরে base image নতুন করে টেনে rebuild করা, যেমন সপ্তাহে একবার। Dockerfile-এ base image digest দিয়ে বাঁধলে build একই থাকে, তবে Renovate বা Dependabot-এর মতো টুল দিয়ে সেই digest আপডেট করাও চালু রাখতে হয়।

কাল্পনিক ঘটনা: CVE-SAMPLE-0099
ঘড়ি
কাজ
প্রভাবিত image
Production-এ ঝুঁকিতে
নতুন digest
  1. খবর এল। Base image-এর একটি TLS library-তে critical CVE প্রকাশিত হয়েছে। প্রথম প্রশ্ন, “আমরা কি আক্রান্ত?” এখনো উত্তর অজানা। আতঙ্কে সব কিছু বন্ধ করা হলো না।
  2. Inventory. Image scan করে নয়, জমা থাকা SBOM খুঁজে এক ঘণ্টায় পাওয়া গেল ৬টি image-এ package-টি আছে। Production-এর চলমান digest-এর সাথে মিলিয়ে দেখা গেল ৪টি চলছে।
  3. Triage. ৪টির মধ্যে ৩টি বাইরের TLS সংযোগ সরাসরি নেয়, তাই জরুরি। ১টি ভেতরের কাজ, ঝুঁকি কম, তবে সেটিও এই rollout-এই যাবে। Fix আসা পর্যন্ত অপ্রয়োজনীয় বাইরের পথ বন্ধ রাখা হলো।
  4. Base fix. Base image-এর রক্ষণাবেক্ষণকারীরা সংশোধিত image প্রকাশ করলেন। Tag একই, digest নতুন। দলের Dockerfile digest-এ বাঁধা, তাই সেই digest আপডেট করে review হলো।
  5. Rebuild, scan. ৬টি image নতুন করে build হলো, scan-এ এই CVE নেই, test পাস। ৬টি নতুন digest. Sign করে registry-তে তোলা হলো।
  6. Rollout. Canary দিয়ে শুরু, তারপর ধাপে ধাপে। ৩টি জরুরি service আগে গেল, তাই ঝুঁকিতে আছে শুধু ভেতরের ১টি।
  7. যাচাই। শেষ service-ও নতুন digest-এ। “Deploy হয়েছে” ধরে না নিয়ে চলমান digest আবার মিলিয়ে দেখা হলো, ফল ০. পুরোনো digest admission নিয়মে আটকে দেওয়া হলো, আর ঘটনার পর্যালোচনা লেখা হলো।

লক্ষ করুন, এখানে কোনো চলমান container-এর ভেতরে ঢুকে package আপডেট করা হয়নি। Container বদলায় না, প্রতিস্থাপিত হয়। এটি পর্ব ১৬-এর golden image-এর ধারণা।

৭ · SBOM, signing আর SLSA: কী আছে, কে বানাল, কীভাবে বানাল

কারখানার খাবারের প্যাকেট ভাবুন। গায়ে উপকরণের তালিকা থাকে, উৎপাদকের সিল থাকে, আর লাইসেন্স নম্বর থাকে। Software-এ এই তিনটির সমতুল্য তিনটি প্রমাণপত্র আছে।

SBOM: উপকরণের তালিকা

SBOM মানে image-এর সব package, version আর সাধারণত লাইসেন্সের যন্ত্র-পাঠ্য তালিকা। পরে নতুন CVE এলে এক খোঁজে জানা যায় কোন image আক্রান্ত।

SPDX, CycloneDX

Signature: উৎপাদকের সিল

Digital signature প্রমাণ করে এই digest এই পরিচয়ের কেউ সই করেছে। ভেতরের কোড ভালো কি না, তা বলে না।

Sigstore cosign

Provenance: কারখানার নথি

Provenance বলে কোন source, কোন builder আর কোন input থেকে image এল। SLSA এই প্রমাণের মান ধাপে ধাপে বাড়ানোর কাঠামো।

SLSA, in-toto

SBOM কেন, আর কোন ফরম্যাট

দুটি প্রধান ফরম্যাট। SPDX এসেছে Linux Foundation-এর project থেকে, যা ISO/IEC 5962:2021 হিসেবেও স্বীকৃত। CycloneDX এসেছে OWASP থেকে, আর নিরাপত্তা-কাজে এটি জনপ্রিয়। ২০২১ সালের মার্কিন Executive Order 14028 সরকারি সরবরাহে SBOM-কে আলোচনার কেন্দ্রে আনে। আপনার দেশে বা গ্রাহকের কাছে কী বাধ্যতামূলক, তা আলাদা করে যাচাই করুন।

# Generate from the image, then scan the SBOM later without pulling it
syft registry.example.com/shop/api@sha256:... -o spdx-json=sbom.spdx.json
trivy image --format cyclonedx -o sbom.cdx.json registry.example.com/shop/api@sha256:...
grype sbom:./sbom.spdx.json

SBOM-এর সীমাও জানুন। Tool যা চিনতে পারে না, তালিকায় তা নেই। হাতে কপি করা binary বা ভেতরে গুঁজে দেওয়া library বাদ যেতে পারে। আর পুরোনো SBOM আজকের image-এর কথা বলে না, তাই প্রতিটি build-এ তৈরি করে digest-এর সাথে বেঁধে রাখতে হয়।

Sigstore cosign: চাবি ছাড়া সই

পুরোনো সমস্যা ছিল signing key সামলানো। Key ফাঁস হলে সবই গেল। Sigstore project (২০২১ সালে সামনে আসে, এখন OpenSSF-এর অধীনে) একটি ভিন্ন পথ দেখায়। cosign দিয়ে keyless signing-এ CI-র OIDC পরিচয় দেখিয়ে কয়েক মিনিটের একটি certificate নেওয়া হয়। Certificate দেয় Fulcio, আর সইয়ের ঘটনা জমা হয় Rekor নামের transparency log-এ, যা জনসমক্ষে যাচাই করা যায়। সইটি নিজে registry-তে image-এর পাশে থাকে।

# Always sign the digest, never a tag
cosign sign registry.example.com/shop/api@sha256:...

cosign verify \
  --certificate-identity "https://github.com/example-org/shop/.github/workflows/release.yml@refs/heads/main" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  registry.example.com/shop/api@sha256:...

যাচাইয়ের সময় শুধু “সই আছে কি না” দেখলে হবে না। দেখতে হবে কে সই করেছে, মানে ঠিক কোন workflow, কোন repository, কোন branch. নইলে যে কেউ নিজের পরিচয়ে সই করা image এনে পার পেয়ে যাবে। Signing-এর আরেকটি বিকল্প Notary Project-এর Notation. বাস্তবে এই যাচাই মানুষ করে না, করে cluster-এর admission control: সই না মিললে image চলতেই পারবে না। সেটি পর্ব ৪৯-এ দেখবেন।

Provenance: আমাদের build-এই ছিল

অংশ ৩-এর attestation-manifest মনে আছে? BuildKit নিজে থেকেই একটি attestation যোগ করেছিল। এটি পড়লে দেখা যায়:

$ docker buildx imagetools inspect localhost:18411/abcl-cr-shop:1.4 \
    --format '{{json .Provenance}}'
{
 "SLSA": {
  "buildDefinition": {
   "buildType": "https://github.com/moby/buildkit/blob/master/docs/attestations/slsa-definitions.md",
   …
   "resolvedDependencies": [
    {
     "digest": {
      "sha256": "d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc"
     },
     "uri": "pkg:docker/alpine@3.20?platform=linux%2Famd64"
    }
   ]
  },
  …

resolvedDependencies-এর digest হুবহু সেই d9e853e8…, যা অংশ ৩-এ alpine:3.20-এর index-এ দেখেছেন। মানে image নিজেই বলছে সে কোন base থেকে এসেছে, tag-এর আড়ালে নয়, digest ধরে। তবে এটি নিজের লেখা বিবৃতি। সই আর বিশ্বস্ত builder ছাড়া এর ওজন কম। SLSA-র উঁচু স্তর ঠিক সেটিই চায়। Build track-এর স্তর, OIDC আর pipeline-এর পুরো ছবি পর্ব ৬১-এ পাবেন।

Build থেকে deploy: কোথায় কোন প্রমাণ জুড়ছে

Build → scan → sign → registry → deploy

Container image-এর supply chain: build, scan, sign, registry, deploy Buildimage, digest ScanCVE তালিকা Signdigest-এ সই Registryimmutable tag Deploydigest দিয়ে নতুন CVE: rebuild SBOM আর provenanceতৈরি হয়, digest ধরা হয় HIGH বা CRITICAL-এtriage, দরকারে থামা CI-র পরিচয়ে keyless সই,transparency log-এ নথি স্থায়ী release tag, নিয়মিতপুনরায় scan, retention Admission সই যাচাই করে(পর্ব ৪৯), digest লগে থাকে
ছবি ৪১.১ — প্রতিটি ধাপ একটি প্রমাণ যোগ করে। আর লাল রেখাটি মনে রাখুন: registry-তে পড়ে থাকা image-ও নতুন CVE-তে আবার build-এ ফেরে।

ক্রমের একটি বাস্তব কথা: cosign সাধারণত registry-তে থাকা digest-এ সই করে। তাই অনেক দল আগে একটি staging repository-তে push করে, সেখানে scan ও sign করে, তারপর সেই একই digest production repository-তে promote করে। ছবির ক্রম হলো যুক্তির ক্রম। Promotion-এর পুরো নকশা পর্ব ৫৮-এ।

৮ · বিশ্বাসের শিকল: বাস্তব ঘটনা আর আত্মরক্ষা

আপনার image-এর ভেতরে যা আছে তার বেশিরভাগ অচেনা মানুষের লেখা। Supply chain আক্রমণের লক্ষ্য তাই আপনি নন, আপনি যাঁদের বিশ্বাস করেন তাঁরা। একবার বিষ ঢুকলে সেটি বিশ্বস্ত পথ ধরেই আসে।

xz, ২০২৪: মার্চ ২০২৪-এ Microsoft-এর প্রকৌশলী Andres Freund লক্ষ করেন, একটি Linux system-এ SSH login অস্বাভাবিক ধীর। খুঁজতে গিয়ে তিনি compression library xz-এর 5.6.0 আর 5.6.1 release-এ লুকানো backdoor পান, যা CVE-2024-3094 নামে নথিভুক্ত হয়। প্রকাশিত বিবরণ অনুযায়ী দীর্ঘ সময় ধরে একজন অবদানকারী project-এর বিশ্বাস অর্জন করেছিলেন, আর ক্ষতিকর অংশ ছিল release ফাইলে, সাধারণ source tree-তে সহজে চোখে পড়ার মতো নয়। Stable distribution-গুলোর ব্যাপক প্রচারের আগেই ধরা পড়ায় বড় বিপর্যয় এড়ানো গেছে বলে মনে করা হয়। আক্রমণকারীর আসল পরিচয় প্রকাশ্যে নিশ্চিত হয়নি।

ঝুঁকিকী ঘটেআত্মরক্ষা
Dependency confusionএকই নামের ভেতরের package আর public package থাকলে tool ভুল করে public-টি নেয়Scoped নাম, একটিমাত্র নির্ভরযোগ্য index, নাম আগেই সংরক্ষণ
Typosquattingজনপ্রিয় নামের কাছাকাছি বানানে ক্ষতিকর package বা imageCopy-paste করা নাম মিলিয়ে দেখা, verified publisher, অনুমোদিত তালিকা
Tag বদলBase image-এর tag অন্য কিছুতে সরে যায়Digest pin, নিজের registry-তে কপি
Build system দখলCI-র ভেতরে ক্ষতিকর ধাপ ঢোকেস্বল্পমেয়াদি পরিচয়, সীমিত অনুমতি, provenance
পুরোনো packageজানা ত্রুটি রয়ে যায়নিয়মিত rebuild, ছোট base image

Dependency confusion ধারণাটি ২০২১ সালে গবেষক Alex Birsan-এর প্রকাশিত কাজের মাধ্যমে ব্যাপকভাবে আলোচনায় আসে। আত্মরক্ষার মূল কথা lockfile: exact version আর hash লিখে রাখা, যাতে install-এর সময় অন্য জিনিস এলে ধরা পড়ে।

# npm: install exactly what package-lock.json says, with integrity checks
npm ci

# pip: refuse any file whose hash is not listed
pip install --require-hashes -r requirements.txt

Dockerfile-এর base image-এর জন্যও একই নীতি: FROM alpine:3.20@sha256:d9e853e8… লিখলে tag পড়ার জন্য থাকে, আর digest সত্যিকারের বাঁধন দেয়। Python-এ --extra-index-url সতর্কভাবে ব্যবহার করুন, কারণ এতে একাধিক index-এর মধ্যে বাছাই হতে পারে। একটি private proxy registry-কে একমাত্র উৎস করা নিরাপদ। পর্ব ৩৪-এর defense in depth এখানেও খাটে, কোনো একটি স্তর একা যথেষ্ট নয়।

আরও গভীরে: Manifest-এর layer digest সংকুচিত byte-এর hash, আর config-এর rootfs.diff_ids অসংকুচিত byte-এর hash, তাই একই layer ভিন্নভাবে compress করলে digest বদলাতে পারে। OCI Image আর Distribution Specification 1.1 (২০২৪) subject ফিল্ড আর Referrers API যোগ করে, যাতে signature বা SBOM-কে সরাসরি image-এর সাথে যুক্ত করা যায়। আরেকটি লক্ষ্য reproducible build: একই source থেকে bit-by-bit একই ফল, যা provenance-কে স্বাধীনভাবে যাচাইযোগ্য করে।

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

  • Tag আর digest-এর পার্থক্য? Tag সরানো যায় এমন নাম, digest হলো manifest-এর SHA-256.
  • latest কি সবচেয়ে নতুন image? না, এটি শুধু default tag.
  • Image মুছলেও registry-র জায়গা কমে না কেন? Blob ভাগাভাগি হয়, garbage collection লাগে।
  • SBOM আর scan-এর পার্থক্য? SBOM উপকরণের তালিকা, scan সেই তালিকাকে জানা ত্রুটির সাথে মেলায়।
  • Signature কী প্রমাণ করে? কে সই করেছে আর digest অক্ষত আছে, কোড নিরাপদ তা নয়।

৯ · AI-এর যুগে registry আর supply chain

AI খুব গুছিয়ে Dockerfile আর pipeline লিখে দেয়। কিন্তু এর জানা তথ্য পুরোনো হতে পারে, আর image বা package-এর নাম সে নিজেও বানিয়ে ফেলতে পারে। Supply chain-এ একটি ভুল নাম মানে অচেনা কারও কোড আপনার production-এ।

AI যা করেঝুঁকিকী করবেন
FROM python:latest আর :latest-এ deployকী চলছে জানা যায় না, rollback ভাঙেVersion tag আর digest ব্যবহার
বানানো image বা package-এর নামকেউ সেই নামে ক্ষতিকর কিছু প্রকাশ করতে পারেঅফিসিয়াল উৎসে নামটি মিলিয়ে দেখা
নেই এমন flag লেখাPipeline ভাঙে, বা সুরক্ষা আছে বলে ভুল ধারণা হয়--help আর অফিসিয়াল নথি
CI-তে চিরস্থায়ী password বা Dockerfile-এ tokenSecret layer-এ বা log-এ থেকে যায়OIDC, স্বল্পমেয়াদি token
Scan ব্যর্থ হলে সব ignore করাসতর্কতা চুপ, ঝুঁকি একইকারণ ও মেয়াদসহ আলাদা ছাড়
সতর্কতা ছাড়া --delete-untaggedRollback-এর image, এমনকি চালু tag নষ্টআগে --dry-run, পরে আসল pull যাচাই
Registry-কে public করা বা 0.0.0.0-তে খোলাImage আর ভেতরের তথ্য ফাঁসPrivate, auth, সীমিত network

AI-কে কাজ দেওয়ার সময় সীমা বলে দিন, আর উত্তরের সাথে যাচাইয়ের পথ চান। কোনো secret, আসল account id বা registry URL prompt-এ দেবেন না।

আমার একটি Python web app-এর জন্য CI pipeline-এর container অংশ লিখে দাও। শর্ত: ১. Base image version tag আর digest, দুটোই দিয়ে বাঁধা থাকবে। digest তুমি বানাবে না, কোথায় বসাতে হবে শুধু দেখাবে। ২. Push-এর পর যে digest আসে সেটি ধরে রাখবে, deploy হবে সেই digest দিয়ে। ৩. Registry-র জন্য স্বল্পমেয়াদি OIDC পরিচয়, কোনো স্থায়ী password নয়। ৪. Scan HIGH আর CRITICAL-এ থামবে, কিন্তু ignore করতে হলে কারণ ও মেয়াদ লিখতে হবে। ৫. প্রতিটি flag কোন টুলের কোন version-এর নথিতে আছে বলো। নিশ্চিত না হলে তা জানাও। কোনো মুছে ফেলার কমান্ড দিলে আগে dry-run দেখাবে আর ঝুঁকি লিখবে।

AI-এর লেখা pipeline হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Digest এত নিরাপদ হলে সবাই tag ছেড়ে শুধু digest ব্যবহার করে না কেন?

কারণ digest স্থিরতা দেয়, আর স্থিরতার একটি দাম আছে। FROM alpine:3.20 লিখলে প্রতিটি rebuild-এ রক্ষণাবেক্ষণকারীদের সর্বশেষ সংশোধনী পাওয়া যায়, যদিও build পুনরাবৃত্তিযোগ্য হয় না। @sha256:… লিখলে build হুবহু একই থাকে, কিন্তু নিরাপত্তা সংশোধনও নিজে থেকে আসে না। Digest কেউ বদলে না দিলে দুই বছর পরেও সেই পুরোনো ত্রুটিসহ image-ই তৈরি হবে। অর্থাৎ অনিশ্চয়তার ঝুঁকি কমিয়ে আপনি পুরোনো হওয়ার ঝুঁকি নিলেন।

আরেকটি সমস্যা মানুষের চোখ। ৬৪ অক্ষরের hex দেখে কেউ বলতে পারেন না এটি কোন version. Code review-তে digest বদল প্রায় অর্থহীন লাগে। এজন্যই বাস্তবে দুটো একসাথে লেখা হয়, alpine:3.20@sha256:…. মানুষ tag পড়েন, যন্ত্র digest মানে। Renovate বা Dependabot-এর মতো টুল নতুন digest-এর pull request খোলে, ফলে পুরোনো হওয়ার ঝুঁকি একটি স্বাভাবিক review-এ পরিণত হয়।

Multi-architecture image-এ আরও একটি সূক্ষ্মতা আছে। Index-এর digest সব platform ধরে রাখে। ভুল করে একটি platform-এর digest বসালে অন্য CPU-তে চালাতে গেলে ব্যর্থ হবে। আর digest চিরস্থায়ীও নয়: আমাদের run-এ cleanup-এর পরে digest দিয়ে pull করা not found দিয়েছে। তাই digest ব্যবহার করলেও registry-তে একটি স্থায়ী tag রাখা দরকার।

Kubernetes-এর মতো system-এও একই হিসাব চলে। Manifest-এ digest লিখলে প্রতিটি node ঠিক একই image চালায়, আর দুই node-এ দুই রকম জিনিস চলার ঝুঁকি থাকে না। তবে তখন deployment-এর ইতিহাস পড়তে একটি টুলের সাহায্য লাগে, যা digest-কে version-এর সাথে মিলিয়ে দেখায়।

মূল উপলব্ধি: নিরাপত্তা শুধু “বদলাবে না” নয়, “নিয়ন্ত্রিতভাবে বদলাবে”। Digest বদলের একটি স্বয়ংক্রিয়, review-করা পথ থাকলে তবেই pinning কাজের।

প্র ০২Log4Shell-এর সময় অনেক দল কেন দিনের পর দিন উত্তর দিতে পারেনি “আমরা কি আক্রান্ত?” SBOM এই ক্ষেত্রে কতটা সাহায্য করে?

ডিসেম্বর ২০২১-এ Java logging library Apache Log4j 2-তে CVE-2021-44228 প্রকাশিত হয়, যা Log4Shell নামে পরিচিত। ত্রুটিটি গুরুতর ছিল, আর library-টি অসংখ্য Java পণ্যের ভেতরে ছিল। কঠিন প্রশ্নটি তাই প্রযুক্তিগত ছিল না, ছিল হিসাবের: এটি আমাদের কোথায় কোথায় আছে?

অনেক ক্ষেত্রে Log4j সরাসরি dependency ছিল না। এটি ছিল dependency-র dependency-র ভেতরে, অথবা অন্য একটি JAR-এর পেটে গুঁজে দেওয়া, অথবা কেনা কোনো পণ্যের মধ্যে। তাই দলগুলোকে server ধরে ধরে ফাইল খুঁজতে হয়েছে আর vendor-দের ইমেইল করতে হয়েছে। প্রতিটি image-এর SBOM আগে থেকে জমা থাকলে প্রথম উত্তরটি মিনিটে পাওয়া যেত: কোন digest-এ কোন version. এরপর সেই digest কোথায় চলছে তা মিলিয়ে নেওয়া সহজ।

তবে SBOM জাদু নয়। SBOM-এর মান নির্ভর করে তা তৈরির টুলের ওপর। গুঁজে দেওয়া JAR সব টুল সমানভাবে ধরে না। পুরোনো SBOM বর্তমান image-এর কথা বলে না। আর package থাকা মানেই ত্রুটি ব্যবহার করা যায় এমন নয়, তাই reachability আর VEX-এর প্রয়োজন থাকে। SBOM জমা রেখে যদি কেউ খুঁজতে না পারে, সেটিও অর্থহীন, তাই খোঁজা যায় এমন একটি ভান্ডার লাগে।

একই বছরের মে মাসে মার্কিন Executive Order 14028 আগেই SBOM-কে সামনে এনেছিল। Log4Shell সেই আলোচনাকে সবার বাস্তব অভিজ্ঞতায় পরিণত করে।

ব্যবহারিক অভ্যাস হলো, প্রতিটি build-এ SBOM তৈরি করে সেটি image-এর digest-এর সাথে attestation হিসেবে বা একটি কেন্দ্রীয় ভান্ডারে রাখা। তাহলে নতুন CVE এলে পুরো registry আবার scan করতে হয় না, তালিকায় খুঁজলেই চলে।

মূল উপলব্ধি: SBOM সমস্যার সমাধান করে না, সমস্যার সীমানা দ্রুত দেখায়। সংকটের প্রথম ঘণ্টায় সেটিই সবচেয়ে দামি তথ্য।

প্র ০৩xz-এর মতো ঘটনা কি scanner, SBOM বা signature ধরতে পারত? তাহলে এসবের মূল্য কী?

সৎ উত্তর হলো, প্রকাশের মুহূর্তে এগুলোর বেশিরভাগই ধরত না। Scanner জানা ত্রুটির তালিকা মেলায়, আর CVE-2024-3094 নম্বর আসার আগে কোনো তালিকায় এটি ছিল না। SBOM ঠিকই লিখত xz 5.6.1, কিন্তু সেটি ক্ষতিকর বলে চিনত না। Signature-ও বলত release-টি বৈধ রক্ষণাবেক্ষণকারীর, কারণ প্রকাশিত বিবরণ অনুযায়ী বিশ্বাসটাই ধাপে ধাপে অর্জন করা হয়েছিল। সই প্রমাণ করে কে, ভালো কি না তা নয়।

তাহলে মূল্য কোথায়? প্রথমত, ঘটনার পরের গতি। ধরা পড়ার সাথে সাথে SBOM আর scanner এক ঘণ্টায় বলে দিতে পারে কোন image-এ এই দুটি version আছে। দ্বিতীয়ত, আক্রমণের জায়গা কমানো। ছোট base image-এ অপ্রয়োজনীয় package কম, আর minimal image-এ অনেক সময় সেগুলো থাকেই না। তৃতীয়ত, অসংগতি ধরা। ঘটনায় release ফাইল আর source tree-র পার্থক্য গুরুত্বপূর্ণ ছিল। Build সরাসরি যাচাই করা source থেকে হলে, আর reproducible build-এর অভ্যাস থাকলে, এমন পার্থক্য চোখে পড়ার সুযোগ বাড়ে।

আর শেষ শিক্ষাটি মানবিক। এটি ধরা পড়েছিল একজন প্রকৌশলীর কৌতূহলে, যিনি ক্ষুদ্র একটি সময়ের পার্থক্য উপেক্ষা করেননি। এছাড়া এটি মনে করিয়ে দেয়, পৃথিবীর বহু গুরুত্বপূর্ণ software অল্প কয়েকজন স্বেচ্ছাসেবকের ওপর দাঁড়িয়ে। প্রতিষ্ঠানের জন্য নিজেদের ব্যবহৃত open-source project-কে সহায়তা করাও এক ধরনের নিরাপত্তা বিনিয়োগ।

আপনার দলের জন্য ব্যবহারিক কথাটি হলো, নিয়ন্ত্রণ বেছে নেওয়ার সময় জিজ্ঞেস করুন এটি কোন আক্রমণের কোন ধাপে কাজ করে। Signing ধরে পথের মাঝে বদলে দেওয়া image. Scanner ধরে জানা ত্রুটি। আর অজানা আক্রমণের জন্য লাগে সীমিত অনুমতি, network নিয়ন্ত্রণ আর নজরদারি, যাতে ক্ষতির পরিধি ছোট থাকে।

মূল উপলব্ধি: কোনো একটি নিয়ন্ত্রণ নিখুঁত নয়। স্তরগুলোর কাজ হলো আক্রমণের দাম বাড়ানো, ধরা পড়ার সম্ভাবনা বাড়ানো আর সাড়ার সময় কমানো।

প্র ০৪“সব image-এ শূন্য CRITICAL” একটি ভালো লক্ষ্য মনে হয়। এতে সমস্যা কোথায়?

একটি মাপকাঠি লক্ষ্য হয়ে গেলে মানুষ সংখ্যাটি মেলাতে শুরু করেন, ঝুঁকি কমাতে নয়। শূন্য CRITICAL চাইলে দল সহজ পথ নেয়: ignore file বড় হয়, scanner-এর severity ছেঁকে ফেলা হয়, অথবা এমন scanner বেছে নেওয়া হয় যা কম দেখায়। সংখ্যা সুন্দর হয়, নিরাপত্তা একই থাকে।

দ্বিতীয় সমস্যা ভুল অগ্রাধিকার। CVSS-এর base score ত্রুটির সাধারণ গুরুত্ব বলে, আপনার পরিবেশের ঝুঁকি নয়। ভেতরের একটি batch job-এ না চলা কোডের ৯.৮ score-এর পেছনে দল এক সপ্তাহ দিল, আর ইন্টারনেটমুখী service-এর ৭.৫ score-এর ত্রুটি, যার আক্রমণ চলছে, পড়ে থাকল। সিমুলেটরের openssl সারিটি ঠিক এই পরিস্থিতি দেখায়। এজন্যই KEV, EPSS, reachability-র মতো সংকেত যোগ করা হয়।

তৃতীয়ত, সব ত্রুটির fix থাকে না। Upstream কখনো কোনো CVE-কে ঠিক না করার সিদ্ধান্তও নিতে পারে। আবার backport-এর কারণে false positive আসে। তখন “শূন্য” অসম্ভব, আর অসম্ভব লক্ষ্য মানুষকে ফাঁকিতে ঠেলে দেয়। চতুর্থত, release আটকে থাকলে নতুন সংশোধনও যেতে পারে না। অনেক সময় নতুন build-এ ঝুঁকি কমছিল, কিন্তু gate তাকে থামিয়ে দিল, কারণ অন্য একটি পুরোনো সতর্কতা খোলা ছিল।

ভালো লক্ষ্যগুলো ভিন্ন। যেমন: fix আছে এমন reachable CRITICAL-এর সংশোধনের সময়, আর base image-এর গড় বয়স। Gate-এর নিয়ম হতে পারে “নতুন ঝুঁকি যোগ হলে release থামবে”, সঙ্গে প্রমাণসহ, মেয়াদি ছাড়।

ছোট base image-ও এখানে সাহায্য করে। যে package image-এ নেই, সেটির সতর্কতাও আসে না, তাই আলোচনার জন্য সত্যিকারের ত্রুটিগুলো আলাদা করে চোখে পড়ে।

মূল উপলব্ধি: সংখ্যা ঝুঁকির ছায়া, ঝুঁকি নয়। মাপুন আপনি কত দ্রুত আসল বিপদ বন্ধ করেন, রিপোর্ট কত পরিষ্কার দেখায় তা নয়।

প্র ০৫Registry বন্ধ হয়ে গেলে কি চলমান system বন্ধ হবে? Registry-কে কেন একটি নির্ভরতা হিসেবে নকশায় ধরতে হয়?

চলমান container সাধারণত চলতে থাকে, কারণ image আগেই node-এ নামানো। সমস্যা আসে যখন নতুন কিছু দরকার হয়: autoscaling-এ নতুন node, একটি node মরে যাওয়ার পর অন্য node-এ container তোলা, অথবা নতুন deployment. তখন image টানতে হয়, আর registry না পেলে container উঠবে না। অর্থাৎ বিপদের সময়ই, যখন সবচেয়ে বেশি capacity লাগে, registry-র ওপর নির্ভরতা ধরা পড়ে। পর্ব ১৭-এর autoscaling সে কারণে registry-র ওপরও নির্ভর।

২০২০ সালে Docker Hub pull-এ rate limit চালু করলে বহু CI pipeline হঠাৎ ভাঙতে শুরু করে। অনেকে তখন বুঝতে পারেন, তাঁদের প্রতিটি build একটি বাইরের প্রতিষ্ঠানের সেবার ওপর দাঁড়িয়ে আছে, যার সাথে কোনো চুক্তিই নেই। সীমার অঙ্ক এরপরও বদলেছে, ভবিষ্যতেও বদলাতে পারে।

নকশায় কয়েকটি স্তর রাখা যায়। Base image নিজের registry-তে কপি করা, যাতে build বাইরের সেবার ওপর না থাকে। Pull-through cache রাখা। Workload-এর region-এই registry রাখা, আর দরকার হলে অন্য region-এ replication. Node image-এ গুরুত্বপূর্ণ image আগেই ভরে রাখা, যাতে নতুন node দ্রুত ওঠে। আর পর্ব ২৪-এর মতো ভেবে দেখা: registry-র জন্য আপনার RTO কত, আর পুরো region চলে গেলে image কোথা থেকে আসবে?

Registry-র নিরাপত্তাও এখানে জড়িত। অতিরিক্ত অনুমতি থাকলে ভুল করে বা ইচ্ছাকৃতভাবে image মুছে যেতে পারে, আর আমাদের run দেখিয়েছে cleanup-ও চালু tag ভাঙতে পারে। তাই audit log, delete-এর সীমিত অনুমতি আর পরীক্ষা করা cleanup জরুরি।

মূল উপলব্ধি: Registry শুধু গুদাম নয়, এটি scale আর recovery-র পথের অংশ। যা ছাড়া নতুন container উঠতে পারে না, তাকে production নির্ভরতা হিসেবেই নকশা করুন।

অনুশীলন

প্রথম তিনটি কাগজে বা সিমুলেটরে করা যাবে। Docker থাকলে নিজের কম্পিউটারে 127.0.0.1-এ registry:2 চালিয়েও দেখতে পারেন। এতে cloud-এর খরচ নেই। শেষে container আর image মুছে ফেলুন।

  1. Layer-এর হিসাব: একটি image-এর তিনটি layer: base ৩০ MB, dependency ১০০ MB, app ২০ MB. নতুন version-এ শুধু app layer বদলে ২২ MB হলো। ৫০টি node-এ আগের version আছে। Manifest আর config-এর মাপ বাদ দিয়ে হিসাব করুন: push-এ কত MB উঠবে? সব node মিলিয়ে কত MB নামবে? Layer ভাগাভাগি না থাকলে কত MB নামত?

    Push: শুধু নতুন layer, ২২ MB. Pull: প্রতিটি node ২২ MB নেয়, ৫০ × ২২ = ১,১০০ MB. ভাগাভাগি না থাকলে একটি image ৩০ + ১০০ + ২২ = ১৫২ MB, আর ৫০ × ১৫২ = ৭,৬০০ MB. সাশ্রয় ৬,৫০০ MB, মানে প্রায় ৮৬%. এজন্যই বড় dependency-কে আলাদা, কম বদলানো layer-এ রাখা হয়।

  2. ফল আগে বলুন: একটি plain registry-তে tag mutable, আর image-গুলো single-platform. ক্রমে ঘটল: (১) image A push হলো app:1 নামে, digest dA. (২) image B push হলো app:1 নামে, digest dB. (৩) image B push হলো app:2 নামে। (৪) registry garbage-collect --delete-untagged চলল। ধাপ ৩-এর পরে আর ধাপ ৪-এর পরে app:1, app:2 আর app@dA কী দেবে?

    ধাপ ৩-এর পরে: app:1 → B, app:2 → B, app@dA → A. Registry-তে দুটি image, B-র দুটি নাম।

    ধাপ ৪-এর পরে: app:1 আর app:2 → B. A-র কোনো tag নেই, তাই manifest মুছে গেছে, app@dA → not found. B-র সাথে ভাগ না করা A-র blob-ও মুছে গেছে। Digest বদলায়নি, শুধু জিনিসটি আর নেই।

  3. সিমুলেটর: scan_triage.js-তে libfoo-র reachable true করলে এর score আর action কী হবে? চালানোর আগে হিসাব করুন, তারপর মিলিয়ে দেখুন। তালিকায় এটি কোন স্থানে যাবে?

    Score: CRITICAL ৪০ + reachable ৩০ = ৭০. Exploited আর fix নেই, তাই আর কিছু যোগ হবে না। Action MITIGATE: no fix yet, reduce exposure. ক্রম: openssl ১০৫, zlib ৮০, libfoo ৭০, webframe ৫৫. অর্থাৎ এটি তৃতীয় স্থানে উঠবে। এক বিট তথ্য, “কোডটি চলে”, পুরো সিদ্ধান্ত বদলে দিল।

  4. সমালোচনা ও নকশা: একটি দলের pipeline: FROM python:latest; CI-তে একটি স্থায়ী Docker Hub password; প্রতিটি build app:latest নামে push হয়; server রাতে docker pull app:latest করে; scan নেই; মাসে একবার ৩০ দিনের পুরোনো সব image মুছে যায়। পাঁচটি সমস্যা ধরুন আর নতুন নকশা দিন।
    • চলমান base: build পুনরাবৃত্তিযোগ্য নয়। ব্যবহার করুন python:3.12-alpine@sha256:…, আর digest আপডেটের স্বয়ংক্রিয় PR.
    • স্থায়ী password: ফাঁসের ঝুঁকি। OIDC আর push-সীমিত পরিচয়, সম্ভব হলে cloud registry.
    • সরে যাওয়া latest: কী চলছে জানা যায় না, rollback নেই। Commit SHA বা version-এর immutable tag, deploy digest দিয়ে।
    • রাতের নিজে-নিজে pull: অপরীক্ষিত কিছু production-এ যায়। Deployment হবে অনুমোদিত, ধাপে ধাপে, ফিরে যাওয়ার পথসহ।
    • Scan আর SBOM নেই: CI-তে scan, প্রতিটি build-এ SBOM, registry-তে নিয়মিত পুনরায় scan, আর সই।
    • বয়স ধরে মোছা: চলমান পুরোনো image মুছে নতুন node ভাঙতে পারে। চলমান digest রক্ষা করুন, শেষ কয়েকটি release রাখুন, আগে dry-run চালান।

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

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

ক্রমএকটি image release-এর ধাপগুলো সঠিক যৌক্তিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Base image digest-এ বেঁধে image build, SBOM তৈরি
  2. Vulnerability scan আর triage
  3. Digest-এ signature দেওয়া
  4. Immutable tag-সহ production registry-তে রাখা
  5. Admission-এ signature যাচাই করে digest দিয়ে deploy

একটি mutable registry-তে app:1.4 নামে image A push হলো, তার digest dA. পরে একই tag-এ image B push হলো। এখন app@dA pull করলে (কোনো cleanup হয়নি) কী পাওয়া যাবে?

  • Image B, কারণ tag এখন B-তে
  • Image A
  • Error, কারণ নতুন push পুরোনো image মুছে দেয়
  • A আর B-র layer মেশানো একটি image

Reference reg.example.com/shop/api:2.0@sha256:abc…-এ tag আর digest দুটোই আছে। Docker কোনটি মেনে image আনবে?

  • Tag, কারণ সেটি আগে লেখা
  • যেটি সবচেয়ে নতুন
  • Digest
  • কোনোটি নয়, এটি অবৈধ রূপ

docker push-এ একটি layer-এর পাশে Layer already exists লেখা এল। এর মানে কী?

  • একই digest-এর blob registry-তে আছে, তাই আবার পাঠানো হয়নি
  • Push ব্যর্থ হয়েছে, আবার চেষ্টা করতে হবে
  • Tag-টি immutable, তাই নতুন image বসেনি
  • পুরোনো layer মুছে নতুনটি বসেছে

একটি self-hosted registry থেকে manifest মোছা হলো, কিন্তু ডিস্কের ব্যবহার একটুও কমল না। সবচেয়ে সম্ভাব্য কারণ কী?

  • Delete কমান্ড আসলে কিছুই করেনি
  • Tag-এর ফাইলগুলো অনেক বড়
  • Layer সংকুচিত থাকে বলে হিসাব ভুল
  • Blob ভাগাভাগি হয়, garbage collection না চালালে তা মোছে না

একটি image-এর SBOM আর বৈধ signature দুটোই আছে। এ থেকে কোন সিদ্ধান্তটি ঠিক?

  • Image-এ কোনো ত্রুটি নেই
  • কী কী উপকরণ আছে আর কোন পরিচয় এই digest-এ সই করেছে, তা জানা যায়
  • আর কখনো scan করতে হবে না
  • Tag আর কখনো সরতে পারবে না

একটি প্রতিষ্ঠানের ভেতরের package acme-utils. কোন ব্যবস্থা dependency confusion-এর ঝুঁকি কমাতে সবচেয়ে কার্যকর?

  • Public index-কে সবার আগে রাখা
  • Package-এর নাম গোপন রাখা
  • Private index-কে একমাত্র উৎস করা আর hash-সহ lockfile ব্যবহার করা
  • Package-এর version নম্বর খুব বড় রাখা

শব্দকোষ

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

মনে রাখুন

  1. Tag সরে, digest সরে না। মানুষের জন্য tag, deployment আর rollback-এর জন্য digest.
  2. Registry-তে আছে blob, manifest, index আর tag. Push ও pull শুধু যা নেই সেটি আনে-নেয়।
  3. মুছলে জায়গা ফেরে garbage collection-এর পরে। আগে dry-run, চলমান digest রক্ষা, পরে pull যাচাই।
  4. Scan-এর ফল severity, reachability আর fix মিলিয়ে সাজান। নিয়মিত rebuild করুন, ছাড় দিন কারণ ও মেয়াদসহ।
  5. SBOM বলে কী আছে, signature বলে কে সই করল, provenance বলে কীভাবে তৈরি। কোনোটিই একা নিরাপত্তা নয়।

পরের পর্বে: মডিউল ৭ এখানেই শেষ। আপনি এখন image বানাতে, চালাতে আর বিশ্বাসযোগ্যভাবে পৌঁছে দিতে পারেন। কিন্তু একশো container অনেকগুলো machine-এ কে চালাবে, কে সামলাবে? মডিউল ৮-এ শুরু হবে Kubernetes.

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

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