Product Management
Đăng nhập
ESC

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

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

Bài 16 — Secret Management trong CI/CD

Đã đọc 0/13 mục · còn khoảng 12 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 tưởng tượng bạn vừa hoàn thành một pipeline CI/CD tuyệt đẹp: code push lên là tự động build, test, rồi deploy thẳng lên production. Mọi thứ chạy mượt như bơ. Nhưng có một câu hỏi bạn buộc phải trả lời trước khi pipeline chạm được vào server thật: làm sao để pipeline biết mật khẩu database, biết API key của cổng thanh toán, biết SSH key để đăng nhập server — mà không để lộ những thứ đó ra ngoài?

Đây chính là bài toán quản lý secret (secret management) trong CI/CD. Nghe qua thì có vẻ là chuyện nhỏ, nhưng thực tế đây là một trong những nguồn rò rỉ bảo mật nghiêm trọng nhất trong ngành phần mềm. Theo báo cáo của GitGuardian, mỗi năm có hàng triệu secret bị commit nhầm lên các kho công khai trên GitHub. Một khóa AWS bị lộ có thể khiến kẻ tấn công quay hàng nghìn máy đào coin trong đêm, để lại hóa đơn hàng chục nghìn đô la.

Ở Việt Nam, mình đã chứng kiến không ít đội nhóm startup vô tình đẩy file .env chứa credential production lên GitHub public, hoặc in nguyên chuỗi token database ra log của Jenkins để "debug cho nhanh". Hậu quả có thể là mất dữ liệu khách hàng, bị tống tiền, hoặc đơn giản là hóa đơn cloud phi mã.

Bài học hôm nay sẽ giúp bạn hiểu bản chất của việc quản lý secret trong pipeline: những anti-pattern (mẫu sai lầm) cần tránh bằng mọi giá, phân tầng (tier) các giải pháp lưu trữ secret từ đơn giản đến chuyên nghiệp, và quy trình cụ thể để đưa secret vào pipeline một cách an toàn. Đây là kiến thức bắt buộc phải nắm nếu bạn muốn làm DevOps một cách nghiêm túc.

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

Secret là gì và tại sao nó khác với config thường?

Trong một ứng dụng, chúng ta có hai loại cấu hình. Config thường là những thứ không nhạy cảm: tên miền, số port, mức log, số lượng worker. Còn secret là những thông tin mà nếu lộ ra sẽ gây thiệt hại: mật khẩu database, API key, token truy cập, private key, chứng chỉ TLS, chuỗi kết nối chứa credential.

Điểm mấu chốt: secret không bao giờ được nằm trong source code. Source code sẽ được clone về hàng chục máy, lưu trong lịch sử git mãi mãi, và có thể bị chia sẻ ra ngoài. Còn secret phải được lưu tách biệt, mã hóa, và chỉ được "tiêm" (inject) vào ứng dụng đúng lúc chạy, đúng môi trường.

Bốn anti-pattern chết người

Trước khi nói về cách làm đúng, hãy khắc cốt ghi tâm bốn kiểu làm sai mà bạn tuyệt đối phải tránh:

1. Commit file .env chứa credential lên git. Đây là lỗi phổ biến số một. File .env rất tiện để chạy local, nhưng chỉ cần một lần git add . thiếu cẩn thận là toàn bộ mật khẩu đi thẳng vào lịch sử repo. Và nhớ rằng: xóa file rồi commit lại không làm sạch được — secret vẫn còn nguyên trong git history. Bất kỳ ai clone repo đều đọc được bằng git log.

2. Hardcode secret trong Jenkinsfile hoặc file pipeline. Nhiều bạn viết thẳng password = "MyDbPass123" vào Jenkinsfile, .gitlab-ci.yml hoặc file YAML của GitHub Actions. File pipeline cũng là code, cũng nằm trong repo, nên đây thực chất là biến thể của lỗi số một.

3. In secret ra log. Câu lệnh vô hại như echo $DATABASE_URL hay curl -v in ra header Authorization sẽ khiến secret hiện nguyên xi trong log build. Log CI thường được lưu nhiều tháng, có khi được đẩy sang hệ thống log tập trung mà cả công ty đọc được. Kẻ tấn công chỉ cần quyền xem log là lấy được secret.

4. Chia sẻ secret qua Slack, Zalo, email. "Anh ơi gửi em cái API key production với" — rồi dán thẳng vào chat. Tin nhắn chat không được mã hóa đúng cách, được index để search, và tồn tại vĩnh viễn. Một nhân viên nghỉ việc vẫn còn lịch sử chat chứa credential là chuyện thường gặp.

