Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn là kỹ sư DevOps tại một startup fintech ở TP.HCM. Pipeline của bạn chạy mượt: code merge, test xanh, build ra image, deploy tự động lên production trong vòng 8 phút. Đội ngũ tự hào vì "ship nhanh". Rồi một sáng thứ Hai, đội bảo mật gửi email khẩn: một thư viện xử lý ảnh mà app đang dùng có lỗ hổng nghiêm trọng, cho phép kẻ tấn công thực thi mã từ xa. Lỗ hổng đó đã được công bố công khai 4 tháng trước. Pipeline của bạn đã deploy nó lên production 200 lần mà không ai hay biết.
Đây chính là khoảng trống mà bài học hôm nay lấp đầy. Một pipeline CI/CD chạy nhanh nhưng không có "trạm kiểm soát bảo mật" thì chẳng khác gì một dây chuyền sản xuất tốc độ cao đóng gói cả sản phẩm lỗi lẫn sản phẩm tốt rồi gửi thẳng đến khách hàng. Trong thế giới hiện đại, tốc độ deploy càng cao thì tốc độ lan truyền lỗ hổng cũng càng cao — trừ khi bạn tự động hóa việc quét bảo mật ngay bên trong pipeline.
Triết lý cốt lõi ở đây là "shift left": dịch chuyển việc kiểm tra bảo mật sang bên trái của vòng đời phần mềm, tức là kiểm tra sớm ngay khi lập trình viên còn đang viết code hoặc mở pull request, thay vì đợi đến khi đã lên production rồi mới "chữa cháy". Sửa một lỗ hổng ở giai đoạn code có thể tốn vài phút; sửa nó khi đã lộ ra ngoài production có thể tốn hàng tỷ đồng và cả danh tiếng công ty.
Trong bài này, chúng ta sẽ đi sâu vào ba loại quét bảo mật tự động phổ biến nhất trong CI/CD: SAST, DAST và SCA. Bạn sẽ hiểu mỗi loại soi vào đâu, phát hiện được gì, bỏ sót gì, và cách gắn chúng vào pipeline một cách thực dụng — không lý thuyết suông.
Khái niệm cốt lõi
Ba loại quét không thay thế nhau — chúng bổ sung cho nhau, giống như ba lớp phòng thủ soi vào ba góc nhìn khác nhau của cùng một ứng dụng.
SAST — Static Application Security Testing
SAST quét mã nguồn tĩnh, nghĩa là nó đọc code của bạn mà không cần chạy ứng dụng. Hãy hình dung một chuyên gia đọc từng dòng code, tìm những mẫu (pattern) nguy hiểm: một chuỗi SQL được ghép trực tiếp từ input người dùng (SQL injection), một giá trị người dùng nhập được in thẳng ra HTML mà không escape (XSS), hay một mật khẩu hard-code ngay trong file config.js.
- Đối tượng: source code, ở dạng chưa biên dịch hoặc chưa chạy.
- Điểm mạnh: chạy rất sớm (ngay trên pull request), chỉ ra chính xác file và dòng code có vấn đề, không cần môi trường chạy.
- Điểm yếu: không "nhìn thấy" được lỗi phát sinh khi ứng dụng vận hành thực tế (ví dụ lỗi cấu hình server, lỗi logic phiên đăng nhập). Dễ sinh ra false positive — báo động giả.
- Công cụ phổ biến: Semgrep, Snyk Code, SonarQube, CodeQL (của GitHub).
DAST — Dynamic Application Security Testing
DAST làm điều ngược lại: nó tấn công ứng dụng đang chạy từ bên ngoài, giống như một hacker thật sự. Nó không cần biết code bên trong (black-box testing), mà gửi các request độc hại đến ứng dụng đã deploy lên môi trường staging, rồi quan sát phản ứng: liệu có thể vượt qua trang đăng nhập không, có bị lộ thông tin nhạy cảm trong header không, có bị injection không.
- Đối tượng: ứng dụng đang chạy (running application), thường trên môi trường staging.
- Điểm mạnh: phát hiện lỗ hổng thực tế khi hệ thống vận hành, bao gồm cả lỗi cấu hình runtime mà SAST không thấy.
- Điểm yếu: chạy chậm hơn nhiều (có thể vài chục phút), cần một môi trường đã deploy, và không chỉ ra được chính xác dòng code gây lỗi.
- Công cụ phổ biến: OWASP ZAP, Burp Suite, StackHawk.
SCA — Software Composition Analysis
SCA quét các thư viện bên thứ ba mà dự án của bạn phụ thuộc vào. Sự thật phũ phàng: 80–90% code trong một ứng dụng hiện đại không phải do bạn viết — mà đến từ npm, Maven, PyPI, Docker base image... SCA đối chiếu danh sách dependency của bạn với các cơ sở dữ liệu lỗ hổng công khai (như CVE, GitHub Advisory Database) để cảnh báo: "Thư viện log4j phiên bản 2.14 của bạn dính CVE-2021-44228, hãy nâng cấp lên 2.17".
- Đối tượng: dependency tree —
package-lock.json,pom.xml,requirements.txt, các layer trong Docker image. - Điểm mạnh: bắt được lỗ hổng đã biết trong thư viện, thường kèm sẵn hướng dẫn nâng cấp phiên bản để vá.
- Điểm yếu: chỉ biết được những lỗ hổng đã được công bố; không phát hiện lỗi zero-day hay lỗi trong code của chính bạn.
- Công cụ phổ biến: Snyk Open Source, Dependabot, Trivy, OWASP Dependency-Check.
Bảng so sánh nhanh
| Loại | Soi vào cái gì | Cần chạy app? | Thời điểm trong pipeline | Ví dụ công cụ |
|---|---|---|---|---|
| SAST | Mã nguồn của bạn | Không | Rất sớm — trên PR | Semgrep, SonarQube, CodeQL |
| DAST | App đang chạy | Có (staging) | Sau khi deploy staging | OWASP ZAP, Burp Suite |
| SCA | Thư viện phụ thuộc | Không | Sớm — trên PR/build | Snyk, Dependabot, Trivy |
Tình huống thực tế
Ví dụ 1 — Log4Shell và sức mạnh của SCA (bối cảnh toàn cầu, bài học cho Việt Nam)
Tháng 12/2021, lỗ hổng Log4Shell (CVE-2021-44228) trong thư viện log4j của Java làm rung chuyển toàn ngành. Đây là một trong những lỗ hổng nghiêm trọng nhất lịch sử, điểm CVSS 10/10, cho phép thực thi mã từ xa chỉ bằng một chuỗi ký tự đặc biệt.
Điều đáng nói: log4j là thư viện bên thứ ba, gần như không lập trình viên nào tự tay động vào nó. Những công ty đã tích hợp SCA (như Snyk hoặc Dependabot) vào pipeline nhận cảnh báo tự động chỉ vài giờ sau khi CVE được công bố. Một ngân hàng số tại Singapore mà tôi biết đã vá xong toàn bộ hệ thống trong 36 giờ nhờ SCA quét ra chính xác 47 service đang dùng phiên bản dính lỗi. Ngược lại, nhiều đội không có SCA phải huy động cả team ngồi grep thủ công qua hàng trăm repo suốt cả tuần.
Bài học: SCA không phải "có thì tốt" — nó là tuyến phòng thủ đầu tiên cho phần code bạn không kiểm soát. Trong một sự cố toàn ngành, sự khác biệt giữa "có SCA" và "không SCA" là 36 giờ so với 1 tuần.
Ví dụ 2 — Startup thương mại điện tử Việt Nam và cú vấp secret trong code
Một startup TMĐT ở Hà Nội (khoảng 25 kỹ sư) từng gặp sự cố khi một lập trình viên junior vô tình commit khóa AWS access key vào repo để "test cho nhanh". Key đó nằm im trong lịch sử Git suốt 3 tuần. Một bot dò quét GitHub công khai đã tìm thấy nó và trong vòng 6 giờ, kẻ tấn công đã tạo hàng loạt máy ảo để đào tiền mã hóa, gây hóa đơn AWS phát sinh gần 180 triệu đồng.
Sau sự cố, đội tích hợp SAST kèm secret scanning (dùng Semgrep với ruleset bảo mật và gitleaks) vào pipeline, chạy như một bước bắt buộc trên mọi pull request. Từ đó, bất kỳ pattern nào giống access key, private key hay token đều bị chặn ngay tại bước CI, PR không thể merge cho đến khi được dọn sạch. Trong 6 tháng sau, hệ thống đã chặn thêm 3 lần commit chứa secret nữa — mỗi lần đều có thể là một sự cố 180 triệu khác.
Bài học: SAST không chỉ tìm SQL injection hay XSS. Cấu hình thêm secret scanning biến nó thành lưới chặn tự động cho một trong những lỗi tốn kém và phổ biến nhất: lộ credential trong code.
Ví dụ 3 — Đội DevOps đặt DAST sai chỗ và cái giá của pipeline chậm
Một công ty SaaS logistics ở Đông Nam Á quyết định "làm bảo mật cho nghiêm túc" bằng cách nhét DAST (OWASP ZAP quét full) vào mọi pull request. Ý định rất tốt, nhưng hậu quả là mỗi PR mất thêm 25–40 phút chỉ để chờ DAST quét. Kỹ sư bắt đầu than phiền, một số người tìm cách bypass, và cuối cùng đội tắt luôn bước quét vì "làm chậm tiến độ".
Sau khi xem lại, họ tái cấu trúc: SAST và SCA chạy nhanh (dưới 2 phút) trên mọi PR; còn DAST — vốn cần app chạy thật và tốn thời gian — chỉ chạy trên nhánh chính sau khi deploy lên staging, mỗi đêm một lần (nightly). Kết quả: PR nhanh trở lại, DAST vẫn được chạy đều đặn, và tỷ lệ kỹ sư "chấp nhận" bước bảo mật tăng vọt.
Bài học: Đặt đúng công cụ vào đúng giai đoạn quan trọng hơn là quét tất cả mọi lúc. Bảo mật mà làm pipeline chậm đến mức bị vô hiệu hóa thì còn tệ hơn không có bảo mật.
Hướng dẫn từng bước
Dưới đây là cách gắn ba loại quét vào một pipeline GitHub Actions theo trình tự hợp lý. Nguyên tắc: cái gì nhanh và sớm thì chạy trên PR, cái gì chậm và cần app chạy thì đẩy về sau.
Bước 1 — SCA và SAST trên mỗi pull request (nhanh, chặn sớm).
name: Security Scan
on: [pull_request]
jobs:
sca:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# SCA: quét dependency tìm CVE đã biết
- name: Trivy dependency scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
severity: 'HIGH,CRITICAL'
exit-code: '1' # fail pipeline nếu có lỗ hổng nặng
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# SAST: quét source code + secret
- name: Semgrep scan
uses: returntocorp/semgrep-action@v1
with:
config: 'p/security-audit p/secrets'
Bước 2 — Đặt "quality gate" bằng ngưỡng severity. Đừng để pipeline fail vì mọi cảnh báo nhỏ. Ban đầu chỉ nên chặn (exit-code: 1) với mức HIGH và CRITICAL; các mức LOW/MEDIUM chỉ ghi log để theo dõi. Điều này giữ pipeline không bị nghẽn vì báo động vặt trong khi vẫn chặn được thứ thực sự nguy hiểm.
Bước 3 — DAST trên staging, chạy định kỳ (không phải mỗi PR).
name: DAST Nightly
on:
schedule:
- cron: '0 2 * * *' # 2h sáng mỗi ngày
jobs:
dast:
runs-on: ubuntu-latest
steps:
- name: OWASP ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.12.0
with:
target: 'https://staging.myapp.vn'
Bước 4 — Đưa kết quả về nơi lập trình viên nhìn thấy. Cấu hình các công cụ upload kết quả dưới định dạng SARIF lên tab Security của GitHub, hoặc comment trực tiếp vào pull request. Bảo mật chỉ có tác dụng khi lập trình viên thấy được kết quả ngay tại nơi họ làm việc, chứ không phải trong một dashboard mà chẳng ai mở.
Bước 5 — Chọn chế độ "blocking" hay "non-blocking" một cách có chủ đích. Với dự án mới, hãy bật blocking ngay từ đầu. Với dự án cũ đang có sẵn hàng trăm cảnh báo tồn đọng, hãy bắt đầu ở chế độ non-blocking (chỉ cảnh báo), thiết lập "baseline" để bỏ qua các lỗi cũ, rồi dần dần chỉ chặn với lỗ hổng mới phát sinh kể từ nay. Cách này tránh làm tê liệt cả đội ngay ngày đầu tiên.
Lỗi thường gặp & mẹo
Lỗi 1 — Bật blocking full ngay ngày đầu trên dự án cũ. Bạn tích hợp SCA vào một codebase 5 năm tuổi, và ngay lập tức pipeline fail với 340 cảnh báo. Cả đội hoảng loạn, và bước quét bị tắt trong tuần. Mẹo: dùng cơ chế baseline/ignore để "đóng băng" nợ cũ, chỉ chặn lỗi mới. Trả nợ cũ dần dần trong các sprint riêng.
Lỗi 2 — Bỏ mặc false positive. SAST nổi tiếng báo động giả. Nếu kỹ sư liên tục thấy cảnh báo sai, họ sẽ mất niềm tin và phớt lờ cả những cảnh báo thật (hiện tượng "alert fatigue"). Mẹo: tinh chỉnh ruleset cho phù hợp với ngôn ngữ và framework của bạn; đánh dấu và loại trừ các false positive đã xác nhận để chúng không xuất hiện lại.
Lỗi 3 — Quên quét Docker image. Nhiều đội quét code rất kỹ nhưng lại dùng base image node:16 cũ chứa hàng chục lỗ hổng ở tầng OS. Mẹo: thêm bước trivy image để quét cả container image sau khi build — SCA không chỉ là chuyện package.json.
Lỗi 4 — Nhét DAST vào mỗi PR. Như ví dụ 3 đã nói, DAST chậm và cần app chạy thật; đặt nó trên mỗi PR sẽ giết chết trải nghiệm dev. Mẹo: để DAST chạy nightly hoặc trước mỗi release trên staging.
Lỗi 5 — Coi ba loại quét là thay thế cho nhau. Có đội nghĩ "tôi có SAST rồi thì khỏi cần SCA". Sai. SAST không thấy lỗ hổng trong thư viện, SCA không thấy lỗi logic trong code bạn, DAST không thấy secret hard-code. Mẹo: dùng cả ba như ba lớp bổ sung, không phải ba lựa chọn.
Mẹo vàng — bảo mật là văn hóa, không phải cái cổng. Công cụ chỉ hiệu quả khi kèm quy trình rõ ràng: ai chịu trách nhiệm khi có cảnh báo, deadline vá lỗi theo mức độ (ví dụ CRITICAL vá trong 24h), và cách xử lý ngoại lệ. Một pipeline quét bảo mật không có người xử lý kết quả chỉ là một máy tạo báo động vô nghĩa.
Bài tập thực hành
- Phân loại nhanh (5 phút): Với mỗi lỗ hổng sau, hãy xác định loại quét nào (SAST/DAST/SCA) có khả năng phát hiện nó cao nhất:
settings.py.
- (b) Thư viện express đang dùng dính một CVE mức HIGH.
- (c) Trang /admin có thể truy cập mà không cần đăng nhập trên môi trường chạy.
- (d) Một biến input người dùng được ghép thẳng vào câu SQL.- Tích hợp thực tế (30–45 phút): Lấy một repo Node.js hoặc Python bất kỳ của bạn, thêm một workflow GitHub Actions chạy Trivy (SCA) và Semgrep (SAST) trên mỗi pull request. Cấu hình để pipeline chỉ fail với mức HIGH và CRITICAL.
- Suy ngẫm về quality gate: Team bạn có một dự án legacy với 200 cảnh báo tồn đọng. Hãy viết ra chiến lược 3 bước để đưa quét bảo mật vào mà không làm tê liệt đội ngũ. (Gợi ý: nghĩ về baseline, non-blocking, và "chỉ chặn lỗi mới".)
- Thử nghiệm DAST: Chạy OWASP ZAP Baseline Scan (bản Docker) nhắm vào một ứng dụng web demo (ví dụ OWASP Juice Shop). Quan sát báo cáo và tự trả lời: những lỗi ZAP tìm ra này, liệu SAST có phát hiện được không? Vì sao?
Tóm tắt
- Pipeline CI/CD nhanh mà thiếu quét bảo mật tự động sẽ nhân bản lỗ hổng lên production với tốc độ chóng mặt. Giải pháp là shift left — kiểm tra bảo mật sớm và tự động ngay trong pipeline.
- SAST đọc mã nguồn bạn viết (tìm SQL injection, XSS, secret hard-code); chạy sớm, nhanh, nhưng dễ báo động giả và không thấy lỗi runtime.
- SCA quét thư viện bạn mượn (đối chiếu với CVE); là tuyến phòng thủ đầu cho phần code bạn không kiểm soát, nhưng chỉ biết lỗ hổng đã công bố.
- DAST tấn công app đang chạy như một hacker thật; bắt được lỗi runtime và cấu hình, nhưng chậm và cần môi trường staging.
- Ba loại bổ sung cho nhau, không thay thế nhau. Đặt SAST và SCA trên mỗi PR; đẩy DAST về nightly/pre-release trên staging.
- Thành công không nằm ở việc bật thật nhiều công cụ, mà ở việc đặt đúng công cụ đúng chỗ, dùng quality gate hợp lý (chặn HIGH/CRITICAL), xử lý false positive, và xây dựng quy trình để có người thực sự vá lỗi. Bảo mật là văn hóa, không phải một cái cổng chặn.