Hãy tưởng tượng một kịch bản mà không ít DevOps Engineer và Backend Developer từng nếm trải: Bạn vừa thiết lập xong một pipeline hoàn chỉnh. Các biến môi trường nhạy cảm như AWS_ACCESS_KEY_ID, DB_PASSWORD, hay các Private API Token đã được bạn cấu hình Masked cẩn thận trên giao diện web và nạp vào job để phục vụ việc deploy. Mọi thứ trông có vẻ an toàn.
Thế nhưng, một thành viên trong team vô tình gõ sai tên một thư viện trong file package.json (kỹ thuật tấn công Typosquatting), hoặc hệ thống của bạn bị dính một cú spoofing thư viện nội bộ trên public registry (Dependency Confusion). Ngay khi lệnh npm install hoặc pip install được chạy trên server build, một hook ẩn dạng postinstall âm thầm được kích hoạt. Nó thu thập dữ liệu process.env, đóng gói lại và dùng một lệnh curl đơn giản đẩy thẳng về máy chủ điều khiển (C2 Server) của hacker.
Không có alert. Không có firewall nào chặn lại. Bạn có thể bị mất dữ liệu nhạy cảm.
Dù nhiều kỹ sư chuyển từ hệ sinh thái khác sang thường có thói quen gọi đây là GitLab Actions, nhưng nền tảng chuẩn xác mà chúng ta đang vận hành là GitLab CI/CD kết hợp cùng GitLab Runner. Dù tên gọi là gì, rủi ro ở tầng Network (cụ thể là Egress Traffic, tức lưu lượng đi ra) vẫn không thay đổi. Nếu Runner của bạn đang được kết nối trực tiếp ra Internet mà không có rào chắn kiểm duyệt ở tầng ứng dụng, bạn đang gây rủi ro cho cả mã nguồn lẫn hạ tầng của mình.
Đó chính là lý do tại sao việc cấu hình Proxy Server bảo mật luồng CI/CD đã trở thành tiêu chuẩn bảo mật thiết yếu (mandatory standard) trong các mô hình Zero Trust hiện đại để bảo vệ các hệ thống DevSecOps.
Làm thế nào để xây dựng một chốt chặn Egress Layer 7 chặt chẽ, tự động nhận diện traffic mà không bắt các developer phải cấu hình thủ công từng dòng lệnh build? Và làm sao để hạn chế rủi ro lộ mật khẩu Proxy qua log? Hãy cùng đi sâu vào kiến trúc và từng dòng lệnh cấu hình.
Pipeline CI/CD và những lỗ hổng Egress nghiêm trọng
Trước khi bắt tay vào gõ lệnh cấu hình hệ thống, chúng ta cần nhìn vào thực tế: Tại sao các giải pháp như iptables hay UFW (hoạt động ở Layer 3/4) trên máy chủ chạy Runner là chưa đủ để bảo vệ luồng CI/CD?
Supply Chain Attack: Dependency Confusion và rủi ro mất .env
Trong thế giới phát triển phần mềm hiện đại, chúng ta phụ thuộc nhiều vào các trình quản lý gói (Package Managers) như NPM (Node.js), PIP (Python) hay Composer (PHP). Các công cụ này được thiết kế với cơ chế Lifecycle Hooks, cho phép chạy các đoạn script tự động trước hoặc sau khi cài đặt package.
Kẻ tấn công thường lợi dụng các hook như preinstall hoặc setup.py để nhúng các đoạn mã thực thi âm thầm (ví dụ: node index.js > /dev/null 2>&1). Vì máy chủ GitLab Runner mặc định được cấp quyền truy cập Internet (Outbound/Egress mở), đoạn mã độc này dễ dàng duyệt qua hệ thống file, tìm các file .env hoặc đọc trực tiếp các biến môi trường đang hoạt động trong memory. Sau đó, chúng khởi tạo một HTTP POST request đẩy dữ liệu ra ngoài.

