Chuỗi cung ứng của một container gồm ba thứ bạn không viết: base image, thư viện hệ thống, thư viện ngôn ngữ. Bài này đo xem mỗi lớp phòng thủ thật sự bắt được cái gì.

Số CVE theo base image, và ba lớp phòng thủ

Chọn base image là quyết định bảo mật lớn nhất

Quét bằng Trivy, chỉ đếm CRITICAL và HIGH:

Base image CRITICAL HIGH Số gói dính Dung lượng
python:3.12-alpine 0 2 2 79,5 MB
python:3.9-alpine3.16 (từ 2022) 0 11 8 73 MB
python:3.12-slim 3 16 11 205 MB
python:3.12 56 362 78 1,62 GB

Bản đầy đủ có 56 lỗ hổng nghiêm trọng362 lỗ hổng cao. Bản alpine cùng phiên bản Python có 0 và 2.

Và dòng thứ hai đáng suy nghĩ hơn cả: một image alpine từ năm 2022, đã bốn năm không cập nhật, vẫn ít lỗ hổng hơn image Debian đầy đủ mới nhất. Bề mặt tấn công quan trọng hơn tuổi.

Lý do đơn giản: python:3.12 chứa trình biên dịch, perl, công cụ mạng, thư viện đồ hoạ — hàng trăm gói mà ứng dụng của bạn không dùng nhưng vẫn phải chịu trách nhiệm. Đây là lập luận bảo mật của phần 10, giờ có số kèm theo.

Cách rẻ nhất để giảm 56 lỗ hổng nghiêm trọng xuống 0 là đổi một dòng FROM.

Máy quét bắt được gì

Tôi dựng một image chứa mã của chính mình: hai lỗi lập trình cố ý (ghép chuỗi thẳng vào shell, không kiểm tra đường dẫn) và ba bí mật ghi cứng.

Quét lỗ hổng (--scanners vuln) tìm ra CVE của libssl3 và các gói hệ thống. Nó không nói một chữ nào về mã của tôi hay ba khoá kia — đúng như thiết kế: nó so danh sách gói với cơ sở dữ liệu CVE, không đọc mã.

Quét bí mật (--scanners secret) thì tìm được:

/app.py (secrets)
Total: 2 (CRITICAL: 2)

CRITICAL: AWS (aws-access-key-id)
  /app.py:2 (added by 'COPY app.py /app.py # buildkit')
CRITICAL: GitHub (github-pat)

Hai trong ba. Khoá bí mật AWS 40 ký tự lọt lưới — nó chỉ là một chuỗi ngẫu nhiên, không có tiền tố nhận dạng như AKIA hay ghp_.

Chi tiết (added by 'COPY app.py /app.py') rất hữu ích: nó chỉ thẳng lớp nào đưa bí mật vào, kể cả khi lớp sau đã xoá tệp đi — đúng chuyện phần 21 đã đo.

Phép thử đầu tiên của tôi cho kết quả sai

Lần đầu tôi dùng AKIAIOSFODNN7EXAMPLE — khoá mẫu quen thuộc trong tài liệu AWS. Máy quét báo 0 bí mật, và tôi suýt kết luận rằng nó không nhìn vào mã ứng dụng.

Đó là khoá ví dụ được đưa vào danh sách bỏ qua. Đổi sang một chuỗi có dạng thật thì nó tìm ra ngay. Nếu bạn kiểm tra máy quét bằng khoá mẫu, bạn sẽ tin là nó hỏng — hoặc tệ hơn, nếu bạn kiểm tra thứ khác bằng khoá mẫu và nó "sạch", bạn sẽ tin là mình an toàn.

Phép thử phải thất bại được thì kết quả thành công mới có nghĩa.

Ghim digest ngăn được chuyện gì

Nhãn có thể bị đẩy đè. Tôi dựng một registry riêng để đo:

digest ban dau: sha256:0791b5c86634...
noi dung: phien ban LANH

(day len mot image KHAC voi CUNG mot nhan 1.0)

keo lai bang NHAN   -> phien ban BI THAY DOI
keo lai bang DIGEST -> phien ban LANH

Cùng một chuỗi ung-dung:1.0, hai nội dung khác nhau, không có cảnh báo nào. Chỉ digest là bất biến — đúng như phần 4 đã đo, giờ nhìn từ góc bảo mật.

FROM python:3.12-alpine@sha256:0791b5c86634a27ca2c8123bb305c1a8150afac05ffa96cce3527ab2e792d853

Đổi lại là bạn phải tự cập nhật digest khi muốn lên bản mới. Đó là chi phí đáng trả cho image nền của môi trường thật; với môi trường phát triển thì nhãn vẫn tiện hơn.

Ba lớp, ba loại rủi ro khác nhau

Lớp Bắt được Không bắt được
Quét lỗ hổng CVE của gói hệ thống và thư viện bí mật, lỗi trong mã bạn viết
Quét bí mật khoá có tiền tố nhận dạng, kèm tên lớp khoá không có dạng đặc trưng, khoá ví dụ
Ghim digest nhãn bị đẩy đè, registry bị chiếm image gốc vốn đã có lỗ hổng

Không lớp nào thay được lớp nào, và không lớp nào đọc mã của bạn. Lỗi ghép chuỗi vào shell trong ví dụ của tôi lọt qua cả ba.

Đưa vào quy trình

# .github/workflows/quet.yml
- name: Quet image
  run: |
    docker build -t ung-dung:${{ github.sha }} .
    docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
      aquasec/trivy:latest image \
      --scanners vuln,secret \
      --severity CRITICAL,HIGH \
      --exit-code 1 \
      ung-dung:${{ github.sha }}

Cờ --exit-code 1 là thứ biến phép quét thành hàng rào thật: build hỏng thay vì in ra một bảng mà không ai đọc.

Ba lưu ý khi bật:

  • Bật ở nhánh mới trước. Quét lần đầu trên một dự án cũ thường ra hàng trăm dòng, và nếu nó chặn ngay thì đội của bạn sẽ tắt nó đi trong tuần đầu.
  • docker scout cần tài khoản Docker, Trivy thì không. Tôi chuyển sang Trivy giữa chừng vì scout yêu cầu đăng nhập, và trong CI thì đó là thêm một bí mật phải quản lý.
  • Số lỗ hổng không phải điểm số. Một CVE trong thư viện bạn thật sự gọi tới nguy hiểm hơn hai mươi CVE trong gói chỉ nằm đó. --severity giúp lọc, nhưng đọc vẫn phải đọc.

Thử ba mươi giây

Quét chính image của bạn, không cần cài gì:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy:latest image --scanners vuln,secret \
  --severity CRITICAL,HIGH --quiet <image-cua-ban>

Rồi đổi một dòng FROM sang bản -alpine hoặc -slim và quét lại. Trong phép đo của tôi, đúng một dòng đó đưa 56 lỗ hổng nghiêm trọng về 0.

Phần cuối của sê-ri là danh sách kiểm trước khi lên sản xuất, gom lại mọi con số đã đo.