Phân tầng (tier) các giải pháp lưu trữ secret

Không phải đội nhóm nào cũng cần một hệ thống quản lý secret cấp doanh nghiệp ngay từ đầu. Hãy hình dung một cầu thang bốn bậc, từ đơn giản đến chuyên nghiệp:

Tier 1 — Biến môi trường thủ công của CI: Đây là điểm khởi đầu. Hầu hết nền tảng CI/CD đều có nơi lưu secret ngay trong giao diện: GitHub Actions có "Repository secrets" và "Environment secrets", GitLab có "CI/CD Variables" (đánh dấu Masked và Protected), Jenkins có "Credentials". Bạn nhập secret một lần qua giao diện, nền tảng mã hóa lúc lưu (encryption at rest), rồi tiêm vào pipeline dưới dạng biến môi trường lúc chạy. Đơn giản, đủ dùng cho phần lớn dự án vừa và nhỏ.

Tier 2 — Secret manager của cloud provider: Khi dự án lớn hơn, bạn muốn secret sống ngoài CI để nhiều hệ thống dùng chung và xoay vòng (rotate) tự động. Lúc này dùng dịch vụ như AWS Secrets Manager, Google Cloud Secret Manager, hoặc Azure Key Vault. Pipeline không lưu secret nữa mà chỉ giữ quyền (qua IAM role) để gọi lấy secret lúc runtime.

Tier 3 — Secret manager chuyên dụng: HashiCorp Vault là đại diện tiêu biểu. Vault cho phép cấp secret động (dynamic secret) — ví dụ mỗi lần pipeline chạy, Vault sinh ra một cặp username/password database mới, chỉ sống 15 phút rồi tự hủy. Đây là chuẩn mực cho các tổ chức lớn cần kiểm soát chặt.

Tier 4 — Không có secret tĩnh (OIDC / workload identity): Bậc cao nhất và cũng là xu hướng hiện đại. Thay vì lưu một secret dài hạn, pipeline dùng cơ chế OIDC (OpenID Connect) để chứng minh danh tính với cloud provider và nhận về một token ngắn hạn ngay tại thời điểm chạy. GitHub Actions kết nối AWS/GCP qua OIDC là ví dụ điển hình: không còn AWS access key nào được lưu ở bất cứ đâu. Không có secret tĩnh thì không có gì để rò rỉ.

Nguyên tắc chung: bắt đầu ở tier phù hợp với quy mô, và luôn hướng tới việc giảm số secret tĩnh phải quản lý.

Tình huống thực tế

Ví dụ 1 — Startup fintech Việt Nam commit nhầm khóa cổng thanh toán

Một startup ví điện tử ở TP.HCM, đội kỹ thuật khoảng 8 người, đang chạy nước rút cho vòng gọi vốn. Một bạn dev mới vào, khi setup môi trường local đã tạo file .env chứa secret key của cổng thanh toán VNPAY và merchant credential. Trong lúc vội, bạn ấy chạy git add -A && git commit -m "wip" rồi push lên một repo GitHub mà đội tưởng là private, nhưng thực ra đã bị set public từ hồi tạo demo.

Chỉ sau 4 tiếng, một con bot quét GitHub đã tìm thấy khóa đó. May mắn là cổng thanh toán này có cơ chế whitelist IP nên chưa bị lạm dụng giao dịch, nhưng đội buộc phải liên hệ nhà cung cấp để thu hồi và cấp lại toàn bộ credential trong đêm, tạm dừng dịch vụ 3 tiếng.

Diễn giải: Vấn đề không chỉ là file .env bị lộ, mà là secret đã nằm trong git history — dù có xóa file cũng vô ích. Đội phải dùng công cụ như git filter-repo để viết lại lịch sử, và quan trọng nhất là xoay vòng toàn bộ secret vì phải coi như chúng đã bị lộ hoàn toàn.

Bài học: Thêm .env vào .gitignore ngay từ commit đầu tiên của mọi dự án. Cài một công cụ quét secret như gitleaks chạy ở pre-commit và trong pipeline CI để chặn secret trước khi nó kịp lên remote. Và luôn giả định repo có thể bị public bất cứ lúc nào.

Ví dụ 2 — Công ty gia công phần mềm in token ra log Jenkins