Sơ đồ minh họa cách mã độc trong package.json đánh cắp biến môi trường khi GitLab Runner không bị giới hạn luồng Outbound.
Hạn chế của Audit Log trước IP động
Bạn có thể nghĩ: Vậy thì tôi sẽ dùng UFW để block và chỉ allow IP của GitHub và NPM là xong.
Thực tế là cách này không khả thi ở quy mô production. Các dịch vụ như GitHub, npmjs, Docker Hub hay AWS API đều sử dụng mạng phân phối nội dung (CDN) hoặc Load Balancer với địa chỉ IP thay đổi liên tục. Nếu bạn cấu hình Firewall Layer 3/4 (chỉ hiểu IP và Port) để cho phép kết nối đến một IP tĩnh của CDN, mã độc cũng có thể lạm dụng chính IP đó để gửi dữ liệu về một tên miền độc hại (C2 domain) đang nằm chung trên cùng hạ tầng CDN. Tường lửa của bạn không thể kiểm tra nội dung gói tin (No Payload Inspection).
Để giải quyết bài toán này, chúng ta cần một Egress Proxy hoạt động ở tầng ứng dụng (Layer 7). Nó cho phép kiểm duyệt dựa trên Tên miền (FQDN) thay vì IP, giải quyết vấn đề IP động, đồng thời ghi log chi tiết các request (Ai gọi? Gọi URL nào? Dung lượng bao nhiêu?) để team SecOps có thể Audit khi cần.
Kiến trúc Egress Gateway: Cấu hình Proxy Server bảo mật luồng CI/CD chuẩn Zero Trust
Để xây dựng một mô hình cô lập thực sự, chúng ta sẽ áp dụng kiến trúc chia tách hạ tầng thành 2 lớp rõ rệt. Sự thay đổi đáng giá ở đây là chúng ta sẽ loại bỏ các công cụ cũ kỹ như cntlm hay xác thực bằng Basic Auth (nhập username/password cho proxy).
Mô hình 2 lớp với WireGuard VPN: Khai tử Basic Auth
Việc chèn mật khẩu proxy vào biến môi trường (dù là Masked Variables) vẫn tiềm ẩn rủi ro nếu có một tiến trình dump memory. Trong kiến trúc Cloud-Native hiện đại, chúng ta giải quyết vấn đề xác thực bằng định tuyến mạng nội bộ (Private Routing).
- VPS #1 (GitLab Runner Node): Nơi trực tiếp thực thi các job CI/CD. Máy chủ này sẽ bị tường lửa chặn các traffic đi ra Internet (Default-Deny Outbound). Nó được cài đặt một client WireGuard VPN.
- VPS #2 (Proxy Gateway): Nơi cài đặt Squid Proxy và WireGuard Server. Đây là chốt chặn mạng tập trung để quản lý kết nối ra Internet.
Nguyên lý Zero Trust: Hai VPS này được nối với nhau qua một đường hầm mã hóa WireGuard. VPS #1 sẽ có IP nội bộ là 10.8.0.2, còn VPS #2 (Proxy) là 10.8.0.1.
Trên Squid Proxy, thay vì cấu hình đòi hỏi mật khẩu (htpasswd), chúng ta chỉ cần thiết lập Access Control List (ACL) chấp nhận dải IP của WireGuard (10.8.0.2). Nghĩa là, các request đi ra từ đường hầm VPN nội bộ này mới được Proxy xác thực và đối chiếu với danh sách tên miền cho phép (Whitelist). Không có mật khẩu, hạn chế rủi ro lộ credential.

