Module 26 Security 5 labs

Software Supply Chain Security: SBOM, Cosign & SLSA

Bảo vệ chuỗi cung ứng phần mềm từ source code đến artifact deploy: tạo Software Bill of Materials (SBOM), ký và verify container image bằng Cosign/Sigstore, tạo SLSA provenance attestation và kiểm soát policy bằng Kyverno.

Công cụ thực hành Syft, Grype, Cosign, Rekor, slsa-verifier, Kyverno, GitHub Actions
Nền tảng CLI, VS Code, Git, Docker, GitHub Actions, GHCR
Thời điểm phát hành 23/05/2026
Ngày biên soạn 23/05/2026
Người biên soạn Trần Văn Hòa — Microsoft Certified Trainer (MCT)

Mục tiêu học tập

1. Lý thuyết cốt lõi

1.1. Supply Chain Attack — Mối đe dọa từ chuỗi cung ứng

Software Supply Chain Attack nhắm vào quá trình xây dựng và phân phối phần mềm thay vì tấn công trực tiếp hệ thống đích. Kẻ tấn công xâm nhập vào một mắt xích của chuỗi (source code, dependency, build system, artifact repository, update mechanism) rồi lây lan đến toàn bộ downstream users.

Hai case study kinh điển

  • SolarWinds (2020): Build pipeline bị nhiễm — mã độc được chèn vào bản cập nhật Orion platform hợp lệ, ký số bởi chính SolarWinds. ~18.000 tổ chức nhận bản update có backdoor SUNBURST, bao gồm các cơ quan liên bang Mỹ.
  • Log4Shell (2021) — CVE-2021-44228: Thư viện Log4j2 (Java) có trong hàng triệu ứng dụng chứa lỗ hổng RCE CVSS 10.0. Không ai biết mình đang dùng thư viện này vì nằm sâu trong dependency tree — chứng minh sự cần thiết của SBOM.

1.2. SBOM — Software Bill of Materials

SBOM là danh sách đầy đủ các thành phần phần mềm trong một artifact: thư viện, version, license, hash, mối quan hệ phụ thuộc. Tương tự "bảng thành phần dinh dưỡng" của thực phẩm. Khi Log4Shell xảy ra, tổ chức có SBOM chỉ mất vài phút để biết mình có bị ảnh hưởng không; tổ chức không có SBOM mất vài tuần.

FormatTổ chứcUse caseCông cụ tạo
CycloneDXOWASPSecurity-focused, DevSecOpsSyft, cdxgen
SPDXLinux FoundationLicense compliance, legalSyft, SPDX Tools
Syft JSONAnchoreTích hợp Grype ecosystemSyft

Syft (Anchore) là công cụ tạo SBOM phổ biến nhất: scan container image, filesystem, OCI archive → xuất CycloneDX/SPDX/JSON. Grype nhận SBOM làm input để scan CVE mà không cần access trực tiếp vào artifact — quan trọng cho air-gapped environments.

1.3. Cosign & Sigstore — Ký artifact

Sigstore là hạ tầng mã nguồn mở do Google, Red Hat, Chainguard phát triển, cung cấp công nghệ ký số hiện đại cho software artifacts. Ba thành phần chính:

Keyless signing flow: CI job authenticate bằng OIDC token → Fulcio cấp cert → Cosign ký image → entry ghi vào Rekor. Khi verify: Cosign check cert còn valid, check Rekor entry tồn tại — không cần distribute public key.

1.4. SLSA — Supply Chain Levels for Software Artifacts

SLSA (phát âm "salsa") là framework của Google định nghĩa 4 mức bảo mật cho build pipeline:

  • SLSA L1: Provenance tồn tại — ghi lại artifact được build từ đâu, bao giờ, bằng gì. Không cần tamper-proof.
  • SLSA L2: Provenance được ký bởi build service (không phải bởi người). Chống giả mạo provenance thủ công.
  • SLSA L3: Build service hardened — isolated build environment, không thể modify provenance sau khi tạo. GitHub Actions đạt L3 với slsa-github-generator.
  • SLSA L4 (deprecated → merged into L3): Two-person review, hermetic builds. Dành cho critical infrastructure.

