Thử hình dung một kịch bản quen thuộc: Đội ngũ developer của bạn vừa đẩy một bản cập nhật tính năng mới lên nhánh chính. Hệ thống CI/CD nhận lệnh, tự động khởi chạy npm ci hoặc cargo build. Pipeline chạy mượt mà, các bài test đều hiển thị màu xanh an toàn, và ứng dụng được đóng gói thành công.
Nhưng đằng sau bề mặt hoàn hảo đó, một đoạn mã độc nằm sâu trong một thư viện mã nguồn mở vừa âm thầm rà soát toàn bộ biến môi trường. Chúng thu thập trái phép toàn bộ GitHub Personal Access Tokens (PATs), khóa bảo mật AWS, cấu hình Kubernetes và âm thầm đẩy gói dữ liệu đó về máy chủ Command & Control (C2) của hacker. Hệ thống tường lửa không hề kêu vang. Không có dấu hiệu sập mạng. Mọi thứ diễn ra không bị phát hiện chỉ trong vài giây biên dịch.
Đó chính xác là cách các cuộc tấn công chuỗi cung ứng (Supply Chain Attack) đang tàn phá hạ tầng của nhiều doanh nghiệp. Khi server tự động tải hàng ngàn dependency không qua kiểm duyệt từ Internet, hệ thống của bạn đang đối mặt với rủi ro cực kỳ lớn. Để giải quyết triệt để vấn đề này, việc kiểm soát Outbound Traffic bằng Proxy kết hợp với cô lập môi trường Build được xem là phương án phòng thủ mang lại hiệu quả cao, tước bỏ hoàn toàn quyền liên lạc của mã độc ngay tại trận.
Vậy làm thế nào để cô lập tiến trình build vào một hộp cát (sandbox) an toàn bằng các công nghệ hiện đại? Cách thiết lập luồng mạng trong suốt ở Layer 3/4 diễn ra như thế nào để chặn đứng rò rỉ dữ liệu mà không làm ảnh hưởng đến trải nghiệm của developer? Hãy cùng đi sâu vào kiến trúc kỹ thuật chi tiết ngay sau đây.
Supply Chain Attack: Khi mối nguy không đến từ bên ngoài
Trong quy trình phát triển phần mềm hiện đại, các dự án công nghệ phụ thuộc sâu sắc vào các hệ sinh thái như npm, PyPI, Cargo hay Docker Hub. Kẻ tấn công hiểu rất rõ điều này: thay vì mất công dò tìm lỗ hổng để xâm nhập trực tiếp vào VPS của bạn, chúng chỉ cần đầu độc một thư viện mã nguồn mở phổ biến.
Bài học từ worm Shai-Hulud (npm) và vụ giả mạo proc-macro1 (Rust – 08/2026)
Lấy ví dụ sự kiện diễn ra vào tháng 08/2026, cộng đồng Rust đã chứng kiến một đợt lây nhiễm tinh vi thông qua package proc-macro1. Mã độc này lây lan sang các crate có lượt tải cao như arrayref. Hacker không cần khai thác lỗ hổng hệ điều hành; chúng chỉ chèn một tệp build.rs chứa mã độc. Ngay khi CI Runner thực thi lệnh cargo build, đoạn script này tự động kích hoạt, tải payload từ xa, sử dụng VBScript và PowerShell để thoát khỏi cơ chế quản lý tiến trình (Job Object) của Cargo, sau đó rà soát và đánh cắp dữ liệu.
Trước đó, hệ sinh thái Node.js cũng phải đối mặt với sâu máy tính Shai-Hulud (2025) và biến thể Mini Shai-Hulud (2026). Dòng mã độc này khai thác các hook preinstall và postinstall của npm để tự động chạy ngay khi package được tải về. Điểm nguy hiểm là nó sử dụng môi trường Bun runtime độc lập để vượt qua các công cụ giám sát Node.js truyền thống, kiểm tra trực tiếp bộ nhớ để trích xuất token, và tự nhân bản lây nhiễm chéo sang các dự án khác.
Lỗ hổng chí mạng của CI Runner: Quyền cao, hoạt động liên tục và Outbound tự do
VPS chạy CI/CD Runner tự quản lý (self-hosted/persistent runner) là một mục tiêu mang lại giá trị khai thác lớn hơn nhiều so với các runner dùng một lần (ephemeral) trên cloud. Có ba lý do chính:
- Khả năng duy trì hoạt động (Persistence): VPS Runner hoạt động liên tục từ ngày này qua tháng khác. Mã độc có thể dễ dàng thiết lập các dịch vụ chạy ngầm (systemd user services) để duy trì quyền kiểm soát, ngay cả khi developer đã gỡ bỏ thư viện chứa mã độc khỏi source code.
- Kho chứa bí mật nhạy cảm: Máy chủ build là nơi hội tụ các token cấp phát dài hạn, chứng chỉ ký số, và khóa đám mây để phục vụ bước deploy lên môi trường production.
- Lây nhiễm chéo qua Cache: Việc dùng chung các thư mục bộ nhớ đệm (
node_modules,.cargo/cache) giúp tối ưu tốc độ build, nhưng lại mở đường cho một dự án bị nhiễm độc lây lan sang toàn bộ các job khác chạy trên cùng một máy chủ.
Tư duy bảo mật ngược: Tại sao Firewall Inbound truyền thống mất khả năng giám sát?
Các kỹ sư quản trị hệ thống thường thiết lập Firewall Inbound cực kỳ khắt khe, chỉ cho phép mở các port tiêu chuẩn như 80, 443 hoặc 22. Tuy nhiên, Firewall Inbound chỉ giám sát các kết nối từ ngoài vào trong.
Trong mô hình CI/CD, chính máy chủ của bạn lại là phía chủ động khởi tạo kết nối đi ra ngoài (Outbound) để tải các package. Khi mã độc lọt vào hệ thống thông qua một lượt tải hợp lệ, nó lại tiếp tục sử dụng kết nối Outbound để gửi thông tin đánh cắp về máy chủ C2. Tường lửa Inbound hoàn toàn không nhận diện được hành vi này, tạo cơ hội cho dữ liệu rò rỉ mà không vấp phải bất kỳ cảnh báo nào.
Đó là lý do chúng ta cần chuyển đổi tư duy: Thiết lập một lớp phòng thủ chủ động bảo vệ luồng dữ liệu Outbound và kiểm duyệt mọi luồng dữ liệu chiều đi ra.

