✍️ धडा 14 — Signing, SBOM आणि provenance: तुम्ही काय पाठवता आणि ते कुठून आले हे सिद्ध करा
📍 तुम्ही इथे आहात: 16 पैकी धडा 14 · मागील: lesson-13-dependencies · पुढील: lesson-15-pipeline-threats
📦 या ब्रँचमध्ये काय आहे
धडे 01–13, आणि प्रत्येक build सोबत जाणारा पुरावा: एक SBOM (आत असलेल्या प्रत्येक गोष्टीची यादी), image digest वर एक signature (CI ने बनवल्यापासून त्यात बदल झालेला नाही), आणि provenance (कोणत्या repo ने, कोणत्या workflow ने आणि कोणत्या review झालेल्या commit ने तो build केला). तुम्ही SPDX आणि CycloneDX, Sigstore / cosign (keyless signing सह), सोप्या शब्दांत SLSA levels, आणि cluster फक्त signed images admit कसे करतो हे शिकता.
- sec/supply.py —
sbom(),digest(),sign(),verify(),provenance_ok() - sec/demo.py —
python3 sec/demo.py artifacts - sec/test_sec.py — तपासणी
L14
🧒 5 वर्षांच्या मुलाला समजावल्यासारखे
शाळेचे स्वयंपाकघर प्रत्येक वर्गात एक डबा पाठवते. कतरिना, एक शिक्षिका, असा एक डबा घेते. वर्ग जेवण्याआधी ती तीन गोष्टी तपासते:
- 📃 डब्यावर चिकटवलेली सामानाची यादी: "bread (bakery A, batch 3.0), jam (5.4), juice (2.32)". बातमीत "jam batch 5.3 खराब आहे" असे आले, तर तिच्या डब्यात ते आहे का हे ती एका सेकंदात पाहू शकते. हे आहे SBOM.
- 🔏 सील. स्वयंपाकघर डबा सील करते आणि त्यावर शिक्का मारते. वाटेत कोणी तो उघडला — अगदी एक sandwich बदलण्यासाठी जरी — तर सील तुटलेले असते. हे आहे signature.
- 🚚 वितरण चिठ्ठी. "आपल्या शाळेच्या स्वयंपाकघरात, आपल्या स्वयंपाकीने, मुख्य स्वयंपाकीने मंजूर केलेल्या पाककृतीतून बनवले." दुसऱ्या स्वयंपाकघरातून आलेला डबा, किंवा कोणीही न तपासलेल्या पाककृतीतून बनलेला डबा, परत जातो. हे आहे provenance.
आणि वर्गाच्या दारावर एक नियम आहे: सील नाही, वितरण चिठ्ठी नाही — तर जेवण नाही. हेच म्हणजे cluster फक्त चांगल्या provenance असलेल्या signed images admit करतो.
🗺️ आकृती
flowchart LR
c["✅ reviewed commit<br/>BaluRaut/school-app"] --> ci["🏗️ CI: release.yml"]
ci --> img["📦 image<br/>digest sha256:…"]
ci --> sb["📃 SBOM<br/>SPDX / CycloneDX"]
ci --> sig["🔏 cosign signature<br/>keyless: workflow identity"]
ci --> prov["🚚 provenance<br/>repo · workflow · commit"]
img --> reg["🗄️ registry"]
reg --> adm{"🛂 admission<br/>signature ✓ provenance ✓"}
adm -->|"fork / other workflow / tampered"| no["❌ rejected"]
adm -->|"all good"| run["☸️ runs"]
🗺️ काढलेली आवृत्ती + एक lab: https://school-edh.pages.dev/security/lesson-diagrams.html#l14
❓ काय
- Artifact — build जे तयार करतो ते: container image, wheel, jar, binary.
- Digest — artifact च्या bytes चा SHA-256 (
sha256:9f1c…).1.4.2सारखा tag दुसऱ्या bytes कडे हलवता येतो; digest नाही. Digest नेच deploy आणि sign करा. - SBOM (software bill of materials) — artifact मधल्या प्रत्येक component ची machine ला
वाचता येणारी यादी: नाव, version, पुरवठादार, licence, hashes. दोन standard formats:
- SPDX (Linux Foundation; एक ISO standard);
- CycloneDX (OWASP). syft किंवा Trivy सारखी tools image वरून SBOM बनवतात. प्रत्येक release साठी SBOMs साठवून ठेवले, तर "नव्या advisory चा आपल्यावर परिणाम होतो का?" हा एक search बनतो, आठवडाभराचे काम नाही.
- Signature — key धारकाने याच नेमक्या digest वर sign केले याचा पुरावा. ज्याच्याकडे public key (किंवा sign करणाऱ्याची ओळख) आहे, तो कोणीही ते verify करू शकतो. एक byte जरी बदलला, तरी digest बदलतो, आणि signature जुळत नाही.
- Sigstore / cosign — बहुतेक teams containers sign करण्यासाठी वापरतात ती open tooling:
- cosign sign आणि verify करते, आणि signatures registry मध्ये image शेजारी साठवते;
- keyless signing: CI मध्ये cosign workflow ची OIDC identity वापरते (उदाहरणार्थ "main वरचा BaluRaut/school-app चा release.yml workflow"); Fulcio त्या identity साठी अल्पायुषी certificate देते; signature public transparency log Rekor मध्ये नोंदवली जाते. Leak होऊ शकेल अशी दीर्घायुषी signing key नाही;
- verify करताना identity आणि issuer तपासले जातात, फक्त "कोणतीतरी वैध signature" नाही.
- Attestation — artifact बद्दल एक signed विधान: "हा त्याचा SBOM आहे", "तो असा build झाला". Provenance हा त्याचाच एक प्रकार.
- Provenance — artifact कुठे आणि कसा build झाला: source repo, commit, build workflow, builder. चांगले provenance कसे असते हे SLSA framework ठरवतो.
- SLSA build levels, सोप्या शब्दांत:
- Level 0 — काहीच नाही: "माझ्यावर विश्वास ठेवा, मी build केले";
- Level 1 — provenance अस्तित्वात आहे: build सांगतो की तो कसा बनला (तरीही ते खोटे बनवता येऊ शकते);
- Level 2 — provenance developer च्या laptop ने नाही, तर hosted build platform ने signed केलेले असते (जसे GitHub Actions);
- Level 3 — build platform मजबूत (hardened) असतो: builds एकमेकांपासून वेगळे असतात, आणि build steps provenance खोटे बनवू किंवा बदलू शकत नाहीत, ना signing secrets पर्यंत पोहोचू शकतात.
- Verify करणारे admission — Kyverno
verifyImages, Sigstore policy-controller, किंवा Ratify सह Gatekeeper: त्या image चा pod चालू देण्याआधी cluster signature आणि attestations तपासतो (धडा 12 चे फाटक, आणखी एका नियमासह).
🤔 का
कारण "आम्ही review केलेला code" आणि "चालणारा container" यांच्या मध्ये अनेक हात असतात: CI runners, caches, registries, mirrors. Registry वर push करू शकणारा, fork मधून build करू शकणारा, किंवा बाजूचा workflow चालवू शकणारा हल्लेखोर योग्य दिसणाऱ्या नावाखाली कोणीही review न केलेला code पाठवू शकतो. दारावर तपासलेली, आपल्या repo शी आणि आपल्या release workflow शी जोडलेली signature ही फट बंद करते. आणि पुढची मोठी library advisory आली, की SBOMs असलेली team "आपण ती कुठे चालवतो?" याचे उत्तर मिनिटांत देते.
🔧 कसे (या repo मध्ये)
sec/supply.py मध्ये:
sbom(components)प्रत्येक component साठीname,versionआणि एक छोटाsha256परत देते. (हे modelname==versionया text चा hash करते; खरा SBOM खऱ्या package file चा hash नोंदवतो.)digest(artifact)म्हणजे artifact text चा SHA-256.sign(artifact, key)हे digest चे HMAC आहे, आणिverify()ते पुन्हा मोजते आणि constant time मध्ये तुलना करते. HMAC एकच shared key वापरते — model फक्त आकार दाखवते. खरे signing (cosign) sign करण्यासाठी private key किंवा keyless certificate आणि verify करण्यासाठी public key किंवा identity वापरते, त्यामुळे verify करणारा sign करू शकत नाही.provenance_ok(att)फक्तrepo == "BaluRaut/school-app",workflow == ".github/workflows/release.yml"आणिreviewed == Trueस्वीकारते, आणि प्रत्येक समस्येची यादी देते.
🧪 करून पाहा
python3 sec/demo.py artifacts
python3 - <<'EOF'
import sys; sys.path.insert(0, "sec"); from supply import sbom, digest, sign, verify, provenance_ok
image = "school/results:1.4.2 layers=3f9c,77ab,e1d0"; key = b"release-signing-key"
s = sign(image, key)
print(f"digest {digest(image)[:16]}… · signature {s}")
for label, art, k in (("same image, same key", image, key), ("one layer changed", image.replace("e1d0", "e1d1"), key),
("same image, other key", image, b"someone-elses-key")):
print(f"verify: {label:<22} → {verify(art, s, k)}")
before = {c["name"]: c for c in sbom([("flask", "3.0.0"), ("requests", "2.32.3")])}
after = {c["name"]: c for c in sbom([("flask", "3.0.0"), ("requests", "2.32.4"), ("pyyaml", "6.0.2")])}
print("SBOM diff:", {n: (before.get(n, {}).get("version"), after[n]["version"]) for n in after if before.get(n) != after[n]})
for att in (dict(repo="BaluRaut/school-app", workflow=".github/workflows/release.yml", reviewed=True),
dict(repo="BaluRaut/school-app", workflow=".github/workflows/debug.yml", reviewed=True),
dict(repo="BaluRaut/school-app", workflow=".github/workflows/release.yml", reviewed=False)):
print(f"provenance {att['workflow']:<30} reviewed={att['reviewed']!s:<5} → {provenance_ok(att)}")
EOF
python3 sec/test_sec.py
✅ तपासा — तुम्हाला काय दिसायला हवे
artifacts हे print करते:
═══ artifacts ═══
── SBOM: [{'name': 'flask', 'version': '3.0.0', 'sha256': 'af4113b6a8b4'}, {'name': 'jinja2', 'version': '3.1.4', 'sha256': 'c85d52da9cf8'}, {'name': 'requests', 'version': '2.32.3', 'sha256': '426739742007'}]
── signature on the image digest: 529a0de3b5e2a1c6 · verify original → True · verify tampered → False
── provenance BaluRaut/school-app → (True, [])
── provenance someone/fork → (False, ['built from someone/fork', 'commit was not reviewed'])
sign in CI (cosign / Sigstore), keep an SBOM per image, and let the cluster admit only signed images with good provenance
✅ done — every door checked
तुमचा snippet हे print करतो:
digest c940ffbe5d5f7d3f… · signature 529a0de3b5e2a1c6
verify: same image, same key → True
verify: one layer changed → False
verify: same image, other key → False
SBOM diff: {'requests': ('2.32.3', '2.32.4'), 'pyyaml': (None, '6.0.2')}
provenance .github/workflows/release.yml reviewed=True → (True, [])
provenance .github/workflows/debug.yml reviewed=True → (False, ['built by .github/workflows/debug.yml'])
provenance .github/workflows/release.yml reviewed=False → (False, ['commit was not reviewed'])
Tests मध्ये ✅ L14 a tampered artifact fails verification; a fork fails provenance येते
आणि शेवटी 16/16 passed येते.
🏁 तुम्ही आत्ताच काय सिद्ध केले
एकच layer id बदलल्याने (e1d0 → e1d1) signature तुटली, आणि योग्य image चुकीच्या
key ने तपासली तरी fail झाली — signature "बदल झालेला नाही" आणि "याच sign करणाऱ्याने sign
केले" या दोन्ही गोष्टी सिद्ध करते. दोन SBOMs ची तुलना केल्याने एका release ने नेमके काय बदलले
ते दिसले: एक upgrade आणि एक नवे package, आणि पाहायला हवे ते तेच. आणि जे signature एकटी
पकडू शकत नाही ते provenance पकडते: आपल्या repo ने build केलेली, पण बाजूच्या workflow ने
किंवा review न झालेल्या commit मधून बनलेली image तरीही नाकारली जाते.
⚠️ नेहमीच्या चुका
- digest ऐवजी tag sign करणे, किंवा digest ने verify करून नंतर tag ने deploy करणे
--certificate-identityशिवाय, फक्त key check सहcosign verify— मग Sigstore certificate मिळवू शकणारा कोणताही workflow pass होतो- CI secret म्हणून साठवलेली, प्रत्येक workflow ला वाचता येणारी दीर्घायुषी signing key
- SBOMs बनवणे पण ते प्रत्येक release साठी कधीच न साठवणे — मग नंतर ते प्रश्नांची उत्तरे देऊ शकत नाहीत
- signatures फक्त CI मध्ये verify करणे, admission वेळी नाही — registry किंवा cluster access असलेला कोणीही CI टाळू शकतो
Auditmode मधली, किंवाfailurePolicy: Ignoreअसलेली verify policy- developer हाताने लिहू शकतो असे provenance (SLSA level 1) जणू level 3 असल्यासारखे विश्वासार्ह मानणे
🏭 प्रत्यक्ष वापरात
खऱ्या account वर — release workflow: build, push, SBOM बनवा, keyless sign करा, आणि
signed build provenance जोडा (.github/workflows/release.yml):
name: release
on:
push:
branches: [main]
permissions:
contents: read
packages: write
id-token: write # for keyless signing and provenance
attestations: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- uses: anchore/sbom-action/download-syft@v0
- name: build and push
id: build
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
docker build -t ghcr.io/baluraut/school-results:${{ github.sha }} .
docker push ghcr.io/baluraut/school-results:${{ github.sha }}
echo "digest=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/baluraut/school-results:${{ github.sha }} | cut -d@ -f2)" >> "$GITHUB_OUTPUT"
- name: SBOM (SPDX and CycloneDX)
run: |
syft ghcr.io/baluraut/school-results@${{ steps.build.outputs.digest }} -o spdx-json=sbom.spdx.json -o cyclonedx-json=sbom.cdx.json
- name: sign and attach the SBOM
run: |
cosign sign --yes ghcr.io/baluraut/school-results@${{ steps.build.outputs.digest }}
cosign attest --yes --type spdxjson --predicate sbom.spdx.json ghcr.io/baluraut/school-results@${{ steps.build.outputs.digest }}
- name: build provenance
uses: actions/attest-build-provenance@v2
with:
subject-name: ghcr.io/baluraut/school-results
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
कुठूनही verify करा — signature main वरच्या याच workflow कडून आलेली, आणि GitHub च्या OIDC ने दिलेली असायला हवी:
cosign verify ghcr.io/baluraut/school-results@sha256:<digest> \
--certificate-identity "https://github.com/BaluRaut/school-app/.github/workflows/release.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
gh attestation verify oci://ghcr.io/baluraut/school-results@sha256:<digest> --repo BaluRaut/school-app
grype sbom:./sbom.spdx.json # "are we affected?" from the stored SBOM
Admission — आपल्या registry मधली image आपल्या release workflow ने signed नसेल, तर Kyverno तो pod नाकारतो (आणि tags ना digests मध्ये बदलतो):
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: { name: verify-school-images }
spec:
webhookTimeoutSeconds: 30
rules:
- name: signed-by-release-workflow
match: { any: [{ resources: { kinds: [Pod] } }] }
verifyImages:
- imageReferences: ["ghcr.io/baluraut/*"]
failureAction: Enforce
mutateDigest: true
attestors:
- entries:
- keyless:
subject: "https://github.com/BaluRaut/school-app/.github/workflows/release.yml@refs/heads/main"
issuer: "https://token.actions.githubusercontent.com"
rekor: { url: "https://rekor.sigstore.dev" }
🏭 Production मध्ये हे का महत्त्वाचे: तुमच्या registry मधून न आलेल्या images साठी दुसरा नियम जोडा (त्या block करा, किंवा review केलेल्या छोट्या यादीलाच परवानगी द्या), नाहीतर signature check फक्त त्याच images ना लागू होतो ज्या तशाही ठीकच असणार होत्या.
⏭️ पुढे
आता प्रत्येक खिडकीचा स्वतःचा रक्षक आहे. पुढे आपण developer च्या laptop पासून production पर्यंत संपूर्ण delivery line वरून चालतो, प्रत्येक टप्प्यावरचे धोके STRIDE ने ओळखतो — आणि secret leak झाल्यानंतरच्या पहिल्या तासाचा सराव करतो.
git checkout lesson-15-pipeline-threats