Provenance attestation là JSON document mô tả: builder identity, build trigger, source commit, build steps, output artifact digest. Được ký bằng Cosign/Sigstore và verify bằng slsa-verifier.

1.5. Policy Enforcement với Kyverno

Kyverno là Kubernetes-native policy engine viết bằng YAML (không cần học Rego như OPA). Chức năng liên quan supply chain: verify image signature — chặn Pod deploy nếu image chưa được ký bởi trusted key/identity. ClusterPolicy loại verifyImages hook vào Kubernetes admission controller, chạy trước khi Pod được tạo.

2. Thực hành (Labs)

LAB-126

Tạo SBOM bằng Syft — CycloneDX & SPDX

CLI · Syft · Docker

🎯 Mục tiêu: Tạo SBOM đầy đủ cho container image và filesystem bằng Syft, xuất định dạng CycloneDX (JSON) và SPDX, phân tích nội dung SBOM.

🧰 Công cụ / nền tảng: Syft CLI, Docker, Python 3.

📦 Chuẩn bị:

# Cài Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \
    | sh -s -- -b /usr/local/bin
syft version

▶️ Các bước (CLI):

# 1. Tạo Python app nhỏ để tạo image
mkdir sbom-lab && cd sbom-lab
cat > requirements.txt << 'EOF'
flask==3.0.3
requests==2.31.0
gunicorn==21.2.0
EOF

cat > app.py << 'EOF'
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return {"status": "ok", "version": "1.0.0"}

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)
EOF

cat > Dockerfile << 'EOF'
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 8080
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"]
EOF

# 2. Build image
docker build -t sbom-demo:1.0.0 .

# 3. Tạo SBOM từ image — format CycloneDX JSON
syft sbom-demo:1.0.0 -o cyclonedx-json=sbom-cyclonedx.json
echo "CycloneDX SBOM components:"
python3 -c "
import json
with open('sbom-cyclonedx.json') as f:
    sbom = json.load(f)
components = sbom.get('components', [])
print(f'  Total components: {len(components)}')
# Tìm Python packages
pkgs = [c for c in components if c.get('type') == 'library']
print(f'  Libraries: {len(pkgs)}')
for p in sorted(pkgs, key=lambda x: x.get('name',''))[:10]:
    print(f'    {p.get(\"name\",\"?\")} {p.get(\"version\",\"?\")}')
"

# 4. Tạo SBOM format SPDX JSON
syft sbom-demo:1.0.0 -o spdx-json=sbom-spdx.json
echo ""
echo "SPDX SBOM packages:"
python3 -c "
import json
with open('sbom-spdx.json') as f:
    sbom = json.load(f)
pkgs = sbom.get('packages', [])
print(f'  Total packages: {len(pkgs)}')
for p in pkgs[:8]:
    print(f'    {p.get(\"name\",\"?\")} {p.get(\"versionInfo\",\"?\")} [{p.get(\"licenseConcluded\",\"?\")}]')
"

# 5. Scan SBOM filesystem (không cần image — air-gap friendly)
syft dir:. -o cyclonedx-json=sbom-fs.json
echo "Filesystem SBOM: $(python3 -c \"import json; d=json.load(open('sbom-fs.json')); print(len(d.get('components',[])), 'components')\")"

# 6. Attach SBOM vào image như OCI attestation
# (yêu cầu Cosign — xem LAB-128)
syft sbom-demo:1.0.0 -o syft-json=sbom-syft.json
echo "Syft JSON SBOM: $(wc -l < sbom-syft.json) lines"

# 7. In license summary từ CycloneDX
python3 << 'PYEOF'
import json
from collections import Counter
with open('sbom-cyclonedx.json') as f:
    sbom = json.load(f)
licenses = []
for comp in sbom.get('components', []):
    for lic in comp.get('licenses', []):
        if 'license' in lic:
            licenses.append(lic['license'].get('id', lic['license'].get('name', 'Unknown')))
counts = Counter(licenses)
print("\nLicense summary:")
for lic, count in counts.most_common(10):
    print(f"  {lic}: {count}")
PYEOF

✅ Kết quả mong đợi: 3 file SBOM tạo thành công: sbom-cyclonedx.json, sbom-spdx.json, sbom-syft.json. CycloneDX report ≥ 50 components (Python packages + OS packages). License summary hiển thị MIT, BSD, Apache 2.0 chiếm đa số. SPDX có field spdxVersion, packages array.