Một công ty outsourcing tại Đà Nẵng làm dự án cho khách hàng Nhật, dùng Jenkins làm CI server. Để debug một lỗi kết nối API, một kỹ sư thêm dòng sh 'echo "Token: $API_TOKEN"' vào Jenkinsfile cho "nhìn cho rõ". Fix xong lỗi, bạn ấy quên xóa dòng đó đi.

Ba tháng sau, khi audit bảo mật theo yêu cầu của khách hàng, đội security phát hiện token production của API khách hàng nằm phơi bày trong hàng trăm bản log build cũ trên Jenkins — mà server Jenkins này lại cho phép toàn bộ nhân viên có tài khoản đều xem được job. Khách hàng Nhật đánh giá đây là sự cố nghiêm trọng, yêu cầu thu hồi token và làm lại quy trình.

Diễn giải: Log là nơi secret rò rỉ âm thầm nhất, vì không ai để ý. Jenkins có tính năng mask credential (dùng plugin Credentials Binding, secret sẽ hiện thành ** trong log), nhưng chỉ hoạt động khi bạn dùng đúng cách qua withCredentials, không hoạt động nếu bạn tự echo biến ra.

Bài học: Không bao giờ in secret ra log, kể cả khi debug. Dùng cơ chế masking của nền tảng. Thiết lập quyền xem log theo nguyên tắc tối thiểu. Và đưa việc "quét log tìm secret" vào checklist audit định kỳ.

Ví dụ 3 — Sàn thương mại điện tử chuyển sang OIDC, xóa sạch AWS key

Một sàn thương mại điện tử quy mô khu vực Đông Nam Á có hơn 40 microservice, mỗi service có pipeline riêng deploy lên AWS. Ban đầu, đội lưu một cặp AWS access key trong GitHub secret của từng repo — tổng cộng hơn 40 cặp key dài hạn nằm rải rác. Việc xoay vòng chúng là ác mộng: mỗi lần rotate phải cập nhật hàng chục nơi, và không ai dám chắc còn key cũ nào bị bỏ quên hay không.

Đội DevOps quyết định chuyển toàn bộ sang cơ chế OIDC giữa GitHub Actions và AWS IAM. Sau khi cấu hình identity provider, mỗi pipeline khi chạy sẽ tự động nhận một token tạm thời (sống 1 giờ) để truy cập AWS, gắn với đúng role có quyền tối thiểu cho service đó. Kết quả: họ xóa được toàn bộ hơn 40 cặp access key tĩnh. Không còn secret AWS nào để rò rỉ, không còn phải rotate thủ công.

Diễn giải: Đây là ví dụ cho tier cao nhất. Secret tĩnh tốt nhất là secret không tồn tại. OIDC chuyển mô hình từ "lưu và bảo vệ secret" sang "chứng minh danh tính và nhận quyền tạm thời".

Bài học: Khi hạ tầng đủ lớn, hãy đầu tư vào cơ chế identity-based (OIDC, workload identity) thay vì tiếp tục nhân bản secret tĩnh khắp nơi. Ít secret tĩnh hơn đồng nghĩa với ít bề mặt tấn công hơn.

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

Dưới đây là quy trình thực tế để đưa secret vào một pipeline GitHub Actions một cách an toàn — nguyên tắc tương tự áp dụng được cho GitLab và Jenkins.

Bước 1 — Kiểm kê secret. Liệt kê mọi secret pipeline cần: DATABASE_URL, STRIPE_SECRET_KEY, SSH_DEPLOY_KEY... Ghi rõ mỗi secret dùng ở môi trường nào (staging, production) và service nào cần. Việc kiểm kê giúp bạn không bỏ sót và không cấp quyền thừa.

Bước 2 — Chọn nơi lưu theo tier. Với dự án vừa: lưu ở GitHub Environment secrets, tách riêng environment staging và production. Với dự án lớn dùng AWS: đưa secret vào AWS Secrets Manager, cấu hình OIDC để pipeline lấy quyền mà không cần key tĩnh.

Bước 3 — Chặn secret rò rỉ từ nguồn. Thêm .env, .pem, .key vào .gitignore. Cài gitleaks hoặc trufflehog chạy như một job trong pipeline để fail build nếu phát hiện secret trong code.

Bước 4 — Tiêm secret đúng cách trong workflow. Trong GitHub Actions, tham chiếu secret qua cú pháp ${{ secrets.STRIPE_SECRET_KEY }} và gán vào biến môi trường của bước cần dùng, ví dụ:

