Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn vừa docker pull một image nginx từ Docker Hub, đóng gói ứng dụng của công ty, đẩy lên production và đi ngủ. Sáng hôm sau, đội security báo rằng chính cái base image đó chứa một lỗ hổng OpenSSL đã được công bố CVE từ 8 tháng trước, và một con bot đã quét thấy nó, khai thác được, rồi cài crypto-miner lên cluster của bạn. Hóa đơn cloud tháng đó tăng gấp ba. Đây không phải kịch bản hù dọa — nó xảy ra hàng tuần với các công ty thật.
Container mang lại cho DevOps tốc độ đóng gói và triển khai chưa từng có, nhưng chính sự tiện lợi "kéo một image về là chạy được ngay" lại là con dao hai lưỡi. Mỗi image bạn build hay pull đều là một hộp đen chứa hàng trăm gói phần mềm (OS packages, thư viện ngôn ngữ, binary) mà bạn không tự tay viết. Trong số đó, bao nhiêu gói có lỗ hổng đã biết? Có secret nào bị lỡ tay commit vào layer không? Có ai đã chèn phần mềm độc hại vào chưa?
Bài này tập trung vào container & image scanning — nghệ thuật soi rọi vào bên trong image và runtime để phát hiện rủi ro trước khi kẻ tấn công làm điều đó. Đây là một mắt xích cụ thể trong bức tranh bảo mật lớn hơn (secret management và policy-as-code sẽ có bài riêng). Ở đây, chúng ta đi sâu vào: bề mặt tấn công của container, cách các scanner hoạt động, và cách nhúng scanning vào pipeline sao cho không làm chậm đội phát triển.
Khái niệm cốt lõi
Bề mặt tấn công của một container
Trước khi scan, phải hiểu ta đang phòng thủ chống lại cái gì. Bề mặt tấn công của container chia làm hai lớp lớn:
1. Lớp Image (tĩnh — trước khi chạy):
- Base image dễ tổn thương (vulnerable base): Bạn
FROM ubuntu:20.04nhưng bản tag đó đã kéo theoglibc,openssl,curlvới các CVE chưa vá. Đây là nguồn lỗ hổng phổ biến nhất, chiếm phần lớn số phát hiện trong mọi lần scan. - Secret bị rò rỉ (leaked secret): Một
AWS_SECRET_ACCESS_KEY, private key SSH, hay token API bịCOPY . .vào image. Kể cả khi bạn xóa nó ở layer sau, nó vẫn nằm trong layer history và aidocker saveimage ra là moi được. - Malware / gói độc hại: Image từ registry công khai không đáng tin, hoặc dependency bị tấn công supply-chain (typosquatting trên npm/PyPI), mang theo backdoor hoặc miner.
- Cấu hình build kém: Image chạy bằng
USER root, chứa quá nhiều công cụ (bash, curl, package manager) làm giàu "bộ đồ nghề" cho attacker sau khi vào được.
- Privileged container: Container chạy với cờ
--privilegedgần như có toàn quyền như trên host, phá bỏ ranh giới cách ly. - Host mount nguy hiểm: Mount
/var/run/docker.sockhay/từ host vào container — attacker thoát ra host chỉ trong vài lệnh. - Capability abuse: Container được cấp các Linux capability thừa thãi như
CAP_SYS_ADMIN,CAP_NET_ADMINmở đường cho escalation. - Hành vi bất thường lúc chạy: Một tiến trình lạ khởi động, kết nối ra IP lạ, ghi vào thư mục bất thường — dấu hiệu đã bị xâm nhập.
Image scanning hoạt động như thế nào
Một image scanner làm ba việc chính:
- Xây dựng SBOM (Software Bill of Materials): Scanner "mổ xẻ" từng layer của image, liệt kê mọi thành phần — tên gói, phiên bản chính xác. SBOM giống như bảng thành phần in trên bao bì thực phẩm: bạn biết đích xác trong hộp có gì.
- Đối chiếu với cơ sở dữ liệu lỗ hổng: Danh sách gói được so với các nguồn CVE (NVD, GitHub Advisory, database riêng của distro như Debian Security Tracker). Mỗi match tạo ra một finding kèm mức độ nghiêm trọng: Critical / High / Medium / Low.
- Quét secret & misconfiguration: Scanner tốt còn dò pattern của secret (regex cho API key, private key) và soi Dockerfile/manifest tìm cấu hình yếu.
CVE và điểm CVSS — đọc cho đúng
Mỗi lỗ hổng có một mã CVE (ví dụ CVE-2021-44228 — chính là Log4Shell huyền thoại) và một điểm CVSS từ 0 đến 10. Nhưng đừng máy móc: một CVE Critical trong gói mà ứng dụng của bạn không bao giờ gọi tới thì rủi ro thực tế thấp hơn nhiều một CVE Medium nằm ngay trên đường request đi qua. Đây là lý do các scanner hiện đại thêm khái niệm reachability / exploitability và EPSS (xác suất bị khai thác thực tế) để giúp bạn ưu tiên đúng chỗ, tránh chết chìm trong hàng nghìn finding.
Tình huống thực tế
Ví dụ 1 — Startup fintech ở TP.HCM và cái base image "béo phì"
Một startup fintech tại Quận 1, TP.HCM (khoảng 25 kỹ sư) build toàn bộ microservice Node.js trên base image node:16 (bản đầy đủ, gần 900 MB). Khi đội DevOps lần đầu chạy trivy image lên các service, kết quả gây sốc: trung bình 240 lỗ hổng mỗi image, trong đó khoảng 15 ở mức Critical — hầu hết đến từ các gói OS mà ứng dụng thanh toán của họ chẳng bao giờ động tới (như perl, git, các công cụ build).
Họ làm một việc đơn giản: đổi base sang node:16-slim, rồi tiến tới node:16-alpine. Số lỗ hổng rớt xuống còn khoảng 30, image co lại còn 180 MB. Chỉ một dòng FROM được thay, không sửa code ứng dụng.
Bài học: Phần lớn "bề mặt tấn công" của image đến từ những thứ bạn không cần. Chọn base image tối giản (slim, alpine, hoặc distroless) là đòn bẩy lớn nhất, rẻ nhất để giảm rủi ro. Scan chỉ ra vấn đề; kỷ luật chọn base mới giải quyết nó tận gốc.
Ví dụ 2 — Secret nằm trong layer suốt 6 tháng ở một công ty e-commerce
Một công ty thương mại điện tử tại Đông Nam Á (đội ~40 người) vô tình COPY . . cả file .env chứa token của cổng thanh toán vào image, rồi ở dòng dưới mới RUN rm .env. Họ nghĩ đã xóa sạch. Sáu tháng sau, khi bật tính năng secret scanning của Trivy trong CI, scanner báo đỏ: token vẫn nằm nguyên trong một layer trung gian vì mỗi lệnh Dockerfile tạo một layer bất biến — xóa ở layer sau không xóa được ở layer trước.
Hậu quả: họ phải xoay (rotate) toàn bộ token cổng thanh toán, rà lại log truy cập registry, và viết lại Dockerfile dùng multi-stage build + .dockerignore.
Bài học: docker rm một file không làm nó biến mất khỏi image history. Secret scanning phải chạy trên image đã build, không chỉ trên source code. Và nguyên tắc vàng: đừng bao giờ đưa secret vào image — hãy inject lúc runtime.
Ví dụ 3 — Log4Shell và giá trị của SBOM
Tháng 12/2021, khi CVE Log4Shell (CVE-2021-44228) bùng nổ, câu hỏi sinh tử với mọi công ty là: "Chúng ta có đang chạy phiên bản Log4j dính lỗi ở đâu không?" Những đội đã sinh SBOM cho mọi image chỉ cần một câu query cơ sở dữ liệu để biết chính xác service nào, image nào, môi trường nào bị ảnh hưởng — họ vá xong trong vài giờ. Những đội không có SBOM phải đi lục tay từng repo, mất nhiều ngày, và không bao giờ chắc chắn đã tìm hết.
Bài học: Scanning không chỉ để chặn build hôm nay — nó tạo ra kho SBOM giúp bạn phản ứng thần tốc khi một CVE mới nổ ra ngày mai với image bạn đã deploy từ lâu.
Hướng dẫn từng bước
Dưới đây là quy trình nhúng scanning vào vòng đời một image, từ local đến production.
Bước 1 — Scan cục bộ ngay khi phát triển (shift left). Cài Trivy và cho developer chạy trước khi push:
# Quét image tìm lỗ hổng
trivy image myapp:dev
# Chỉ hiện Critical và High, bỏ qua tiếng ồn
trivy image --severity CRITICAL,HIGH myapp:dev
# Quét cả secret và misconfiguration
trivy image --scanners vuln,secret,misconfig myapp:dev
Bước 2 — Sinh và lưu SBOM.
trivy image --format cyclonedx --output sbom.json myapp:1.0.0
Lưu SBOM này như một artifact của mỗi build. Sau này CVE mới xuất hiện, bạn scan lại SBOM cũ mà không cần build lại.
Bước 3 — Nhúng vào CI pipeline và fail build đúng mức. Ví dụ với GitHub Actions:
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
severity: CRITICAL,HIGH
exit-code: '1' # fail pipeline nếu có finding
ignore-unfixed: true # bỏ qua lỗ hổng chưa có bản vá
Cờ ignore-unfixed rất quan trọng: đừng chặn build vì một CVE mà upstream còn chưa ra bản vá — bạn chẳng làm gì được, chỉ tạo bực bội.
Bước 4 — Scan tại registry (lớp phòng thủ thứ hai). Bật scanning trên registry: Amazon ECR có "scan on push", Harbor tích hợp Trivy quét định kỳ. Điều này bắt được cả những image đẩy lên qua đường khác pipeline chính, và tự động re-scan khi database CVE cập nhật.
Bước 5 — Áp policy admission ở cluster. Dùng một admission controller (ví dụ Kyverno — sẽ có bài về policy-as-code) để từ chối deploy image không được ký hoặc chưa qua scan. Đây là chốt chặn cuối trước production.
Bước 6 — Runtime security (bổ sung).
Cài công cụ như Falco để phát hiện hành vi bất thường lúc chạy: một shell được spawn trong container, kết nối ra IP lạ, ghi file vào /etc. Scanning tĩnh không thấy được những gì xảy ra sau khi container đã chạy.
Lỗi thường gặp & mẹo
Lỗi 1 — Scan nhưng không ai hành động. Đội bật scanner, thấy 500 finding, choáng, rồi bỏ qua tất cả. Hậu quả là scanning trở thành nghi thức vô nghĩa. Mẹo: Bắt đầu chỉ fail build ở mức Critical, dùng ignore-unfixed, và dùng file .trivyignore để tạm bỏ qua các CVE đã có kế hoạch xử lý (kèm ngày hết hạn để rà lại).
Lỗi 2 — Chỉ scan lúc build, không bao giờ scan lại. Một image "sạch" hôm nay có thể "bẩn" tuần sau khi một CVE mới được công bố cho gói nó đang chứa — dù bạn không đổi một dòng nào. Mẹo: Bật re-scan định kỳ tại registry và giữ SBOM để truy vấn nhanh.
Lỗi 3 — Base image quá "béo". Càng nhiều gói, càng nhiều CVE. Mẹo: Ưu tiên distroless (chỉ có runtime, không shell, không package manager) hoặc alpine/slim. Ít gói = ít bề mặt tấn công = ít finding = ít việc phải làm.
Lỗi 4 — Nhầm scanning với đủ bảo mật. Image sạch mà chạy --privileged với mount docker.sock thì vẫn thảm họa. Mẹo: Kết hợp scanning (tĩnh) với hardening runtime: chạy non-root, read-only filesystem, drop mọi capability không cần, cấm privileged.
Lỗi 5 — Không pin phiên bản base image. FROM node:16 hôm nay và tuần sau có thể là hai image khác nhau. Mẹo: Pin theo digest (FROM node:16@sha256:...) để build có thể tái lập và scan có ý nghĩa.
Mẹo vàng — Đưa thông tin vào đúng người, đúng lúc. Finding phải xuất hiện ngay trên pull request của developer, kèm gợi ý "nâng gói X lên bản Y để hết CVE". Bảo mật thành công khi nó là một phần công việc thường ngày của kỹ sư, không phải một cổng gác do đội security dựng lên chặn đường.
Bài tập thực hành
- Scan cơ bản: Chọn một image bạn đang dùng (ví dụ
python:3.9), chạytrivy image python:3.9 --severity CRITICAL,HIGH. Đếm số lỗ hổng. Sau đó đổi sangpython:3.9-slimrồipython:3.9-alpine, scan lại và ghi lại con số. Bạn giảm được bao nhiêu phần trăm?
- Bắt secret: Tạo một Dockerfile cố tình
COPYmột file chứa chuỗi giả dạng AWS key (AKIAIOSFODNN7EXAMPLE), rồiRUN rmnó ở layer sau. Build image và chạytrivy image --scanners secret. Scanner có phát hiện ra không? Điều này chứng minh nguyên tắc gì về layer?
- Sinh SBOM: Sinh SBOM định dạng CycloneDX cho một image, mở file JSON và tìm xem có bao nhiêu gói được liệt kê. Thử tưởng tượng một CVE mới nổ ra cho một trong các gói đó — bạn sẽ query SBOM thế nào để biết mình có bị ảnh hưởng?
- Nhúng CI (nâng cao): Viết một GitHub Actions (hoặc GitLab CI) job build image rồi scan, fail pipeline nếu có Critical với
ignore-unfixed: true. Thử tạo một finding giả để xác nhận pipeline thực sự đỏ.
Tóm tắt
Container scanning là hàng phòng thủ đầu tiên và rẻ nhất trong bảo mật container. Ba ý cốt lõi cần nhớ:
- Hiểu bề mặt tấn công hai lớp: lớp Image (base dễ tổn thương, secret rò rỉ, malware) và lớp Runtime (privileged, host mount, capability abuse). Scanning giải quyết chủ yếu lớp Image.
- Scanner làm ba việc: sinh SBOM, đối chiếu CVE database, dò secret & misconfig. Hãy đọc finding thông minh — ưu tiên theo mức độ và khả năng bị khai thác thực tế, đừng máy móc theo điểm CVSS.
- Nhúng scanning vào mọi khâu: local (shift left) → CI (fail build ở Critical,
ignore-unfixed) → registry (scan on push + re-scan định kỳ) → admission policy → runtime security. Giữ SBOM để phản ứng thần tốc khi CVE mới nổ ra.