🧹 Cleanup: docker rmi sbom-demo:1.0.0; cd .. && rm -rf sbom-lab.

LAB-127

Scan SBOM bằng Grype — CVE từ Bill of Materials

CLI · Grype · Syft

🎯 Mục tiêu: Dùng Grype để scan CVE từ SBOM (không cần pull image), so sánh kết quả scan trực tiếp image vs scan từ SBOM, tạo vulnerability report có thể share.

🧰 Công cụ / nền tảng: Grype CLI, Syft CLI, Docker.

📦 Chuẩn bị:

# Cài Grype (nếu chưa có)
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh \
    | sh -s -- -b /usr/local/bin
grype version

# Cập nhật Grype vulnerability database
grype db update

▶️ Các bước (CLI):

# 1. Tạo image có dependency cũ (Python 3.9 có nhiều CVE hơn)
mkdir grype-lab && cd grype-lab
cat > Dockerfile << 'EOF'
FROM python:3.9-slim
WORKDIR /app
RUN pip install flask==0.12.2 requests==2.18.0 pillow==8.0.0
COPY . .
EOF
docker build -t vuln-app:1.0 .

# 2. Method A: Grype scan trực tiếp image
echo "=== Method A: Direct image scan ==="
grype vuln-app:1.0 --scope all-layers 2>/dev/null | head -30

# 3. Method B: Syft tạo SBOM → Grype scan SBOM
echo ""
echo "=== Method B: SBOM-based scan (air-gap friendly) ==="
syft vuln-app:1.0 -o syft-json=app-sbom.syft.json -q
grype sbom:app-sbom.syft.json 2>/dev/null | head -30

# 4. Xuất Grype report JSON
grype sbom:app-sbom.syft.json -o json=grype-report.json -q 2>/dev/null

# 5. Phân tích report
python3 << 'PYEOF'
import json
with open('grype-report.json') as f:
    report = json.load(f)

matches = report.get('matches', [])
print(f"Total vulnerabilities: {len(matches)}")

from collections import Counter
severity_counts = Counter(
    m['vulnerability']['severity'] for m in matches
)
for sev in ['Critical', 'High', 'Medium', 'Low', 'Negligible']:
    count = severity_counts.get(sev, 0)
    bar = '█' * min(count, 40)
    print(f"  {sev:12s}: {count:4d} {bar}")

print("\nTop 5 affected packages:")
pkg_counts = Counter(
    f"{m['artifact']['name']}@{m['artifact']['version']}"
    for m in matches
)
for pkg, count in pkg_counts.most_common(5):
    print(f"  {pkg}: {count} CVEs")

print("\nCritical CVEs:")
crits = [m for m in matches if m['vulnerability']['severity'] == 'Critical']
for m in crits[:5]:
    cve = m['vulnerability']['id']
    pkg = m['artifact']['name']
    ver = m['artifact']['version']
    fix = m['vulnerability'].get('fix', {}).get('versions', ['no fix'])
    print(f"  {cve} in {pkg} {ver} → fix: {fix}")
PYEOF

# 6. SBOM diff — so sánh hai phiên bản
cat > requirements-v2.txt << 'EOF'
flask==3.0.3
requests==2.31.0
Pillow==10.3.0
EOF
cat > Dockerfile.v2 << 'EOF'
FROM python:3.12-slim
WORKDIR /app
COPY requirements-v2.txt .
RUN pip install -r requirements-v2.txt
EOF
docker build -f Dockerfile.v2 -t fixed-app:2.0 .
syft fixed-app:2.0 -o syft-json=app-v2-sbom.syft.json -q
grype sbom:app-v2-sbom.syft.json -o json=grype-v2-report.json -q 2>/dev/null

python3 -c "
import json
r1 = json.load(open('grype-report.json'))
r2 = json.load(open('grype-v2-report.json'))
c1 = sum(1 for m in r1['matches'] if m['vulnerability']['severity'] == 'Critical')
c2 = sum(1 for m in r2['matches'] if m['vulnerability']['severity'] == 'Critical')
print(f'v1.0 Critical CVEs: {c1}')
print(f'v2.0 Critical CVEs: {c2}')
print(f'Reduction: {c1-c2} Critical fixed')
"