Kiến trúc cô lập 2 lớp chuẩn Zero Trust: Điều hướng luồng traffic đi qua WireGuard và chốt chặn Egress Proxy.
4 bước thực hành cấu hình Proxy Server bảo mật luồng CI/CD với Squid & WireGuard
Bài thực hành này giả định bạn đang sử dụng Ubuntu 24.04 LTS (hoặc phiên bản Ubuntu 26.04 LTS với cấu hình bảo mật cao) cho cả hai máy chủ.
Bước 1: Setup WireGuard và cài đặt Squid Proxy (không cần htpasswd)
Trên VPS #2 (Proxy Gateway), việc đầu tiên là đảm bảo mạng riêng ảo WireGuard đã kết nối thành công với VPS #1. (Giả định giao diện mạng wg0 đã up, IP của Proxy là 10.8.0.1 và IP của Runner là 10.8.0.2).
Tiến hành cài đặt Squid thông qua các lệnh sau:
Cập nhật hệ thống:
sudo apt-get update
Cài đặt Squid:
sudo apt-get install squid -y
Bật dịch vụ khởi động cùng hệ thống:
sudo systemctl enable squid
Tạo danh sách Whitelist Domain (Tên miền được phép truy cập):
Không nên mở hoàn toàn các kết nối. Hãy lập danh sách những dịch vụ mà luồng build của bạn thực sự cần gọi tới. Khởi tạo file cấu hình:
sudo nano /etc/squid/allowed_domains.txt
Nội dung file cấu hình (sử dụng dấu chấm . ở đầu để đại diện cho cả domain gốc lẫn các subdomain). Chép đoạn code sau vào file:
.github.com
.gitlab.com
.npmjs.org
.registry.npmjs.org
.docker.io
.docker.com
.pypi.org
Cấu hình Squid (/etc/squid/squid.conf):
Backup file cấu hình mặc định:
sudo cp /etc/squid/squid.conf /etc/squid/squid.conf.bak
Sau đó mở file mới để thiết lập các rule bảo mật Layer 7:
sudo nano /etc/squid/squid.conf
Ghi đè nội dung sau vào file. Chú ý logic của các bộ lọc (ACL) đã được giải thích bên trong:
# 1. Khai báo IP của GitLab Runner qua kênh WireGuard (Không dùng mật khẩu)
acl runner_vpn src 10.8.0.2/32
# 2. Lấy danh sách tên miền hợp lệ từ file text
acl allowed_sites dstdomain "/etc/squid/allowed_domains.txt"
# 3. Chỉ cho phép các cổng an toàn (chặn các cổng lạ như SMTP 25, Telnet 23)
acl Safe_ports port 80 443
acl SSL_ports port 443
acl CONNECT method CONNECT
# 4. Access Control Rules (Thứ tự áp dụng từ trên xuống dưới)
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
# Cho phép truy cập nếu request xuất phát từ IP VPN và domain nằm trong Whitelist
http_access allow runner_vpn allowed_sites
# Chặn các request không thỏa mãn điều kiện trên
http_access deny all
# Lắng nghe trên IP của WireGuard, cổng 3128 (Không public cổng này ra ngoài Internet)
http_port 10.8.0.1:3128
# Bảo mật thông tin Proxy: Ẩn phiên bản phần mềm khi trả lỗi 403
httpd_suppress_version_string on
Lưu file, parse thử cú pháp để kiểm tra nhằm tránh lỗi typo:
sudo squid -k parse
Sau đó reload dịch vụ để áp dụng cấu hình:
sudo systemctl reload squid
Bước 2: Siết chặt Firewall (UFW) và chặn Outbound từ Runner
Chuyển sang VPS #1 (GitLab Runner). Đây là bước rất quan trọng. Dù bạn đã có Proxy chuyên nghiệp, nhưng nếu Runner vẫn có quyền tự đi đường tắt ra Internet, hệ thống vẫn còn lỗ hổng. Chúng ta phải điều hướng các luồng mạng đi qua Proxy.
Cho phép SSH để bạn không bị khóa khỏi server:
sudo ufw allow 22/tcp
Cho phép mạng Loopback (Quan trọng để Docker daemon và các container nội bộ giao tiếp):
sudo ufw allow in on lo
sudo ufw allow out on lo
Cho phép traffic đi qua giao diện WireGuard (wg0):
sudo ufw allow in on wg0
sudo ufw allow out on wg0
Đổi chính sách mặc định sang việc chặn các kết nối Inbound và Outbound:
sudo ufw default deny incoming
sudo ufw default deny outgoing
Kích hoạt rào chắn và kiểm tra thông tin chi tiết:
sudo ufw enable
sudo ufw status verbose
Từ thời điểm này, nếu một container chạy job cố gắng curl https://google.com, kết nối sẽ bị Firewall Layer 3/4 drop ngay lập tức (timeout). Luồng kết nối khả thi của nó là thông qua cổng 10.8.0.1:3128.
Bước 3: Inject Proxy toàn cục cho GitLab Runner (Global Docker Config)
Nếu làm theo cách cũ, bạn sẽ phải chèn cờ --build-arg HTTP_PROXY=... vào từng lệnh docker build trong các dự án. Việc này dễ sai sót và tạo ra gánh nặng bảo trì (maintenance burden) cho developer.
Thay vào đó, theo chuẩn tài liệu vận hành GitLab Runner, chúng ta có thể lợi dụng tính năng pre_build_script để tự động inject cấu hình proxy vào Docker client của các container trước khi các job bắt đầu.
Mở file cấu hình của Runner trên VPS #1:
sudo nano /etc/gitlab-runner/config.toml
Chỉnh sửa section [[runners]] như sau (giữ nguyên khối code):
[[runners]]
name = "secure-docker-runner"
url = "https://gitlab.com/"
token = "glrt-your-runner-token"
executor = "docker"
# Inject file config.json của Docker để tự động điều hướng các tiến trình build qua proxy
pre_build_script = """
mkdir -p ~/.docker
cat < ~/.docker/config.json
{
"proxies": {
"default": {
"httpProxy": "http://10.8.0.1:3128",
"httpsProxy": "http://10.8.0.1:3128",
"noProxy": "localhost,127.0.0.1,.svc.cluster.local"
}
}
}
EOF
"""
# Thiết lập môi trường cho bản thân container chạy job (Dành cho npm, pip, curl...)
environment = [
"http_proxy=http://10.8.0.1:3128",
"https_proxy=http://10.8.0.1:3128",
"HTTP_PROXY=http://10.8.0.1:3128",
"HTTPS_PROXY=http://10.8.0.1:3128",
"no_proxy=localhost,127.0.0.1",
"NO_PROXY=localhost,127.0.0.1"
]
[runners.docker]
image = "ubuntu:24.04"
privileged = false
Lưu lại và khởi động lại dịch vụ:
sudo gitlab-runner restart
Bằng cách này, các job build image mới hay chạy lệnh npm install bên trong shell đều được đẩy qua Proxy một cách tự nhiên. Developer không cần phải sửa đổi mã nguồn trong file .gitlab-ci.yml.
Bước 4: Khai báo biến NO_PROXY và no_proxy
Bạn có để ý trong file cấu hình ở Bước 3, chúng ta phải khai báo cả hai định dạng chữ hoa (NO_PROXY) và chữ thường (no_proxy) không?
Thực tế, các ngôn ngữ và công cụ hệ thống có cách parse biến môi trường khác nhau. Ví dụ: Các tool viết bằng Go (như chính bản thân GitLab Runner hoặc Docker) thường ưu tiên biến chữ thường. Trong khi đó, các tool như Python (urllib) hay Ruby đôi khi lại fallback về chữ hoa.
Nếu bạn có các dịch vụ nội bộ (như Kubernetes Metadata, database staging, hoặc một private registry nội bộ), bạn phải liệt kê chúng vào danh sách loại trừ này để kết nối đi thẳng trong LAN thay vì đánh vòng ra Proxy rồi bị chặn lại.
3 best practices quan trọng khi vận hành Proxy cho CI/CD
Triển khai cấu hình Proxy Server bảo mật luồng CI/CD thành công mới chỉ là bước khởi đầu. Vận hành nó thế nào để hạn chế lỗ hổng mới lại là câu chuyện khác. Dưới đây là 3 kinh nghiệm thực tiễn dành cho quản trị viên hệ thống.
Không nên tắt strip_query_terms trên Squid
Một trong những sai lầm phổ biến của Sysadmin khi debug mạng là vô tình làm lộ mật khẩu nội bộ thông qua hệ thống log.
Nhiều API bên thứ 3 cũ kỹ vẫn chấp nhận (hoặc yêu cầu) truyền Secret Token trực tiếp vào URL dạng Query String. Ví dụ: https://api.partner.com/v1/sync?token=glpat-Secret123.
Mặc định, Squid Proxy có bật sẵn tính năng strip_query_terms on. Chức năng này rất hiệu quả: trước khi ghi URL nào vào file /var/log/squid/access.log, nó sẽ tự động cắt bỏ chuỗi tham số sau dấu hỏi chấm (?).
Nếu bạn tắt tính năng này đi (strip_query_terms off) để tiện debug, các URL thô chứa Token kia sẽ được in vào file log dưới dạng plain-text. Nếu hệ thống log này được đẩy về Elasticsearch chung của công ty, bạn có thể gây ra sự cố rò rỉ secret diện rộng. Hãy đảm bảo tính năng này luôn ở trạng thái on.