Nguyên tắc cô lập môi trường Build (Blast Radius Reduction)
Trước khi can thiệp vào luồng mạng, bước cơ bản là phải giới hạn quyền hạn của tiến trình build ở mức thấp. Việc nâng cấp bảo mật VPS bằng cách tích hợp Zero Trust thay thế phương pháp cũ đòi hỏi chúng ta không được mặc định tin tưởng bất kỳ đoạn script nào được kích hoạt trong pipeline.
Không chạy Runner trên máy host: Bắt buộc dùng Container
Quy tắc cơ bản: Tránh cài đặt trực tiếp các công cụ như Node.js, Rust, hay Python lên hệ điều hành gốc của VPS Runner. Hãy cấu hình Runner sử dụng Docker executor. Nhờ đó, mỗi job biên dịch sẽ được thực thi trong một container cách ly, đảm bảo môi trường luôn an toàn và tự động tiêu hủy toàn bộ tệp tin tạm ngay sau khi hoàn thành.
Hardening Container: Cắt giảm đặc quyền triệt để
Dù đã chạy trong container, tiến trình build vẫn có rủi ro thoát quyền (container breakout) nếu không được làm cứng (hardening) đúng kỹ thuật. Bạn cần áp dụng các cấu hình sau khi khởi chạy:
- Tước bỏ đặc quyền hạt nhân (
--cap-drop=ALL): Loại bỏ toàn bộ Linux Capabilities. Một quá trình biên dịch mã nguồn thông thường hoàn toàn không cần đến quyền can thiệp vào card mạng hay nhân hệ điều hành. - Hệ thống tệp chỉ đọc (
--read-only): Thiết lập root filesystem của container ở chế độ chỉ đọc. Bất kỳ nỗ lực nào của mã độc nhằm ghi đè tệp nhị phân hệ thống hay tải thêm backdoor đều sẽ bị hạt nhân Linux từ chối. Đối với các thư mục yêu cầu ghi cache, hãy mount chúng thẳng vào RAM thông qua cờ--tmpfs /tmp --tmpfs /var/cache. - Quản lý an toàn
docker.sock: Việc mount socket của Docker (-v /var/run/docker.sock:/var/run/docker.sock) vào container tương đương với việc giao nộp quyền Root của VPS cho tiến trình build. Nếu quy trình yêu cầu build image, hãy áp dụng mô hình Rootless Docker hoặc sử dụng các công cụ Daemonless chuyên dụng chạy ở userspace như Kaniko hay Buildah.
Chiến lược kiểm soát Outbound Traffic bằng Proxy (Default Deny)
Sau khi đã cô lập tiến trình, chúng ta tiến hành thiết lập hàng rào mạng. Việc kiểm soát Outbound Traffic bằng Proxy đưa hệ thống chuyển từ tư duy cho phép tất cả, chỉ chặn nguồn độc hại sang mô hình chặn tất cả, chỉ cho phép những gì được phê duyệt tường minh (Default Deny).
Kiến trúc 2 lớp mạng nội bộ: Cắt đứt kết nối Internet trực tiếp
Thay vì cấp phát cho Build Container một luồng kết nối Internet trực tiếp, cấu trúc mạng sẽ được phân tách thành hai khu vực chuyên biệt:
- Mạng cô lập (internal-net): Container chạy quá trình build bị giới hạn hoàn toàn trong mạng này. Hạ tầng mạng không cung cấp bất kỳ gateway nào để ra Internet.
- Mạng kết nối ngoài (egress-net): Mạng sở hữu đường định tuyến ra thế giới bên ngoài.
- Cầu nối Proxy: Đóng vai trò là chốt chặn trọng yếu đứng giữa hai mạng. Mọi request tải package từ npm hay Cargo đều phải đi qua cổng kiểm duyệt này.
Từ Squid Proxy đến Varnish Orca: Giải pháp Transparent Redirection ở Layer 3/4
Ở các mô hình truyền thống, kỹ sư thường ép luồng traffic bằng cách khai báo các biến môi trường như HTTP_PROXY, hoặc sửa cấu hình thủ công trong tệp .npmrc hay .cargo/config.toml. Mặc dù phương pháp này có tác dụng, nhưng mã độc tinh vi hoàn toàn có thể ghi đè các biến này hoặc sử dụng các thư viện mạng gọi trực tiếp socket TCP, từ đó vượt qua Proxy dễ dàng.
Để khắc phục điểm yếu này, các hệ thống hiện đại ứng dụng Varnish Orca (hoặc các kỹ thuật Transparent Proxy tương tự). Thay vì phụ thuộc vào cấu hình của package manager, phương pháp này thực hiện điều hướng luồng dữ liệu hoàn toàn trong suốt khi xây dựng Internal Proxy Gateway trên VPS Linux ở cấp độ mạng (Layer 3/Layer 4).
Cơ chế hoạt động:
- Sử dụng
iptableshoặcnftablestrên host để bắt (intercept) toàn bộ gói tin định tuyến ra port 80/443 của container. - Chuyển hướng các gói tin này vào cổng kiểm duyệt của proxy.
- Tiến trình build (npm, pip, cargo) không hề biết sự tồn tại của proxy, developer cũng không cần phải chỉnh sửa bất kỳ tệp cấu hình nào trong mã nguồn dự án.
- Mọi nỗ lực gọi kết nối ra ngoài của mã độc (dù sử dụng bất kỳ thư viện hay ngôn ngữ nào) đều bị ép buộc đi qua bộ lọc kiểm duyệt.