✅ Kết quả mong đợi: Method A và B cho kết quả giống nhau (chênh lệch không đáng kể do DB timing). vuln-app:1.0 có nhiều Critical/High hơn fixed-app:2.0 rõ rệt. grype-report.json có field matches với đầy đủ CVE ID, severity, fix version.

🧹 Cleanup: docker rmi vuln-app:1.0 fixed-app:2.0; cd .. && rm -rf grype-lab.

LAB-128

Ký container image bằng Cosign — Keyless Sigstore

CLI · Cosign · Docker · GHCR

🎯 Mục tiêu: Ký container image bằng Cosign (cả key-based lẫn keyless), push signature lên registry, verify signature, attach SBOM làm attestation.

🧰 Công cụ / nền tảng: Cosign CLI, Docker, GitHub Container Registry (GHCR), Syft.

📦 Chuẩn bị:

# Cài Cosign
curl -sSfL https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 \
    -o /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign
cosign version

# Login vào GHCR (cần GitHub PAT với write:packages scope)
echo $GITHUB_TOKEN | docker login ghcr.io -u $GITHUB_USER --password-stdin

▶️ Các bước (CLI):

# === PHẦN A: Ký bằng key truyền thống ===

# 1. Build image
mkdir cosign-lab && cd cosign-lab
cat > Dockerfile << 'EOF'
FROM alpine:3.19
RUN echo "signed-app v1.0" > /etc/version
CMD ["cat", "/etc/version"]
EOF
docker build -t signed-app:1.0 .

# 2. Tag và push lên GHCR
# Thay YOUR_GITHUB_USER bằng username thật
IMAGE="ghcr.io/YOUR_GITHUB_USER/signed-app:1.0"
docker tag signed-app:1.0 $IMAGE
docker push $IMAGE

# 3. Tạo keypair Cosign
cosign generate-key-pair
# Tạo cosign.key (private) và cosign.pub (public)
# Nhập passphrase hoặc để trống

# 4. Ký image bằng private key
cosign sign --key cosign.key $IMAGE
# Nhập passphrase nếu đã đặt
# Signature được push lên registry cùng image (OCI artifact)

# 5. Verify signature bằng public key
cosign verify --key cosign.pub $IMAGE | python3 -m json.tool
# Kết quả: JSON array với payload, signature, certificate


# === PHẦN B: Keyless signing (OIDC — dùng trong CI) ===

# 6. Keyless sign (chạy interactively — sẽ mở browser cho OIDC)
IMAGE_V2="ghcr.io/YOUR_GITHUB_USER/signed-app:1.0-keyless"
docker tag signed-app:1.0 $IMAGE_V2
docker push $IMAGE_V2

COSIGN_EXPERIMENTAL=1 cosign sign $IMAGE_V2
# Mở browser → đăng nhập GitHub/Google/Microsoft
# Fulcio cấp cert, sign xong, entry ghi vào Rekor

# 7. Verify keyless (check Rekor transparency log)
COSIGN_EXPERIMENTAL=1 cosign verify \
    --certificate-identity "YOUR_EMAIL" \
    --certificate-oidc-issuer "https://accounts.google.com" \
    $IMAGE_V2 | python3 -m json.tool


# === PHẦN C: Attach SBOM làm attestation ===

# 8. Tạo SBOM
syft signed-app:1.0 -o cyclonedx-json=sbom.json -q

# 9. Attach SBOM vào image như OCI attestation
cosign attach sbom --sbom sbom.json $IMAGE

# 10. Attach SBOM như attestation (có thể verify)
cosign attest --key cosign.key \
    --predicate sbom.json \
    --type cyclonedx \
    $IMAGE

# 11. Verify attestation và đọc SBOM
cosign verify-attestation \
    --key cosign.pub \
    --type cyclonedx \
    $IMAGE \
    | jq -r '.payload | @base64d | fromjson | .predicate.components | length'
# In ra số components trong SBOM

# 12. Kiểm tra Rekor entry (public transparency log)
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $IMAGE | cut -d@ -f2)
rekor-cli search --sha $DIGEST --rekor_server https://rekor.sigstore.dev

