Mọi nhóm đều sớm mắc phải sai lầm giống nhau: mật khẩu database, API key, hay thông tin xác thực cloud bị commit vào repository Git. Đôi khi là developer mới chưa có kinh nghiệm. Đôi khi là bản vá nóng lúc 2 giờ sáng. Dù lý do gì, một khi secret đã vào lịch sử Git là coi như bị lộ — dù bạn xóa nó ở commit tiếp theo.
Câu trả lời hiển nhiên là “đừng commit secrets”. Nhưng điều đó không giải quyết được vấn đề thực sự: vậy thì secrets được lưu ở đâu? Với nhóm nhỏ chưa có hệ thống quản lý secrets riêng, câu trả lời thường là Google Docs dùng chung, tin nhắn Slack, hoặc file .env nằm trên máy ai đó. Thế này còn tệ hơn.
HashiCorp Vault là giải pháp chuẩn trong ngành. Nhưng vận hành nó không đơn giản — bạn cần server riêng, quản lý policy, gia hạn token, và tích hợp cho từng công cụ trong hệ thống. Với nhóm ba người quản lý vài microservice, đó là một đống hạ tầng cần chăm sóc liên tục.
Tôi đã từng ở đúng tình huống này. Mozilla SOPS kết hợp với Age là công cụ giúp chúng tôi thoát ra. Secrets được mã hóa lưu ngay trong repository Git — có version control, diff được, review được — mà không cần bất kỳ hạ tầng ngoài nào để duy trì.
Bắt Đầu Nhanh — Chạy Được Trong 5 Phút
Cài Đặt Age và SOPS
Age là công cụ mã hóa hiện đại được thiết kế để thay thế PGP cho hầu hết các mục đích thực tế. SOPS (Secrets OPerationS) là công cụ của Mozilla sử dụng Age (hoặc PGP/KMS) để mã hóa các file có cấu trúc như YAML, JSON, và .env — trong khi vẫn giữ tên trường ở dạng plaintext để diff vẫn đọc được.
Trên macOS:
brew install age
brew install sops
Trên Linux:
# Cài Age
curl -Lo age.tar.gz https://github.com/FiloSottile/age/releases/latest/download/age-v1.1.1-linux-amd64.tar.gz
tar -xf age.tar.gz
sudo mv age/age age/age-keygen /usr/local/bin/
# Cài SOPS
curl -Lo sops https://github.com/getsops/sops/releases/latest/download/sops-v3.9.0.linux.amd64
chmod +x sops
sudo mv sops /usr/local/bin/
Tạo Age Key
age-keygen -o ~/.config/sops/age/keys.txt
# Kết quả:
# Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
Public key có thể chia sẻ thoải mái với đồng nghiệp. Private key trong keys.txt giữ trên máy bạn — hãy sao lưu ở nơi an toàn (sẽ nói thêm ở phần sau).
Mã Hóa File Secret Đầu Tiên
Tạo file secrets.yaml:
database:
password: "supersecret123"
host: "prod-db.internal"
api_key: "sk-live-abc123xyz"
Mã hóa bằng SOPS:
sops --encrypt \
--age age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \
secrets.yaml > secrets.enc.yaml
File kết quả trông như thế này — giá trị được mã hóa, tên key vẫn đọc được:
database:
password: ENC[AES256_GCM,data:xyz123...,tag:abc==,type:str]
host: ENC[AES256_GCM,data:def456...,tag:ghi==,type:str]
api_key: ENC[AES256_GCM,data:jkl789...,tag:mno==,type:str]
sops:
age:
- recipient: age1ql3z7...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
Commit secrets.enc.yaml lên Git. Thêm secrets.yaml vào .gitignore. Giải mã khi cần:
sops --decrypt secrets.enc.yaml > secrets.yaml
# Hoặc giải mã trực tiếp và chỉnh sửa:
sops secrets.enc.yaml
Tìm Hiểu Sâu — SOPS và Age Hoạt Động Như Thế Nào
Hiểu Mô Hình Mã Hóa
SOPS sử dụng mã hóa lai (hybrid encryption). Khi bạn mã hóa một file, nó tạo ra một data encryption key (DEK) ngẫu nhiên, mã hóa nội dung file bằng AES-256-GCM với DEK đó, rồi mã hóa chính DEK bằng Age public key của bạn. Thêm nhiều recipient thì mỗi người nhận được một bản mã hóa riêng của DEK — không cần mã hóa lại toàn bộ dữ liệu khi nhóm có thêm người.
Tên trường được giữ ở plaintext theo thiết kế. Bạn có thể thấy những secrets nào đang tồn tại và review diff trong pull request mà không lộ giá trị thực. Trong các quy trình tuân thủ và review code, điều này hóa ra quan trọng hơn hầu hết các nhóm kỳ vọng khi lần đầu thiết lập.
Cấu Hình .sops.yaml Cho Dự Án
Phải truyền Age recipient vào mỗi lệnh sẽ rất mệt. Thay vào đó, tạo file .sops.yaml ở thư mục gốc của dự án:
creation_rules:
# Mã hóa tất cả file khớp với *secrets* hoặc *.enc.yaml
- path_regex: .*(secrets|enc)\.yaml$
age: >
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p,
age1another_team_member_public_key_here
# Quy tắc chặt hơn cho môi trường production
- path_regex: environments/prod/.*\.yaml$
age: age1prod_key_only_here
Bây giờ sops --encrypt secrets.yaml tự động chọn đúng key dựa trên đường dẫn file. Không cần flag, không cần copy-paste chuỗi key nữa.
Sử Dụng Nâng Cao
Thiết Lập Nhóm với Nhiều Recipient
Đây là lúc SOPS thể hiện giá trị thực sự. Mỗi developer tạo Age keypair riêng và chia sẻ public key. Thêm tất cả vào .sops.yaml:
creation_rules:
- path_regex: .*secrets.*
age: >
age1alice_public_key,
age1bob_public_key,
age1carol_public_key
Khi Alice mã hóa một file, Bob và Carol đều có thể giải mã bằng private key của mình. Khi có người rời nhóm, xóa key của họ khỏi .sops.yaml và chạy sops updatekeys secrets.enc.yaml. Bản mã hóa của họ trong lịch sử Git trở nên vô dụng khi không có private key tương ứng.
Để onboarding, tôi giữ một thư mục keys/ ở thư mục gốc của repo chứa public key của tất cả thành viên dưới dạng file text thuần. Người mới thêm key của họ, mở PR, và một maintainer mã hóa lại các secrets dùng chung với danh sách recipient đã cập nhật. Toàn bộ quá trình chỉ mất khoảng mười phút.
Tích Hợp với CI/CD Pipeline
Với GitHub Actions, lưu Age private key dưới dạng repository secret và inject vào trong workflow:
- name: Giải mã secrets
env:
SOPS_AGE_KEY: ${{ secrets.AGE_PRIVATE_KEY }}
run: |
sops --decrypt secrets.enc.yaml > secrets.yaml
source <(sops --decrypt --output-type dotenv app.enc.env)
Với Kubernetes: Flux CD có tích hợp SOPS để giải mã secrets lúc deploy bằng key lưu trong Kubernetes secret. Các manifest của bạn vẫn được mã hóa trong Git suốt đến tận cluster.
Xoay Vòng Key
Cập nhật .sops.yaml với key mới, rồi mã hóa lại:
# Cập nhật key cho một file cụ thể
sops updatekeys secrets.enc.yaml
# Mã hóa lại tất cả sau khi nhóm thay đổi nhân sự
find . -name "*.enc.yaml" -exec sops updatekeys {} \;
Mẹo Thực Tế
Luôn thêm file chưa mã hóa vào .gitignore. Cách đặt tên gọn nhất: file mã hóa là secrets.enc.yaml, file chưa mã hóa là secrets.yaml, rồi chỉ gitignore phiên bản chưa mã hóa.
SOPS cũng hỗ trợ định dạng dotenv. Không cần chuyển đổi sang YAML trước:
sops --encrypt --input-type dotenv --output-type dotenv app.env > app.enc.env
Sao lưu Age private key của bạn. SOPS không có cơ chế khôi phục. Mất private key khi nó là recipient duy nhất của một file, và những secrets đó biến mất vĩnh viễn. Password manager (1Password, Bitwarden) là nơi phù hợp để lưu bản sao lưu này.
Nói thêm về điều này — trước khi mã hóa bất kỳ secret nào, hãy tạo ra thứ gì đó đủ mạnh để xứng đáng được bảo vệ. Tôi thường mở sẵn ToolCraft’s Password Generator cho việc này: nó chạy hoàn toàn trên trình duyệt, không gửi dữ liệu đi đâu, nghĩa là tôi có thể tạo thông tin xác thực database hoặc API token có entropy cao mà không lo ngại về quyền riêng tư. Khi chuyển key giữa các máy, Hash Generator trên cùng trang web đó cũng xử lý SHA-256 checksum hoàn toàn phía client.
Kiểm tra recipient định kỳ. Khối sops: ở cuối mỗi file được mã hóa liệt kê tất cả recipient ở dạng plaintext. Xem lại định kỳ — đặc biệt với các secret dùng trong pipeline tự động — để xác nhận không có key lạ nào bị thêm vào.
Thêm pre-commit hook. Ngăn file secret chưa mã hóa bị stage ngay từ đầu:
# .git/hooks/pre-commit
#!/bin/bash
for file in $(git diff --cached --name-only | grep -E 'secrets\.yaml$|app\.env$'); do
echo "LỖI: File secret chưa mã hóa đang được stage: $file"
echo "Chạy: sops --encrypt $file > ${file%.yaml}.enc.yaml"
exit 1
done
SOPS hay Vault — chọn công cụ phù hợp. SOPS + Age phù hợp khi secrets của bạn xoay vòng hàng tháng thay vì theo từng request, nhóm dưới 20 người, và bạn không muốn vận hành thêm hạ tầng. HashiCorp Vault (hoặc bản fork mã nguồn mở OpenBao) phù hợp hơn khi bạn cần dynamic secrets, access policy theo từng service, hoặc thông tin xác thực có thời hạn tự động hết hạn. Hầu hết các nhóm bắt đầu với SOPS và chỉ chuyển sang Vault khi đã thực sự chạm đến giới hạn của nó.
Secret sprawl — thông tin xác thực rải rác khắp Slack, email, và dotfile local — là loại nợ kỹ thuật ẩn mình cho đến khi có sự cố nghiêm trọng. SOPS mang lại cho mọi thành viên trong nhóm một quy trình làm việc an toàn theo mặc định mà không cần server nào để chăm sóc. Nó không bao phủ mọi use case mà Vault xử lý được, nhưng đủ dùng cho 80% nhóm chưa thực sự cần đến độ phức tạp của Vault.

