Phần 34 đo Pod Security Standards — thứ kiểm soát pod làm gì. Bài này lùi một bước: kiểm soát cái nằm trong pod.

Kết quả quét sáu ảnh, tình trạng chữ ký, và phép thử đổi ảnh trên cùng một thẻ

Quét sáu ảnh

Ảnh Cỡ CRIT HIGH Tổng
registry.k8s.io/pause:3.9 0 MB 0 0 0
distroless/static-debian12:nonroot 1 MB 0 0 0
python:3.12-alpine 18 MB 0 2 25
nginx:1.27-alpine 21 MB 2 33 107
nginx:1.21 (cũ) 53 MB 30 196 811
python:3.12 (Debian) 381 MB 56 365 2.889

Hai dòng cuối cùng đáng nhìn kỹ nhất.

python:3.12python:3.12-alpine chạy cùng một Python. Số lỗ hổng: 2.889 so với 25 — ít hơn 115 lần, và không phải sửa một dòng mã nào.

Chênh lệch không đến từ Python mà từ mọi thứ đi kèm: trình biên dịch, thư viện phát triển, tiện ích dòng lệnh — hàng trăm gói mà ứng dụng không dùng nhưng vẫn có mặt để bị đếm và để bị khai thác.

nginx:1.21 là bản cũ vài năm: 30 lỗ hổng nghiêm trọng so với 2 của bản mới. Ghim phiên bản mà không bao giờ cập nhật cũng là một rủi ro, không chỉ là sự ổn định.

Nhưng cỡ ảnh không giải thích được hết

python:3.12-alpine 18 MB có 25 lỗ hổng. nginx:1.27-alpine 21 MB có 107 — gấp bốn lần, gần như cùng cỡ.

Nên "dùng alpine" không phải một bảo đảm. Nó là một chỉ dấu tốt, không phải một câu trả lời. Cách duy nhất biết là quét.

Quét một ảnh mất vài giây sau lần đầu:

trivy image --scanners vuln python:3.12-alpine

Và trong CI, cái đáng làm không phải là "chặn khi có lỗ hổng" — mọi ảnh đều có. Cái đáng làm là chặn khi số lỗ hổng nghiêm trọng tăng so với lần trước, hoặc chặn theo danh sách CVE có bản vá sẵn:

trivy image --severity CRITICAL,HIGH --ignore-unfixed --exit-code 1 <anh>

--ignore-unfixed là cờ quan trọng nhất: lỗ hổng chưa có bản vá thì bạn không làm gì được, và để chúng làm đỏ bảng điều khiển khiến người ta bắt đầu bỏ qua cả bảng.

Chữ ký: ai có, ai không

registry.k8s.io/pause:3.9             CÓ chữ ký
distroless/static-debian12:nonroot    CÓ chữ ký, kèm cả SBOM ký theo
nginx:1.27-alpine                     KHÔNG tìm thấy
python:3.12-alpine                    KHÔNG tìm thấy

Ảnh của Kubernetes và của Google đều ký bằng cosign, kiểm được ngay:

cosign verify --certificate-identity-regexp '.*' \
  --certificate-oidc-issuer-regexp '.*' registry.k8s.io/pause:3.9
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - Existence of the claims in the transparency log was verified offline
  - The code-signing certificate was verified using trusted CA certificates

Ảnh chính thức trên Docker Hub thì không — chúng dùng cơ chế khác (Docker Content Trust), không phải cosign. Nên đừng kết luận "không có chữ ký nghĩa là không đáng tin"; nó nghĩa là bạn không kiểm được bằng công cụ này, và trong một quy trình tự động thì hai điều đó dẫn tới cùng một chỗ.

Một chi tiết nhỏ mà thông báo lỗi tự dạy tôi: hỏi distroless về SLSA provenance thì nó trả lời

none of the attestations matched the predicate type: slsaprovenance,
found: https://spdx.dev/Document

Tức là nó số liệu chứng thực, chỉ là loại khác — một SBOM định dạng SPDX, đã ký. Thông báo lỗi nói luôn nó có gì.