jobs:
  deploy:
    environment: production
    steps:
      - name: Deploy
        env:
          STRIPE_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
        run: ./deploy.sh

Nền tảng sẽ tự động che (mask) giá trị này nếu nó vô tình xuất hiện trong log.

Bước 5 — Giới hạn phạm vi bằng environment và protection rule. Đặt secret production trong environment production và bật required reviewer: chỉ khi có người phê duyệt thì job chạm production mới được chạy. Nhờ đó một pull request từ nhánh lạ không thể lấy được secret production.

Bước 6 — Thiết lập xoay vòng và giám sát. Lên lịch rotate secret định kỳ (ví dụ 90 ngày). Bật cảnh báo khi secret được truy cập bất thường. Ghi lại ai đã thêm/sửa secret nào.

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

Tưởng xóa file là hết. Xóa .env rồi commit không xóa được secret khỏi git history. Luôn coi secret đã lộ là phải rotate ngay, song song với việc dọn history.

Dùng chung một secret cho mọi môi trường. Nếu staging và production dùng cùng một database password, thì lộ staging là mất luôn production. Hãy tách secret theo từng môi trường.

Cấp quyền quá rộng. Cho pipeline một AWS key có quyền admin "cho tiện" là thảm họa nếu key bị lộ. Áp dụng nguyên tắc least privilege: pipeline deploy chỉ nên có đúng quyền deploy, không hơn.

Quên fork/pull request có thể lộ secret. Trên nhiều nền tảng, pull request từ fork bên ngoài mặc định không được cấp secret — đừng vô hiệu hóa cơ chế bảo vệ này. Kẻ xấu có thể gửi PR chứa code echo $SECRET để đánh cắp.

Mẹo — quét secret ở hai lớp. Chạy gitleaks cả ở pre-commit hook (chặn tại máy dev) lẫn trong CI (chặn tại server). Một lớp có thể bị bỏ qua, hai lớp thì an toàn hơn nhiều.

Mẹo — dùng masking và không log chi tiết. Tránh set -x trong shell script deploy vì nó in mọi câu lệnh kèm giá trị biến. Với curl, dùng file cấu hình thay vì truyền token trên dòng lệnh để không lộ qua danh sách tiến trình (ps).

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

  • Kiểm kê và phân loại. Lấy một dự án bạn đang có (hoặc dự án mẫu). Liệt kê tất cả secret nó cần, và với mỗi secret hãy ghi: dùng ở môi trường nào, độ nhạy cảm ra sao, hiện đang được lưu ở đâu. Đánh dấu những chỗ đang vi phạm anti-pattern.
  • Chặn rò rỉ. Cài gitleaks vào một repo test. Cố tình thêm một dòng giả lập secret (ví dụ AWS_KEY=AKIAIOSFODNN7EXAMPLE) rồi commit thử, xác nhận rằng công cụ phát hiện và chặn được.
  • Tiêm secret an toàn. Tạo một workflow GitHub Actions (hoặc GitLab CI) đọc một secret từ giao diện nền tảng, dùng nó trong một bước, và kiểm chứng rằng giá trị bị che thành * trong log dù bạn cố echo nó ra.
  • Suy nghĩ nâng cao. Với một dự án deploy lên AWS, hãy phác thảo cách chuyển từ việc lưu AWS access key tĩnh sang dùng OIDC. Liệt kê các bước và giải thích vì sao cách này an toàn hơn.

Tóm tắt

Quản lý secret là ranh giới giữa một pipeline "chạy được" và một pipeline "an toàn để đưa vào production". Bạn cần khắc ghi bốn anti-pattern phải tránh: commit .env chứa credential, hardcode secret trong file pipeline, in secret ra log, và chia sẻ secret qua chat/email. Về giải pháp, hãy hình dung bốn tầng: từ biến môi trường của CI (tier 1), lên secret manager của cloud (tier 2), tới công cụ chuyên dụng như Vault (tier 3), và cao nhất là mô hình không secret tĩnh với OIDC (tier 4). Nguyên tắc xuyên suốt là: tách secret khỏi code, cấp quyền tối thiểu, tách theo môi trường, che secret trong log, và xoay vòng định kỳ. Ba tình huống thực tế đã cho thấy cái giá của việc làm sai — và cả phần thưởng khi làm đúng. Secret tốt nhất là secret không cần tồn tại; còn secret buộc phải có thì phải được bảo vệ như tài sản quý giá nhất của hệ thống.

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