✅ Kết quả mong đợi: cosign verify --key cosign.pub trả về JSON với critical.identity.docker-referencecritical.image.docker-manifest-digest. Signature xuất hiện trong GHCR registry bên cạnh image tag. SBOM attestation verify thành công, jq in ra số component. Keyless verify check được OIDC issuer và email identity.

🧹 Cleanup: cd .. && rm -rf cosign-lab cosign.key cosign.pub. Xóa image trên GHCR qua GitHub UI nếu cần.

LAB-129

SLSA Provenance trong GitHub Actions & slsa-verifier

GitHub Actions · slsa-github-generator · slsa-verifier

🎯 Mục tiêu: Tạo SLSA L3 provenance attestation cho container image bằng slsa-github-generator trong GitHub Actions, verify provenance với slsa-verifier.

🧰 Công cụ / nền tảng: GitHub Actions, slsa-github-generator, slsa-verifier CLI, GHCR.

📦 Chuẩn bị: Repo GitHub với Dockerfile. Cài slsa-verifier local: go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latest.

▶️ Các bước:

# 1. Tạo workflow build + provenance
mkdir -p .github/workflows

cat > .github/workflows/build-and-sign.yml << 'EOF'
name: Build, Sign & SLSA Provenance

on:
  push:
    tags: ['v*']
  workflow_dispatch:

env:
  IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/supply-chain-demo

jobs:
  # ── BUILD IMAGE ──────────────────────────────────────────────
  build:
    name: Build Container Image
    runs-on: ubuntu-latest
    outputs:
      image: ${{ steps.build.outputs.image }}
      digest: ${{ steps.build.outputs.digest }}
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push image
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ env.IMAGE_NAME }}:${{ github.ref_name }}
          # Trả về digest dạng sha256:abc123...

      - name: Export digest
        run: |
          echo "image=${{ env.IMAGE_NAME }}" >> $GITHUB_OUTPUT
          echo "digest=${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT

  # ── SLSA PROVENANCE (L3) ─────────────────────────────────────
  provenance:
    name: Generate SLSA Provenance
    needs: [build]
    permissions:
      actions: read        # Đọc workflow metadata
      id-token: write      # OIDC token cho Fulcio/Sigstore
      packages: write      # Push attestation lên GHCR
    uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
    with:
      image: ${{ needs.build.outputs.image }}
      digest: ${{ needs.build.outputs.digest }}
      registry-username: ${{ github.actor }}
    secrets:
      registry-password: ${{ secrets.GITHUB_TOKEN }}
    # Workflow này tạo provenance JSON, ký bằng Cosign keyless,
    # push attestation lên GHCR, ghi vào Rekor transparency log

  # ── COSIGN SIGN (thêm image signature) ───────────────────────
  sign:
    name: Sign Image with Cosign
    needs: [build]
    runs-on: ubuntu-latest
    permissions:
      packages: write
      id-token: write
    steps:
      - name: Install Cosign
        uses: sigstore/cosign-installer@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Sign image (keyless)
        run: |
          cosign sign --yes \
            "${{ needs.build.outputs.image }}@${{ needs.build.outputs.digest }}"
        # --yes: bỏ qua interactive prompt trong CI

  # ── SBOM ATTESTATION ─────────────────────────────────────────
  sbom-attest:
    name: Attach SBOM Attestation
    needs: [build, sign]
    runs-on: ubuntu-latest
    permissions:
      packages: write
      id-token: write
    steps:
      - uses: actions/checkout@v4

      - name: Install Syft & Cosign
        run: |
          curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \
            | sh -s -- -b /usr/local/bin
          curl -sSfL https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 \
            -o /usr/local/bin/cosign && chmod +x /usr/local/bin/cosign

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Generate and attach SBOM
        run: |
          IMAGE_REF="${{ needs.build.outputs.image }}@${{ needs.build.outputs.digest }}"
          syft "$IMAGE_REF" -o cyclonedx-json=sbom.json -q
          cosign attest --yes \
            --predicate sbom.json \
            --type cyclonedx \
            "$IMAGE_REF"
EOF

# 2. Commit và push để trigger trên tag
git add .github/
git commit -m "feat: add SLSA L3 provenance + Cosign signing pipeline"
git tag v1.0.0
git push origin main --tags