Chữ ký gắn với digest, không gắn với thẻ

Đây là phép đo quan trọng nhất bài. Tôi ký ảnh của chính mình:

thời gian ký: 0,3 giây

Rồi dựng một ảnh khác và đẩy lên cùng thẻ v1:

digest trước  sha256:ce59063d25caa7fba09e08dc3ac3a05f86cd34d...
digest sau    sha256:20605e12bcbb5f9f724be72ccddb89cb651f648...

cosign verify  ->  Error: no signatures found

Cùng một thẻ, và chữ ký biến mất hoàn toàn.

Đó chính là điều bạn muốn: chữ ký chứng nhận nội dung, không chứng nhận cái tên. Thẻ chỉ là con trỏ, và con trỏ thì ai có quyền ghi vào registry đều đổi được.

Hệ quả trực tiếp cho cách viết manifest:

# đổi được dưới chân bạn
image: cong-ty/api:v1.4.2

# không đổi được
image: cong-ty/api@sha256:ce59063d25caa7fba09e08dc3ac...

Dùng digest là cách duy nhất để "cái tôi đã kiểm" và "cái đang chạy" chắc chắn là một. Đổi lại là bạn phải cập nhật digest mỗi lần phát hành — việc mà bộ cập nhật tự động làm giúp được.

Chặn ảnh không ký ở tầng cụm

Kiểm chữ ký trong CI là chưa đủ: ai đó vẫn kubectl apply được một ảnh chưa qua CI. Chốt chặn thật nằm ở webhook nhận vào — Kyverno, Sigstore Policy Controller, hoặc Connaisseur.

# Kyverno, rút gọn
verifyImages:
  - imageReferences: ["cong-ty/*"]
    attestors:
      - entries:
          - keys:
              publicKeys: |-
                -----BEGIN PUBLIC KEY-----
                ...

Hai lời khuyên từ những gì đo được ở trên:

  • Bắt đầu bằng chế độ chỉ cảnh báo. Bật cưỡng chế ngay là chặn luôn mọi ảnh bên thứ ba chưa ký — và đo được ở trên, nginxpython nằm trong nhóm đó.
  • Chỉ áp cho ảnh của chính bạn trước. cong-ty/* ký được vì bạn kiểm soát quy trình dựng; docker.io/* thì không.

Bốn việc theo thứ tự đáng làm

  1. Đổi ảnh nền sang bản gọn. Rẻ nhất, hiệu quả nhất — đo được 115 lần.
  2. Quét trong CI, chặn theo --ignore-unfixed. Chặn cái sửa được, đừng chặn cái không sửa được.
  3. Ghim bằng digest. Không cần công cụ gì, chỉ cần đổi cách viết manifest.
  4. Ký và kiểm chữ ký. Đáng làm khi ba bước trên đã xong, vì nó cần quy trình chứ không chỉ cần một lệnh.

Thứ tự này quan trọng: ký một ảnh 2.889 lỗ hổng chỉ chứng minh rằng chính bạn là người tạo ra nó.

Thử ba mươi giây

Xem có bao nhiêu ảnh trong cụm đang dùng thẻ thay vì digest:

kubectl get pods -A -o json | python3 -c '
import sys, json
the = digest = 0
for p in json.load(sys.stdin)["items"]:
    for c in (p["status"].get("containerStatuses") or []):
        img = c.get("image", "")
        if "@sha256:" in img: digest += 1
        else:
            the += 1
            if ":latest" in img or ":" not in img.rsplit("/", 1)[-1]:
                print("  the troi noi:", p["metadata"]["namespace"], img)
print(f"-> {digest} ghim bang digest, {the} dung the")
'

Mọi dòng thẻ trôi nổi là một ảnh có thể đổi nội dung mà không đổi tên. Còn tỷ lệ ở dòng cuối cho biết bạn đang ở đâu trên con đường bốn bước phía trên — với phần lớn cụm, con số đầu tiên là 0.

Phần sau: Gateway API — thứ được dựng để thay Ingress, và cái nó làm được mà Ingress không.