Xây dựng Allowlist tối ưu: Chỉ cấp phép cho registry uy tín
Dù sử dụng Squid, Varnish Orca hay bất kỳ giải pháp proxy nào, nguyên lý cốt lõi vẫn là thiết lập một danh sách trắng (Allowlist) cực kỳ khắt khe. Chỉ các tên miền thực sự phục vụ cho việc tải dependencies mới được phê duyệt.
Ví dụ một danh sách Allowlist tiêu chuẩn:
.npmjs.org
registry.npmjs.org
.crates.io
static.crates.io
.github.com
.githubusercontent.com
Khi mã độc cố gắng phân giải và kết nối tới C2 Server (ví dụ: api.hacker-server.com), Proxy sẽ đối chiếu, không tìm thấy trong Allowlist và lập tức trả về lỗi HTTP 403 Forbidden hoặc ngắt kết nối TCP.
Thực chiến: Cấu hình Proxy và Network cho CI/CD Runner bằng Docker Compose (V2/V5)
Cấu trúc hạ tầng lý thuyết đã rõ ràng. Bây giờ, chúng ta sẽ áp dụng chúng vào thực tế bằng công cụ Docker Compose hiện đại (V2/V5).
Lưu ý: Kể từ tháng 04/2025, phiên bản docker-compose cũ (v1) đã bị loại bỏ khỏi nhiều môi trường CI/CD. Cú pháp mới không còn sử dụng khai báo version ở dòng đầu tiên và lệnh thực thi chuyển thành docker compose (không có dấu gạch ngang).
Setup internal-net và egress-net
Tạo tệp docker-compose.yml để thiết lập hai mạng cô lập và cấu hình container:
services:
secure-proxy:
image: custom-transparent-proxy:latest # (Có thể tùy biến từ Squid hoặc Varnish)
container_name: secure-proxy
restart: always
volumes:
- ./proxy-config:/etc/proxy-config:ro
- ./whitelist.txt:/etc/whitelist.txt:ro
networks:
- egress-net # Đường ra Internet để tải package
- internal-net # Mạng đón nhận traffic từ Runner
cap_add:
- NET_ADMIN # Cần thiết để thao tác iptables/điều hướng Layer 3
sysctls:
- net.ipv4.ip_forward=1
ci-runner:
image: gitlab/gitlab-runner:latest
container_name: ci-runner
networks:
- internal-net # Cô lập hoàn toàn, KHÔNG cấp quyền ra egress-net
read_only: true
cap_drop:
- ALL
tmpfs:
- /tmp
- /var/run
networks:
egress-net:
driver: bridge
internal-net:
driver: bridge
internal: true # Thuộc tính quan trọng khóa chặt Internet
Cấu hình kiểm duyệt ACL chặn kết nối lạ
Nếu lớp kiểm duyệt của bạn sử dụng Squid làm core, cấu hình ACL sẽ đóng vai trò như một màng lọc bảo mật. Nó chỉ cho phép các cổng an toàn (80, 443) và đối chiếu với tệp whitelist.txt.
Một phần trích xuất cấu hình bảo mật mẫu:
acl SSL_ports port 443
acl Safe_ports port 80
acl Safe_ports port 443
acl CONNECT method CONNECT
# Vô hiệu hóa URN để phòng tránh lỗ hổng bảo mật liên quan đến giao thức
acl URN proto URN
http_access deny URN
# Chặn ngay lập tức các cổng không an toàn
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
# Đọc danh sách tên miền được phép
acl allowed_registries dstdomain "/etc/whitelist.txt"
acl localnet src 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
# Chỉ cấp phép cho IP nội bộ truy cập các registry hợp lệ
http_access allow localnet allowed_registries
http_access deny all # Mặc định chặn tất cả
http_port 3128 intercept # Chế độ intercept đón traffic từ iptables (Transparent mode)
Bịt lỗ hổng DNS Tunneling: Chặn đường lách qua cổng UDP/53
Một thủ đoạn tinh vi của hacker khi phát hiện giao thức HTTP/HTTPS bị chặn là chuyển hướng sang DNS Tunneling. Chúng thu thập token, mã hóa thành chuỗi Base64 và nối vào một tên miền do chúng kiểm soát (ví dụ: [chuoi-token].c2-server.com), sau đó phát một lệnh truy vấn phân giải tên miền (DNS Query) qua cổng UDP 53.
Để bịt kín lỗ hổng này trong kiến trúc:
- Ở cấp độ container: Không cho phép Runner tự do phân giải DNS ra ngoài. Đảm bảo luồng truy vấn DNS của Runner bị chặn hoặc điều hướng về một trình phân giải (Resolver) nội bộ được kiểm soát chặt.
- Ở cấp độ VPS Host: Tương tự như các phương pháp bảo mật VPS Linux với UFW, Fail2Ban và Hardening SSH, bạn cần cấu hình tường lửa chặn mặc định toàn bộ luồng UDP/TCP đi ra cổng 53, chỉ ngoại lệ cho IP của dịch vụ DNS nội bộ đáng tin cậy. Khi đó, mã độc không thể ép máy chủ gửi truy vấn đến một DNS công cộng lạ để rò rỉ dữ liệu.
Defense-in-Depth: Các lớp phòng thủ bổ sung cho Pipeline
Việc điều hướng và kiểm duyệt traffic ở Layer 3/4 là một lớp bảo vệ mạng mạnh mẽ, nhưng bảo mật hệ thống luôn cần áp dụng nguyên lý Defense-in-Depth (Phòng thủ chiều sâu). Việc kết hợp thêm các lớp bảo vệ ở tầng ứng dụng sẽ mang lại hiệu quả cao hơn.
Lợi thế của npm v12 và việc ép dùng Lockfile
Ở các phiên bản trước đây, cộng đồng bảo mật luôn khuyến cáo developer phải sử dụng cờ --ignore-scripts khi chạy npm ci để chặn đứng các hook tự động như preinstall và postinstall (vũ khí chính của worm Shai-Hulud).
Tuy nhiên, giới công nghệ đã chứng kiến một bước tiến lớn: Kể từ phiên bản npm v12, tính năng allowScripts đã được tắt theo cấu hình mặc định. Hệ thống sẽ tự động vô hiệu hóa toàn bộ các lifecycle scripts của third-party trừ khi người dùng chủ động phê duyệt. Cập nhật này giúp giảm thiểu rủi ro kích hoạt mã độc ngay từ đầu mà không cần kỹ sư cấu hình thủ công. Đối với các công cụ hoặc phiên bản cũ hơn, việc sử dụng các tham số vô hiệu hóa script vẫn là yêu cầu bắt buộc.
Bên cạnh đó, duy trì thói quen ép buộc hệ thống sử dụng Lockfile là nguyên tắc quan trọng:
- Node.js: Dùng
npm cithay vìnpm installđể hệ thống kiểm tra tính toàn vẹn (integrity) qua filepackage-lock.json. - Rust: Sử dụng
cargo build --locked --frozenđể Cargo biên dịch ở chế độ ngoại tuyến hoàn toàn dựa trênCargo.lock, cấm hệ thống tự ý kết nối mạng tìm kiếm gói mới.
Giám sát log từ Proxy: Thiết lập cảnh báo tức thời
Proxy không chỉ để chặn, mà còn là công cụ giám sát mạnh mẽ. Bất kỳ nỗ lực kết nối nào ra khỏi Allowlist đều bị Proxy từ chối và ghi lại trạng thái (ví dụ: TCP_DENIED/403).
Đội ngũ DevOps có thể cấu hình các tác vụ thu thập log (như Promtail hoặc Filebeat) đẩy dữ liệu về hệ thống ELK Stack hoặc Grafana Loki. Thiết lập một bộ Alert tự động: Nếu biểu đồ request bị từ chối đột ngột tăng vọt, hệ thống sẽ lập tức bắn cảnh báo về kênh Slack/Telegram. Bạn sẽ biết ngay lập tức hệ thống đang bị một dependency lạ cố gắng gọi ra ngoài để dò la cấu hình.
Dùng Private Registry làm bộ đệm kiểm duyệt package
Thay vì để Runner tải trực tiếp gói từ môi trường public (dù đã được Proxy cấp phép), các tổ chức quy mô lớn nên xây dựng Private Registry (chẳng hạn Sonatype Nexus, Verdaccio) để làm bộ đệm nội bộ.
Quy trình bảo mật sẽ hoạt động theo trình tự: Runner → Transparent Proxy → Private Registry → Public Registry. Khi đó, mọi package tải từ bên ngoài sẽ được quét tĩnh, phân tích thành phần (SCA), và cấp chứng nhận an toàn trước khi đẩy vào bộ nhớ đệm nội bộ cho Runner sử dụng. Cách làm này xử lý tận gốc nguy cơ Dependency Confusion (nhập nhèm gói nội bộ và gói công cộng).