# 3. Theo dõi workflow
gh run list --workflow="build-and-sign.yml"
gh run watch

# 4. Verify provenance sau khi build xong
# Cài slsa-verifier
go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latest

# Lấy digest của image vừa build
IMAGE="ghcr.io/YOUR_GITHUB_USER/supply-chain-demo:v1.0.0"
DIGEST=$(crane digest $IMAGE)  # cài crane: go install github.com/google/go-containerregistry/cmd/crane@latest

slsa-verifier verify-image \
    --source-uri "github.com/YOUR_GITHUB_USER/YOUR_REPO" \
    --source-tag "v1.0.0" \
    "$IMAGE@$DIGEST"
# Output: PASSED: SLSA verification passed

# 5. Verify Cosign signature
cosign verify \
    --certificate-identity-regexp "https://github.com/YOUR_GITHUB_USER/.*" \
    --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
    "$IMAGE@$DIGEST"

# 6. Verify SBOM attestation
cosign verify-attestation \
    --type cyclonedx \
    --certificate-identity-regexp "https://github.com/YOUR_GITHUB_USER/.*" \
    --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
    "$IMAGE@$DIGEST" \
    | jq -r '.payload | @base64d | fromjson | .predicate.metadata.component.name'

✅ Kết quả mong đợi: GitHub Actions hiển thị 4 job: build, provenance, sign, sbom-attest — tất cả xanh. slsa-verifier verify-image in PASSED: SLSA verification passed. cosign verify trả về JSON với issuer: https://token.actions.githubusercontent.com. GHCR repo hiển thị thêm các OCI artifact: sha256:...sig (signature) và sha256:...att (attestation).

🧹 Cleanup: Xóa tag trên GitHub nếu cần. Image và attestation trên GHCR giữ lại làm portfolio proof.

LAB-130

Policy enforcement với Kyverno — chặn unsigned image

Kyverno · Kubernetes · Cosign · kubectl

🎯 Mục tiêu: Cài Kyverno, tạo ClusterPolicy verify image signature bằng Cosign, kiểm chứng rằng Pod dùng unsigned image bị chặn và Pod dùng signed image được phép deploy.

🧰 Công cụ / nền tảng: Kubernetes (kind/minikube), Kyverno v1.12+, kubectl, Cosign, GHCR.

📦 Chuẩn bị:

# Tạo cluster local với kind
kind create cluster --name supply-chain-demo

# Cài Kyverno via Helm
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno kyverno/kyverno \
    --namespace kyverno \
    --create-namespace \
    --set replicaCount=1
kubectl wait --for=condition=ready pod \
    -l app.kubernetes.io/component=admission-controller \
    -n kyverno --timeout=120s

▶️ Các bước (CLI):

# 1. Chuẩn bị: có public key từ LAB-128 (cosign.pub)
#    Hoặc tạo keypair mới cho lab này
cosign generate-key-pair --output-key-prefix lab-key

# 2. Tạo Kubernetes Secret chứa public key
kubectl create secret generic cosign-pubkey \
    --from-file=cosign.pub=lab-key.pub \
    --namespace default

# 3. Tạo ClusterPolicy Kyverno verify image
cat > kyverno-verify-image-policy.yaml << 'EOF'
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
  annotations:
    policies.kyverno.io/title: Verify Image Signature
    policies.kyverno.io/description: >-
      Chặn deploy Pod nếu container image chưa được ký bởi
      trusted key. Áp dụng cho namespace default và production.
spec:
  validationFailureAction: Enforce   # Enforce = block; Audit = log only
  background: false
  rules:
    - name: check-image-signature
      match:
        any:
          - resources:
              kinds: [Pod]
              namespaces: [default, production]
      verifyImages:
        - imageReferences:
            - "ghcr.io/YOUR_GITHUB_USER/*"
          attestors:
            - entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      # Paste nội dung lab-key.pub tại đây
                      -----END PUBLIC KEY-----
                    signatureAlgorithm: sha256
          mutateDigest: true    # Thay tag bằng digest — immutable reference
          verifyDigest: true
          required: true
EOF
kubectl apply -f kyverno-verify-image-policy.yaml

