Một ngày làm việc bình thường, hệ thống CI/CD của bạn bỗng nhiên ngừng hoạt động hoàn toàn. Đáng báo động hơn, qua rà soát log hệ thống, bạn phát hiện toàn bộ token AWS, SSH key và secret lưu trên server build đã bị đóng gói và gửi thẳng về một máy chủ lạ. Nguyên nhân không xuất phát từ việc cấu hình sai tường lửa hay rò rỉ mã nguồn từ team dev, mà đến từ một thư viện open-source bé nhỏ bạn vừa npm install đã bị kẻ gian chiếm quyền và chèn mã độc.
Trong môi trường phát triển phần mềm hiện tại, việc để server CI/CD (Runner) tự do gọi request ra mạng Internet public kéo dependency mang lại rủi ro rất lớn. Đặc biệt, từ sau những sự cố nghiêm trọng vào giữa năm 2026, giới DevOps nhận ra rằng không thể giao phó toàn bộ sinh mệnh dự án cho các public registry. Đó là lý do các kỹ sư hệ thống lựa chọn việc build Private Package Registry trên VPS. Giải pháp này tạo ra một pháo đài trung gian, giúp kiểm soát mã nguồn nội bộ và lọc sạch các mối đe dọa từ bên ngoài.
Làm thế nào để thiết lập một Registry hiện đại, tự động hóa SSL với Traefik và chống lại các kỹ thuật tấn công tinh vi? Hãy cùng đi sâu vào bài toán kiến trúc và từng bước thực hành ngay dưới đây.
Nỗi đau thực tế: Khi Public Registry trở thành tử huyệt của hệ thống
Việc phụ thuộc trực tiếp vào các kho lưu trữ công cộng (npmjs.com, PyPI, Maven Central) đang biến pipeline CI/CD của nhiều doanh nghiệp thành mục tiêu tấn công giá trị cao.
Ám ảnh Supply Chain Attack và bài học từ Mini Shai-Hulud
Tấn công chuỗi cung ứng (Software Supply Chain Attack) đã bước sang một giai đoạn cực kỳ tinh vi. Không còn là những đoạn script đào coin đơn giản, các chiến dịch hiện đại nhắm thẳng vào quy trình tự động hóa của doanh nghiệp.
Điển hình là chiến dịch Mini Shai-Hulud bùng phát. Kẻ tấn công không đi theo lối mòn cũ, mà sử dụng kỹ thuật đầu độc bộ nhớ đệm CI (CI cache poisoning). Thông qua một Pull Request độc hại, mã độc được tiêm vào bộ nhớ đệm. Khi workflow publish hợp lệ chạy, mã độc sẽ trích xuất token OIDC trực tiếp từ bộ nhớ của tiến trình runner (runner process memory). Từ đó, chúng tự động chèn mã độc và publish đè lên hàng trăm package khác, tạo ra một đợt lây nhiễm dây chuyền diện rộng mà các hệ thống quét tĩnh (SAST) gần như không thể phát hiện.
Rủi ro Dependency Confusion (nhầm lẫn phụ thuộc)
Đây là một kỹ thuật khai thác logic phân giải package mức độ nghiêm trọng. Giả sử doanh nghiệp của bạn có một thư viện dùng riêng mang tên @tencongty/auth-utils. Hacker phát hiện ra tên này qua metadata rò rỉ, sau đó lên npmjs.com tạo một package y hệt nhưng gán version rất cao (ví dụ: 9999.0.0).
Trình quản lý gói khi giải quyết cây phụ thuộc sẽ tự động so sánh và ưu tiên tải package có version cao hơn từ Public Registry. Thay vì kéo code an toàn từ server nội bộ, hệ thống CI lại tải thẳng mã độc về thực thi, dẫn đến việc toàn bộ môi trường production bị xâm nhập.
Quyết định build Private Package Registry trên VPS mang lại lợi thế gì?
Để cắt đứt chuỗi lây nhiễm, mô hình Private Package Registry kết hợp Proxy Cache được triển khai, mang lại 3 giá trị vận hành then chốt:
- Bảo vệ mã nguồn nội bộ (Proprietary Code): Developer có thể chia sẻ các thư viện dùng chung dễ dàng qua lệnh
npm installmà không sợ lộ source code ra ngoài Internet. Các rule phân quyền sẽ giúp chặn đứng tình trạng Dependency Confusion. - Tăng tốc độ CI/CD (Low Latency): Registry nội bộ đóng vai trò như một Proxy Cache. Khi package public được gọi lần đầu, nó sẽ tải về và lưu vào cache của VPS. Các lần build sau, hệ thống kéo file
.tgztrực tiếp từ mạng nội bộ (LAN/Intranet). Thời gian cài đặt dependency có thể giảm từ vài phút xuống chỉ còn vài chục giây. - Đảm bảo tính sẵn sàng (Outage Resilience): Nếu npmjs hay GitHub gặp sự cố mất kết nối mạng, tiến trình build của bạn vẫn hoạt động bình thường vì các thư viện cần thiết đã được cache an toàn trên ổ cứng VPS.
Lựa chọn công cụ: Tại sao lại là Verdaccio?
Trong mảng quản lý Artifact, Verdaccio và Sonatype Nexus là hai công cụ nổi bật, phục vụ cho các quy mô hệ thống khác nhau:
- Sonatype Nexus Repository: Giải pháp đa ngôn ngữ (hỗ trợ Maven, PyPI, Docker images). Tuy nhiên, Nexus chạy trên nền Java JVM, đòi hỏi VPS cấu hình cao (khuyến nghị từ 4 vCPU và 8GB RAM) để xử lý các tác vụ lập chỉ mục và cấp phát vùng nhớ Heap.
- Verdaccio: Rất phù hợp cho hệ sinh thái Node.js. Verdaccio vận hành vô cùng nhẹ nhàng, chỉ cần VPS cấu hình 1 vCPU, 1-2GB RAM là đủ để chạy mượt mà cùng Docker. Đây là lựa chọn lý tưởng cho các team dùng npm/yarn/pnpm cần một giải pháp triển khai nhanh gọn, tiết kiệm chi phí nhưng vẫn đảm bảo đầy đủ tính năng bảo mật.
Checklist chuẩn bị trước khi cấu hình
Để bắt tay vào triển khai build Private Package Registry trên VPS, bạn cần chuẩn bị các thành phần hạ tầng sau:
- Máy chủ VPS: Cấu hình 1-2 vCPU, 2GB RAM, 20-50GB SSD/NVMe (ưu tiên ổ cứng dung lượng khá để lưu trữ cache package lâu dài). Khuyên dùng hệ điều hành Ubuntu 24.04 LTS.
- Trước khi cài đặt, bạn nên tham khảo cách tối ưu VPS Ubuntu 24.04 để dọn dẹp các service thừa và định hình môi trường chạy Docker ổn định.
- Docker & Docker Compose: Đảm bảo hệ thống đã cài đặt sẵn phiên bản Engine và plugin Compose cập nhật.
- Domain (tên miền): Đã trỏ bản ghi A về IP của VPS (ví dụ:
npm.tencongty.com) để chuẩn bị cấp phát chứng chỉ bảo mật. - Firewall: Mở sẵn các cổng 80 (HTTP) và 443 (HTTPS) trên UFW cũng như Cloud Firewall của nhà cung cấp VPS.
4 bước triển khai Verdaccio + Traefik Auto-SSL trên Docker
Trong môi trường container hóa hiện đại, việc dùng Nginx cấu hình file text truyền thống hay Nginx Proxy Manager đã trở nên cồng kềnh với các hệ thống tự động hóa. Bài viết này sử dụng Traefik (phiên bản v3) làm Reverse Proxy. Traefik được thiết kế native cho Docker, có khả năng tự động đọc label để định tuyến và tự động cấp phát, gia hạn chứng chỉ Let’s Encrypt mà không cần can thiệp thủ công.
Bước 1: Khởi tạo thư mục và phân quyền (UID 10001)
Tạo cấu trúc thư mục trên VPS để ánh xạ dữ liệu (bind mount). Việc này giúp dữ liệu package và cấu hình không bị mất đi khi khởi động lại container:
mkdir -p /opt/registry/{traefik,verdaccio/conf,verdaccio/storage,verdaccio/plugins}
Di chuyển vào thư mục lưu trữ:
cd /opt/registry
Khắc phục lỗi Permission Denied:
Đây là lỗi mà nhiều người mới setup thường gặp. Container Verdaccio chạy dưới quyền user non-root (uid=10001). Bạn phải đổi chủ sở hữu thư mục trên máy Host, nếu không container sẽ crash ngay lập tức do không có quyền ghi ổ đĩa.
Thay đổi quyền sở hữu thư mục cho user non-root của Verdaccio:
sudo chown -R 10001:65533 ./verdaccio
Cấp quyền đọc/ghi/thực thi cần thiết:
sudo chmod -R 770 ./verdaccio
Đồng thời tạo file rỗng chứa chứng chỉ cho Traefik:
touch ./traefik/acme.json
Phân quyền bảo mật nghiêm ngặt cho file chứng chỉ:
chmod 600 ./traefik/acme.json
Bước 2: Thiết lập config.yaml chặn Dependency Confusion
Tạo file ./verdaccio/conf/config.yaml. Điểm mấu chốt ở cấu hình này là định nghĩa scope công ty (ví dụ @tencongty/*) và nghiêm cấm khai báo proxy: npmjs cho scope này. Hành động này yêu cầu Verdaccio chỉ tìm kiếm package nội bộ trên ổ đĩa, cắt đứt hoàn toàn luồng request ra public registry.
storage: /verdaccio/storage
auth:
htpasswd:
file: /verdaccio/storage/htpasswd
max_users: 100 # Cấu hình thành -1 sau khi tạo đủ tài khoản dev để khóa đăng ký mới
uplinks:
npmjs:
url: https://registry.npmjs.org/
packages:
# 1. Scope nội bộ: CHỈ đọc/ghi ổ cứng, KHÔNG được phép proxy ra ngoài
'@tencongty/*':
access: $authenticated
publish: $authenticated
unpublish: $authenticated
# 2. Các package công cộng (tự động fetch và cache từ uplink)
'**':
access: $authenticated
publish: $authenticated
unpublish: $authenticated
proxy: npmjs
middlewares:
audit:
enabled: true
logs:
type: stdout
format: pretty
level: http
# Giới hạn dung lượng upload để tránh lỗi 413 Request Entity Too Large
max_body_size: 100mb
Bước 3: Viết docker-compose.yml tích hợp Traefik
Tạo file docker-compose.yml tại thư mục /opt/registry. File này khởi chạy cả Traefik và Verdaccio, giao tiếp nội bộ qua network Docker an toàn.
Mẹo thực chiến (Tránh Rate Limit của Let’s Encrypt): Khi mới thiết lập và chạy thử nghiệm (test), bạn nên trỏ Traefik về server staging của Let’s Encrypt. Nếu khởi động lại container quá nhiều lần bị lỗi cấu hình, domain của bạn có thể bị khóa (Rate Limit) lên tới 7 ngày. Bạn có thể thêm cờ --certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory trong lúc test, sau khi hệ thống chạy ổn định thì xóa dòng đó đi.
version: '3.8'
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
command:
- "--api.insecure=false"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# Tự động chuyển hướng HTTP sang HTTPS
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
# Cấu hình Let's Encrypt sinh SSL tự động
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
- "--certificatesresolvers.myresolver.acme.email=admin@tencongty.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
# Thêm dòng dưới đây để dùng Staging server lúc test, xóa đi khi chạy production:
# - "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./traefik/acme.json:/letsencrypt/acme.json"
verdaccio:
image: verdaccio/verdaccio:5.24
container_name: verdaccio
restart: unless-stopped
environment:
- VERDACCIO_PORT=4873
volumes:
- './verdaccio/storage:/verdaccio/storage'
- './verdaccio/conf:/verdaccio/conf'
- './verdaccio/plugins:/verdaccio/plugins'
labels:
- "traefik.enable=true"
# Thay đổi domain dưới đây thành domain thực tế của bạn
- "traefik.http.routers.verdaccio.rule=Host(`npm.tencongty.com`)"
- "traefik.http.routers.verdaccio.entrypoints=websecure"
- "traefik.http.routers.verdaccio.tls.certresolver=myresolver"
- "traefik.http.services.verdaccio.loadbalancer.server.port=4873"
# Cho phép truyền file lớn (100MB) qua Proxy để publish mượt mà
- "traefik.http.middlewares.limit.buffering.maxRequestBodyBytes=104857600"
Khởi chạy hệ thống bằng lệnh docker compose up -d. Traefik sẽ tự động nhận diện domain, fetch chứng chỉ SSL và định tuyến mượt mà vào container Verdaccio.
Bước 4: Cấu hình .npmrc cho team
Truy cập https://npm.tencongty.com để kiểm tra giao diện Web UI. Sau đó, developer có thể đăng nhập qua terminal bằng lệnh:
npm login --registry https://npm.tencongty.com
Để dự án tự động hiểu và sử dụng Private Registry, hãy thiết lập file .npmrc ở thư mục root của project:
@tencongty:registry=https://npm.tencongty.com/
always-auth=true
//npm.tencongty.com/:_authToken=${NPM_TOKEN}
Những Best Practice bảo mật phòng thủ chuyên sâu 2026
Việc dựng xong Registry chỉ là bước khởi đầu. Để thực sự phòng thủ trước các luồng tấn công supply chain tinh vi, bạn cần kết hợp các chính sách vận hành khắt khe dưới đây.
Giới hạn Outbound Traffic cho CI/CD Pipeline
Một khi Runner bị nhiễm mã độc, bước đầu tiên chúng làm là gọi request ra ngoài Internet để tuồn secret key về máy chủ C2. Do đó, doanh nghiệp cần kiểm soát Outbound Traffic bằng cách kết hợp rule tường lửa chặn toàn bộ truy cập mạng, chỉ mở whitelist (allow-list) cho IP của Private Registry nội bộ. Việc này khóa chặt khả năng liên lạc của mã độc với bên ngoài.
Thiết lập thời gian ủ (Minimum Release Age)
Các bài học từ vụ lây nhiễm lớn cho thấy, các phiên bản mã độc thường bị cộng đồng phát hiện và gỡ bỏ khỏi public registry trong khoảng 3 đến 5 giờ.
Để né các đợt tấn công chớp nhoáng này, doanh nghiệp nên áp dụng cơ chế thời gian ủ (Cooldown). Bằng cách thiết lập cấu hình chặn tải các package mới publish, hệ thống CI của bạn sẽ tự động bỏ qua những phiên bản quá mới, chờ đến khi chúng được cộng đồng thẩm định an toàn mới đưa vào bộ nhớ đệm.
Lưu ý về đơn vị cấu hình: Tùy thuộc vào trình quản lý gói mà đơn vị thời gian được tính khác nhau. Ví dụ, trong npm có thể tính bằng ngày (vd: npm config set min-release-age 3), nhưng với các trình quản lý mới như pnpm, cấu hình thường tính bằng phút. Để thiết lập thời gian ủ 7 ngày trên pnpm, bạn cần dùng lệnh: minimum-release-age=10080 (7 ngày * 24h * 60 phút).
Tắt Postinstall Scripts (mặc định an toàn mới)
Đa số mã độc chuỗi cung ứng kích hoạt thông qua các script chạy ngầm như preinstall hoặc postinstall. Khi lệnh install chạy xong, các script này tự động thực thi bash script, gọi request ra ngoài và tải payload độc hại về.
Hiện nay, các trình quản lý gói hiện đại như pnpm (từ phiên bản 10 trở lên) đã tự động tắt mặc định tính năng chạy script này để bảo vệ người dùng. Nếu team của bạn vẫn đang sử dụng npm, hãy chủ động thiết lập khóa rào cản này trên toàn bộ CI/CD runner bằng lệnh:
npm config set ignore-scripts true
Áp dụng nguyên tắc Immutable Artifact (tài sản bất biến)
Một khi package @tencongty/core@1.1.0 đã được publish, bạn cần thiết lập rule để ngăn chặn việc sửa đổi hay ghi đè nội dung lên chính version 1.1.0 đó.
Việc ghi đè sẽ làm thay đổi mã băm (integrity hash) của tệp tin. Các công cụ CI/CD khi đọc tệp package-lock.json sẽ báo lỗi Integrity Mismatch và làm lỗi tiến trình build. Nguy hiểm hơn, nếu hệ thống cho phép ghi đè, hacker có thể âm thầm chèn mã độc vào version đang chạy ổn định để qua mặt các công cụ quét mã nguồn tĩnh (SAST). Nguyên tắc chuẩn là: Mọi thay đổi code dù nhỏ tới đâu đều phải được đẩy lên một version mới (Bump version).
Xử lý lỗ hổng Docker tự động bypass UFW
Trong môi trường hệ điều hành Ubuntu, khi bạn publish một port container ra ngoài host, Docker Engine sẽ tự động chèn rule iptables trực tiếp vào PREROUTING chain, bypass hoàn toàn UFW. Dù bạn có gõ lệnh ufw deny, cổng dịch vụ vẫn phơi bày ra Internet.
Vì chúng ta đã dùng Traefik định tuyến qua network nội bộ (không publish port 4873 của Verdaccio ra host), hệ thống đã an toàn hơn rất nhiều. Tuy nhiên, để tuân thủ nguyên tắc bảo mật mạng (Defense in Depth), bạn nên tham khảo các phương pháp bảo mật VPS Linux với UFW, đồng thời kết hợp công cụ ufw-docker (của chaifeng) để ép Docker Engine phải tuân thủ nghiêm ngặt các quy tắc tường lửa của UFW, đảm bảo chỉ có Traffic đi qua cổng 80 và 443 được cấp phép.
Loại bỏ long-lived token bằng OIDC (Trusted Publishing)
Thay vì sử dụng một NPM Token tĩnh (long-lived) để lưu trong mục Secrets của GitHub Actions hoặc GitLab CI (rất dễ bị extract nếu server build nhiễm mã độc), hãy cấu hình cơ chế OIDC (OpenID Connect).
Cơ chế này hoạt động theo cách: CI runner sẽ yêu cầu cấp một token định danh ngắn hạn (short-lived) ngay tại thời điểm build. Hệ thống registry xác thực token này với bên cấp phát (như GitHub/GitLab) và cấp quyền publish package trong một khoảng thời gian ngắn. Kỹ thuật này ngăn chặn triệt để tình trạng hacker lùng sục logs hoặc quét bộ nhớ đệm để đánh cắp token xuất bản dài hạn, bảo vệ danh tính doanh nghiệp an toàn.
Chủ động giám sát tài nguyên lưu trữ
Private Registry khi chạy lâu dài sẽ tự động tải về hàng GB dữ liệu package từ Internet. Tình trạng đầy ổ cứng (Full Disk) sẽ khiến hệ thống không thể ghi file log hoặc publish package mới. Sysadmin nên chủ động giám sát VPS bằng Prometheus Grafana để thiết lập cảnh báo (Alerting) qua Telegram/Slack khi dung lượng lưu trữ vượt ngưỡng 85%, từ đó lên phương án dọn dẹp cache kịp thời.
Câu hỏi thường gặp (FAQ)
1. Tại sao nên dùng VPS riêng thay vì xài luôn npm public hay GitHub Packages?
Để làm chủ hoàn toàn dữ liệu. VPS Private Registry đóng vai trò như một bộ đệm (cache) giúp CI/CD build cực nhanh trong mạng nội bộ và chặn đứng nguy cơ tải nhầm mã độc từ bên ngoài (Dependency Confusion).
2. Verdaccio cần VPS cấu hình bao nhiêu là đủ?
Bạn chỉ cần cấu hình 1 vCPU, 1-2GB RAM và khoảng 20-50GB ổ cứng (ưu tiên SSD/NVMe) chạy Ubuntu 24.04 là có thể vận hành mượt mà cho một team dev quy mô vừa.
3. Làm sao để sửa lỗi 413 Request Entity Too Large khi publish package?
Lỗi này do giới hạn dung lượng tải lên. Bạn cần cấu hình tăng dung lượng đồng bộ ở 2 nơi: Thêm rule maxRequestBodyBytes (với Traefik) / client_max_body_size (với Nginx) và khai báo max_body_size: 100mb vào file config.yaml của Verdaccio.
4. Nếu VPS cài Registry ngừng hoạt động, hệ thống CI/CD có bị ảnh hưởng không?
Có. Vì CI/CD được cấu hình trỏ nguồn về Private Registry, nếu máy chủ ngắt kết nối, tiến trình build sẽ báo lỗi. Do đó, sysadmin cần backup thư mục storage định kỳ và thiết lập giám sát uptime.
5. Thời gian ủ (Release Age Cooldown) hoạt động như thế nào?
Là độ trễ cố ý trước khi hệ thống được phép tải một package mới. Cơ chế này giúp CI/CD tự động bỏ qua các bản cập nhật chứa mã độc vừa bị hacker đẩy lên mạng, chờ đến khi cộng đồng rà soát an toàn mới tiến hành lưu cache.
Kết luận
Quyết định build Private Package Registry trên VPS là một bước tiến mang tính bước ngoặt về mặt kiến trúc, giúp đội ngũ DevOps giành lại quyền kiểm soát toàn bộ luồng dependency. Bằng việc kết hợp sức mạnh của Verdaccio, Traefik tự động SSL và áp dụng các cơ chế phòng thủ hiện đại như thời gian ủ (Release Age) hay OIDC, bạn đã xây dựng thành công một chốt chặn an ninh vững chắc.
Hệ thống CI/CD giờ đây không còn phải đối mặt trực tiếp với những rủi ro khó lường từ Internet, mã nguồn nội bộ được bảo vệ kín kẽ, và tốc độ deploy được tối ưu đáng kể. Đã đến lúc rà soát lại pipeline của team, thiết lập lại luồng cấp phát token và vận hành luồng code của bạn một cách chuyên nghiệp, an toàn.