Câu hỏi thường gặp (FAQ)
1. Tại sao Firewall Inbound truyền thống không chặn được Supply Chain Attack?
Vì Firewall truyền thống chỉ chặn luồng mạng đi vào (Inbound), trong khi mã độc chuỗi cung ứng sử dụng luồng mạng đi ra (Outbound) từ chính bên trong máy chủ CI/CD để đánh cắp dữ liệu.
2. Giải pháp kiểm soát Outbound Traffic bằng Proxy phổ biến là gì?
Có thể sử dụng Squid Proxy bằng cách cấu hình biến môi trường thủ công, hoặc ứng dụng Varnish Orca để điều hướng luồng mạng trong suốt ở Layer 3/4 mà không cần sửa code.
3. Bản cập nhật npm v12 hỗ trợ bảo mật CI/CD như thế nào?
npm v12 mặc định chuyển tính năng allowScripts thành off, tự động chặn các đoạn mã tự chạy (như preinstall hay postinstall) để ngăn mã độc kích hoạt ngầm.
4. Rủi ro bảo mật của VPS Runner so với Ephemeral Runner là gì?
VPS Runner hoạt động liên tục và dùng chung bộ nhớ đệm, khiến mã độc dễ dàng duy trì hoạt động, tạo dịch vụ ngầm và lây nhiễm chéo sang các dự án khác.
5. Allowlist (Danh sách trắng) trong Proxy CI/CD hoạt động ra sao?
Proxy chỉ cho phép tiến trình build kết nối tới các tên miền uy tín đã được phê duyệt sẵn (như registry.npmjs.org) và mặc định chặn toàn bộ các request gửi đến máy chủ lạ.
Kết luận
Các cuộc tấn công nhắm vào chuỗi cung ứng phần mềm không đòi hỏi tin tặc phải vượt qua hàng rào tường lửa phức tạp từ bên ngoài. Chúng chọn một con đường êm ái hơn: Ẩn mình bên trong chính những gói dependency mà hệ thống của bạn tự động nạp vào mỗi ngày. Khi tiến trình build bắt đầu, đó cũng là lúc mã độc tự do thu thập bí mật và đẩy ra bên ngoài thông qua những kết nối Outbound không bị giám sát.
Việc thiết lập kiến trúc mạng cô lập kết hợp kiểm soát Outbound Traffic bằng Proxy, đặc biệt là qua các công cụ điều hướng luồng mạng trong suốt ở Layer 3/4 như Varnish Orca, mang lại một cơ chế bảo vệ chủ động, mạnh mẽ. Khi luồng dữ liệu ra vào được quy hoạch rõ ràng và các giao thức tự động bị giới hạn khắt khe (như bản cập nhật của npm v12), bạn đã tước đoạt thành công công cụ giao tiếp của mã độc.