Tính năng strip_query_terms ngăn chặn việc ghi nhận các Token nhạy cảm vào file access.log của hệ thống Proxy.
Thiết lập Fail2ban và viết job Canary test kết nối
Dù chúng ta đã giấu cổng 3128 vào mạng WireGuard riêng tư, việc áp dụng chiến lược phòng thủ chiều sâu (Defense in Depth) vẫn mang lại giá trị thiết thực.
Thứ nhất, đối với hạ tầng Proxy, việc giám sát log để phát hiện các truy vấn bất thường là cần thiết. Nếu bạn phải public cổng Proxy ra ngoài (trong trường hợp không thể dùng VPN), hãy cài đặt Fail2ban. Chỉ cần tạo một jail rà soát file access.log tìm mã lỗi HTTP 407 (Proxy Authentication Required) hoặc 403 (Forbidden). Nếu một IP rà soát quá 5 lần trong 5 phút, hãy cấu hình iptables block IP đó.
Thứ hai, tường lửa trên VPS Runner có thể vô tình bị thay đổi luật trong quá trình bảo trì hệ thống. Để đảm bảo kiến trúc cô lập được duy trì, hãy cấu hình một Job Canary chạy ở stage đầu tiên của các pipeline.
Ví dụ file .gitlab-ci.yml:
test_egress_isolation:
stage: .pre
image: curlimages/curl:latest
script:
- echo "Kiểm tra Proxy Whitelist..."
# Test 1: Gọi domain lạ (Phải nhận được HTTP 403 từ Proxy)
- HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://evil-domain.com)
- if [ "$HTTP_CODE" != "403" ]; then echo "LỖI: Domain lạ không bị chặn!"; exit 1; fi
- echo "Kiểm tra Firewall Cô lập..."
# Test 2: Bỏ qua biến môi trường Proxy, cố đi thẳng ra mạng (Phải bị Timeout do UFW chặn)
- unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
- curl --max-time 3 https://google.com && (echo "CẢNH BÁO: Firewall đang hở, Outbound trực tiếp đang mở!"; exit 1) || echo "Firewall UFW đang chặn Outbound tốt. An toàn!"
Nếu Job này Passed, bạn mới có thể an tâm cho các job build và deploy tiếp theo.
Xu hướng tương lai: Transparent Proxy với Cilium eBPF
Hiện nay, việc cấu hình Proxy Server bảo mật luồng CI/CD bằng Squid và các biến môi trường HTTP_PROXY là phương pháp ổn định, dễ triển khai cho môi trường VPS độc lập. Tuy nhiên, nếu hạ tầng của bạn đã scale lên Kubernetes, việc cấu hình các pod tuân thủ biến môi trường đôi khi khá rườm rà.
Xu hướng DevSecOps hiện đại đang dịch chuyển dần sang Transparent Proxy (Proxy vô hình). Các công cụ như Cilium (dựa trên công nghệ lõi eBPF của Linux kernel) hoặc Service Mesh như Istio Egress Gateway cho phép bạn định tuyến, chặn bắt và kiểm duyệt lưu lượng Outbound bằng FQDN ở mức kernel. Ứng dụng bên trong container sẽ tự động kết nối ra Internet mà không nhận biết sự tồn tại của Proxy, trong khi SecOps vẫn nắm quyền kiểm soát các gói tin. Đây là một keyword đáng để team hạ tầng của bạn R&D trong các quý tới.
Câu hỏi thường gặp (FAQ)
1. Cấu hình Proxy Server trung gian có làm chậm tốc độ build CI/CD không?
Có thêm 1 hop mạng nên độ trễ nhích nhẹ (khoảng 1-5ms nếu 2 VPS đặt cùng Region/VPC). Tuy nhiên, mức này không đáng kể so với tổng thời gian pull image hay cài package, bù lại hệ thống tránh được nguy cơ rò rỉ dữ liệu.
2. Dùng Nginx làm Egress Proxy thay cho Squid được không?
Được nhưng không tối ưu. Nginx thiết kế chủ đạo để làm Reverse Proxy (xử lý luồng Inbound). Để làm Forward Proxy chặn luồng ra (Egress) theo danh sách tên miền (FQDN) và xử lý log bảo mật (strip_query_terms), Squid là công cụ chuyên dụng và dễ cấu hình ACL hơn.
3. IP của GitHub hay Docker Hub thay đổi liên tục, Proxy có bị chặn nhầm không?
Không. Squid hoạt động ở Layer 7 (Tầng ứng dụng), kiểm duyệt Whitelist dựa trên Tên miền (FQDN) thông qua SNI (Server Name Indication) của gói tin, không bị ảnh hưởng bởi việc thay đổi IP động phía sau CDN.
4. Khai báo biến NO_PROXY trong GitLab Runner có tác dụng gì?
Tạo ngoại lệ định tuyến (Bypass). Các domain/IP được liệt kê trong NO_PROXY (ví dụ: localhost, IP server database nội bộ) sẽ được Runner kết nối trực tiếp trong mạng LAN, không bị điều hướng qua Proxy Gateway.
5. Nếu đã dựng Egress Proxy thì có cần dùng Masked Variables hay HashiCorp Vault nữa không?
Cần thiết dùng cả hai. Secret Manager (Vault/Masked Variables) bảo vệ vòng đời token (Layer Ứng dụng), còn Egress Proxy chặn mã độc đẩy dữ liệu ra ngoài (Layer Mạng). Đây là 2 lớp phòng thủ độc lập trong kiến trúc Zero Trust.
Kết luận
Đối mặt với vấn nạn Tấn công chuỗi cung ứng (Supply Chain Attack) và nguy cơ thất thoát cấu hình, việc chỉ sử dụng Firewall Layer 3/4 là một cách tiếp cận thiếu an toàn. Thông qua việc cấu hình Proxy Server bảo mật luồng CI/CD chuẩn Zero Trust với Squid và WireGuard, chúng ta đã thiết lập thành công một chốt chặn Layer 7 vững chắc.
Bạn có thể quản lý đường ra của các đoạn script độc hại, giảm nguy cơ lộ mật khẩu qua mạng, đồng thời có trong tay một hệ thống Audit Log minh bạch cho các request đi ra từ Runner.
Tài liệu tham khảo