# 4. Verify policy đã active
kubectl get clusterpolicy verify-image-signature
kubectl describe clusterpolicy verify-image-signature | grep "Validation Failure Action"

# 5. Test Case A: Deploy Pod dùng UNSIGNED image → phải bị chặn
cat > pod-unsigned.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: unsigned-pod
  namespace: default
spec:
  containers:
    - name: app
      image: ghcr.io/YOUR_GITHUB_USER/supply-chain-demo:unsigned
      command: ["sleep", "3600"]
EOF
kubectl apply -f pod-unsigned.yaml
# Kết quả mong đợi: Error from server: admission webhook denied
# "image signature verification failed"

# 6. Ký image thật (từ LAB-128 hoặc ký mới)
IMAGE="ghcr.io/YOUR_GITHUB_USER/supply-chain-demo:v1.0.0"
cosign sign --key lab-key.key $IMAGE
# Nhập passphrase nếu có

# 7. Test Case B: Deploy Pod dùng SIGNED image → phải được phép
cat > pod-signed.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: signed-pod
  namespace: default
spec:
  containers:
    - name: app
      image: ghcr.io/YOUR_GITHUB_USER/supply-chain-demo:v1.0.0
      command: ["sleep", "3600"]
EOF
kubectl apply -f pod-signed.yaml
# Kết quả mong đợi: pod/signed-pod created

# 8. Kiểm tra Pod đang chạy và image đã được mutate sang digest
kubectl get pod signed-pod -o jsonpath='{.spec.containers[0].image}'
# Output: ghcr.io/.../supply-chain-demo@sha256:abc123... (digest, không phải tag!)

# 9. Xem Kyverno audit log
kubectl get policyreport -A
kubectl get clusterpolicyreport

# 10. Chuyển sang Audit mode để test không block
kubectl patch clusterpolicy verify-image-signature \
    --type='json' \
    -p='[{"op":"replace","path":"/spec/validationFailureAction","value":"Audit"}]'

✅ Kết quả mong đợi: kubectl apply -f pod-unsigned.yaml bị từ chối với message chứa "image signature verification failed". kubectl apply -f pod-signed.yaml thành công. Pod signed-pod chạy với image reference là digest (sha256:...) thay vì tag — immutable. kubectl get policyreport liệt kê kết quả verify cho từng Pod.

🧹 Cleanup: kubectl delete pod signed-pod; kind delete cluster --name supply-chain-demo; rm -f lab-key.key lab-key.pub kyverno-verify-image-policy.yaml pod-*.yaml.

3. Tình huống doanh nghiệp thực tế

Bối cảnh

Một công ty SaaS sau sự cố Log4Shell nhận ra không biết service nào đang dùng Log4j và phiên bản nào. Đội security mất 2 tuần để inventory thủ công 47 service. Ban lãnh đạo yêu cầu không bao giờ để lặp lại tình huống tương tự, đồng thời muốn đảm bảo chỉ image được build từ CI pipeline chính thức mới được deploy lên production.

Giải pháp Supply Chain Security

  • SBOM-first culture: Tất cả CI pipeline phải tạo SBOM (Syft) và push lên artifact registry (GHCR/ECR) kèm image. Tích hợp Grype scan SBOM vào pipeline — fail build nếu có CRITICAL CVE.
  • Cosign keyless signing: Mọi image build từ GitHub Actions được ký keyless — identity là GitHub Actions OIDC. Không cần quản lý private key. Rekor làm audit trail.
  • Kyverno admission policy: ClusterPolicy enforce — chỉ image được ký bởi token.actions.githubusercontent.com với repo thuộc org công ty mới được phép deploy. Mọi image build tay trên máy dev bị chặn tự động.
  • SLSA L3 target: Lộ trình 6 tháng: L1 (provenance tồn tại) → L2 (ký bởi build service) → L3 (slsa-github-generator, isolated build). Ưu tiên service tier 1 trước.
  • Kết quả: Khi có CVE mới, team tra cứu SBOM trên artifact registry trong < 5 phút thay vì 2 tuần. Production cluster từ chối 100% image không rõ nguồn gốc.

📚 Nguồn tham khảo

Module 25: DevSecOps Fundamentals Module 27: Kubernetes Security
Zalo