File .env chứa AWS access key bị commit lên GitHub public repo. Bot quét phát hiện trong 30 giây, spin lên hàng trăm EC2 đào crypto. Key revoke được nhưng bill thì không. GitHub báo cáo phát hiện hơn 100 triệu secrets bị leak trên platform chỉ riêng năm 2024.
Quản lý secrets nghe đơn giản nhưng thực tế phức tạp: secrets phải đến đúng service, đúng môi trường, được rotate định kỳ, được audit, và không tạo ma sát quá lớn cho developer. Bài này đi qua các tầng quản lý từ .env đến HashiCorp Vault, kèm pattern rotation không downtime.
Secrets là gì — và vì sao chúng đặc biệt
Config thông thường — port number, log level, feature flag — nếu lộ ra ngoài thì phiền nhưng không gây thiệt hại trực tiếp. Secrets thì khác: lộ một database password nghĩa là kẻ tấn công đọc được toàn bộ data. Lộ một API key của payment gateway nghĩa là ai đó có thể tạo giao dịch thay bạn. Lộ một signing key nghĩa là JWT có thể bị forge.
Secrets bao gồm nhiều loại: API key và token dùng để xác thực với service bên ngoài (Stripe, SendGrid, AWS), database credentials (username/password hoặc connection string), TLS certificate và private key, encryption key dùng để mã hoá data at rest, SSH key cho deploy hoặc server access, và OAuth client secret. Mỗi loại có lifecycle và yêu cầu rotation khác nhau, nhưng tất cả đều chia sẻ một đặc tính: giá trị của chúng phải được bảo vệ ở mọi thời điểm — at rest, in transit, và in use.
Điểm khác biệt cốt lõi giữa config và secret: config có thể public mà không ảnh hưởng bảo mật, secret thì không. Quy tắc đơn giản này quyết định cách lưu trữ và phân phối chúng phải khác nhau hoàn toàn.
Thời đại .env — tiện nhưng nguy hiểm
File .env phổ biến nhờ thư viện dotenv (Ruby, Node.js, Python, Go đều có). Ý tưởng đơn giản: liệt kê biến môi trường trong file text, load vào process khi khởi động.
# .env
DATABASE_URL=postgres://admin:s3cretP@[email protected]:5432/myapp
STRIPE_SECRET_KEY=sk_live_abc123xyz
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
JWT_SECRET=super-secret-jwt-signing-key
Trong môi trường development cá nhân, .env hoạt động tốt: mỗi developer có file riêng, chứa credentials cho database local hoặc sandbox API. Vấn đề bắt đầu khi .env lan ra ngoài phạm vi đó.
Sai lầm phổ biến nhất là commit .env vào git. Dù repo là private, credentials trong git history tồn tại vĩnh viễn — xoá file rồi commit lại không xoá được history. Cần git filter-repo hoặc BFG Repo Cleaner để thật sự loại bỏ, và ngay cả khi đã dọn thì bất kỳ ai đã clone repo trước đó vẫn có bản copy. Sai lầm thứ hai là chia sẻ .env qua Slack, email, hoặc Confluence. Kênh chat lưu message history vĩnh viễn — secret gửi qua Slack năm ngoái vẫn nằm đó, searchable bởi bất kỳ ai trong workspace.
Pattern .env.example giúp giảm thiểu rủi ro: commit một file mẫu liệt kê tất cả biến cần thiết nhưng không chứa giá trị thật.
# .env.example — commit file này vào git
DATABASE_URL=postgres://user:password@localhost:5432/myapp_dev
STRIPE_SECRET_KEY=sk_test_...
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
JWT_SECRET=change-me-in-local-env
Developer mới clone repo, copy .env.example thành .env, điền giá trị thật. File .env nằm trong .gitignore, không bao giờ được commit. Đây là mức tối thiểu mà mọi project nên có.
12-factor và environment variables
12-factor app đề xuất lưu config trong environment variables — tách config khỏi code, cho phép cùng artifact chạy trên nhiều môi trường chỉ bằng cách đổi biến. Ý tưởng đúng về nguyên tắc, nhưng environment variables có nhiều rủi ro mà ít người để ý khi dùng cho secrets.
Thứ nhất, env vars visible trong process listing. Trên Linux, cat /proc/<pid>/environ cho bất kỳ ai có quyền đọc process đó xem toàn bộ environment — bao gồm cả secrets. Trên container runtime, docker inspect show env vars dạng plaintext. Đây không phải lỗ hổng lý thuyết — đây là attack vector thực tế khi attacker có shell access hạn chế.
Thứ hai, env vars dễ bị log vô tình. Framework crash reporter, error tracking tool (Sentry, Bugsnag), hoặc đơn giản là console.log(process.env) trong lúc debug — tất cả đều có thể dump secrets vào log system. Một dòng debug code quên xoá đã đủ để database password xuất hiện trong Elastic.
Thứ ba, child process kế thừa toàn bộ environment của parent. Khi app spawn subprocess — chạy script, gọi CLI tool — subprocess đó nhận tất cả env vars, kể cả những secrets nó không cần. Đây vi phạm nguyên tắc least privilege mà không ai nhận ra cho đến khi audit.
Env vars vẫn là cơ chế phổ biến nhất để truyền config vào container và serverless function. Nhưng với secrets nhạy cảm cao, cần thêm các lớp bảo vệ phía trên — secret manager inject secrets vào env vars lúc runtime thay vì lưu trữ secrets trực tiếp trong env config.
Phát hiện leak sớm — git secret scanning
Phòng thủ tốt nhất là không để secrets vào git ngay từ đầu. Có ba lớp quét có thể áp dụng chồng lên nhau.
Pre-commit hook
Tool như git-secrets (AWS), detect-secrets (Yelp), hoặc gitleaks chạy trước mỗi commit, scan staged files tìm pattern giống secret (API key format, high-entropy string, known prefixes như sk_live_, AKIA). Nếu phát hiện, commit bị chặn ngay trên máy developer.
# Cài gitleaks qua pre-commit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
Pre-commit hook chạy local nên developer có thể bypass bằng --no-verify. Đây là lớp phòng thủ đầu tiên, không phải lớp cuối cùng.
CI pipeline scanning
Chạy gitleaks hoặc trufflehog trong CI pipeline — scan toàn bộ diff hoặc toàn bộ history. Nếu phát hiện secret, pipeline fail, PR không merge được. Lớp này không bypass được vì chạy trên server.
Platform-level scanning
GitHub Secret Scanning quét mọi push, phát hiện token của hơn 200 provider (AWS, Stripe, Google, Slack…), tự động notify và trong nhiều trường hợp tự revoke token đó thông qua partner program. GitLab cũng có tính năng tương tự. Đây là lớp cuối cùng — nếu secret lọt qua pre-commit và CI, platform vẫn bắt được.
Ba lớp chồng lên nhau: local hook chặn sớm nhất (developer biết ngay), CI chặn chắc chắn (không bypass được), platform chặn cuối cùng (kể cả force push). Không lớp nào hoàn hảo một mình, nhưng kết hợp thì xác suất leak giảm đáng kể.
Các tầng quản lý secrets
Không phải mọi project đều cần HashiCorp Vault. Chọn tầng phù hợp với quy mô, compliance requirement, và operational capacity của team.
Tầng 1: .env + .gitignore
Phù hợp cho development local và side project. File .env nằm trong .gitignore, mỗi developer tự quản lý secrets của mình. Không có encryption, không có audit, không có rotation tự động. Đủ dùng khi blast radius nhỏ (sandbox API key, local database) và số người truy cập hạn chế.
Giới hạn rõ ràng: không dùng cho production. Không có cách nào audit ai đã đọc file, không có versioning, không có rotation mechanism. Khi team lớn hơn 3-4 người, việc đồng bộ secrets giữa developer trở thành vấn đề — ai đó đổi database password mà quên thông báo, cả team mất nửa ngày debug “connection refused”.
Tầng 2: CI/CD secrets
GitHub Actions Secrets, GitLab CI/CD Variables, CircleCI Contexts — cho phép lưu secrets ở platform level, inject vào pipeline khi cần. Secrets được encrypt at rest, chỉ expose cho workflow/job cụ thể, và masked trong log output (platform tự thay giá trị secret bằng *** trong log).
# GitHub Actions — secret inject qua environment
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
run: ./deploy.sh
Ưu điểm: setup đơn giản, tích hợp sẵn với CI/CD, có access control (chỉ maintainer mới set secrets, contributor không thấy giá trị). Nhược điểm: secrets nằm rải rác ở mỗi repo hoặc mỗi project — khi cần rotate database password dùng chung bởi 10 service, phải vào từng repo đổi thủ công. Không có audit log chi tiết “ai đã đọc secret này lúc nào”, chỉ biết “secret đã được dùng trong run #1234”.
Tầng 3: Cloud secret manager
AWS Secrets Manager, GCP Secret Manager, Azure Key Vault — dịch vụ managed chuyên lưu trữ secrets. Encrypt at rest bằng KMS, có versioning (rollback về giá trị cũ), có audit log qua CloudTrail/Cloud Audit Logs, có fine-grained IAM policy cho từng secret.
# Đọc secret từ AWS Secrets Manager
import boto3
import json
def get_db_credentials():
client = boto3.client("secretsmanager", region_name="ap-southeast-1")
response = client.get_secret_value(SecretId="prod/myapp/database")
return json.loads(response["SecretString"])
# Trả về {"username": "admin", "password": "..."}
App đọc secret lúc startup hoặc khi cần, thay vì nhận qua env var. Secret không nằm trong container image, không nằm trong CI config, không nằm trên disk — chỉ tồn tại trong memory của process đang chạy. Khi rotate, cập nhật giá trị trong secret manager, app đọc giá trị mới ở lần gọi tiếp theo (hoặc qua cache invalidation).
Cloud secret manager phù hợp cho hầu hết team chạy production trên cloud. Chi phí thấp (AWS Secrets Manager khoảng $0.40/secret/tháng), operational overhead gần bằng không vì là managed service. Giới hạn: vendor lock-in ở mức API (dù concept tương tự giữa các cloud), và không có dynamic secret generation.
Tầng 4: HashiCorp Vault
Vault là bước nhảy lớn về cả capability và operational complexity. Ngoài lưu trữ static secrets như cloud secret manager, Vault sinh dynamic secrets — credentials tạm thời, có thời hạn (lease), tự động revoke khi hết hạn.
# Vault sinh database credential tạm thời — TTL 1 giờ
vault read database/creds/readonly
# Trả về:
# username: v-token-readonly-abc123
# password: A1B2C3D4E5...
# lease_id: database/creds/readonly/xyz789
# lease_duration: 1h
Mỗi service instance nhận credentials riêng, tồn tại trong thời gian ngắn. Nếu credential bị leak, nó tự hết hạn sau 1 giờ thay vì tồn tại vĩnh viễn như static password. Nếu cần revoke ngay, Vault revoke theo lease — không ảnh hưởng instance khác.
Vault cũng cung cấp encryption as a service (Transit engine — app gửi plaintext, Vault trả ciphertext, app không bao giờ thấy encryption key), PKI engine (sinh TLS certificate on-demand), và TOTP engine. Mỗi engine giải quyết một use case cụ thể.
Tuy nhiên, Vault phải được vận hành — deploy cluster (thường 3-5 node), quản lý unseal keys, backup storage backend, monitor health, upgrade version. Vault không khả dụng nghĩa là app không lấy được secrets — đây là single point of failure nghiêm trọng nếu không thiết kế HA đúng cách. Chỉ nên dùng Vault khi team có capacity vận hành hoặc dùng HCP Vault (managed service của HashiCorp).
Kubernetes secrets — base64 không phải encryption
Kubernetes Secrets lưu data dạng base64 trong etcd. Base64 là encoding, không phải encryption — echo "cGFzc3dvcmQ=" | base64 -d cho ra password ngay lập tức. Bất kỳ ai có quyền kubectl get secret đều đọc được giá trị thật.
# Kubernetes Secret — base64 encoded, KHÔNG encrypted
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
username: YWRtaW4= # "admin"
password: czNjcmV0UEBzcw== # "s3cretP@ss"
Commit file YAML này vào git nghĩa là commit password vào git — chỉ khác là nó “trông” như không phải password vì base64. Đây là hiểu lầm phổ biến nhất về Kubernetes Secrets.
Hai giải pháp phổ biến để quản lý K8s secrets an toàn hơn.
Sealed Secrets (Bitnami) cho phép encrypt secret bằng public key của cluster, commit ciphertext vào git. Chỉ controller chạy trong cluster mới có private key để decrypt. Developer tạo SealedSecret, commit vào repo, GitOps pipeline apply lên cluster, controller decrypt thành Secret thông thường. Secret plaintext không bao giờ rời khỏi cluster.
External Secrets Operator sync secrets từ external provider (AWS Secrets Manager, Vault, GCP Secret Manager) vào Kubernetes Secrets. Định nghĩa ExternalSecret resource trỏ đến secret trong provider, operator tự tạo và cập nhật K8s Secret khi giá trị nguồn thay đổi.
# ExternalSecret — sync từ AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secret-store
kind: ClusterSecretStore
target:
name: db-credentials
data:
- secretKey: password
remoteRef:
key: prod/myapp/database
property: password
Cả hai approach đều giải quyết vấn đề “secret plaintext trong git”. Sealed Secrets đơn giản hơn (không cần external provider), External Secrets Operator linh hoạt hơn (tích hợp với secret manager đã có, hỗ trợ rotation).
Secret rotation — không downtime
Rotation là thay đổi giá trị secret định kỳ hoặc khi có sự cố (nhân viên nghỉ, credential bị leak). Nghe đơn giản nhưng rotation sai cách gây outage — đổi database password trong secret manager mà app đang dùng password cũ, mọi connection mới fail ngay lập tức.
Dual-read pattern
Pattern an toàn nhất cho zero-downtime rotation: hệ thống luôn chấp nhận cả giá trị cũ và mới trong một khoảng thời gian chuyển tiếp (grace period).
Với database credentials, luồng rotation trông như thế này. Bước một: tạo user mới với password mới trong database, user cũ vẫn active. Bước hai: cập nhật secret manager với credentials mới. Bước ba: các app instance dần nhận credentials mới (qua restart, config reload, hoặc SDK refresh). Bước bốn: khi tất cả instance đã dùng credentials mới (verify qua audit log hoặc connection monitoring), disable user cũ.
Khoảng thời gian giữa bước hai và bước bốn là grace period — cả credentials cũ và mới đều hoạt động. Thời gian này phụ thuộc vào tốc độ propagation: nếu app restart nhanh (container) thì vài phút, nếu app cần deploy lại thì có thể vài giờ.
AWS Secrets Manager có tính năng rotation tự động cho RDS: Lambda function sinh password mới, cập nhật cả database user và secret value, có staging label (AWSCURRENT và AWSPREVIOUS) để app có thể fallback. Đây là implementation sẵn của dual-read pattern.
Khi nào rotation bắt buộc
Rotation định kỳ (mỗi 90 ngày chẳng hạn) giảm window of exposure nếu secret bị leak mà không biết. Nhưng quan trọng hơn, rotation ngay lập tức cần xảy ra khi: nhân viên có access rời công ty, credential bị commit vào public repo (dù đã xoá — git history vẫn có), hoặc có dấu hiệu unauthorized access trong audit log.
Rotation mà phải làm thủ công (SSH vào server đổi password, cập nhật 15 repo) thì team sẽ trì hoãn hoặc quên. Tự động hoá rotation là đầu tư có ROI rõ ràng — không phải vì tiết kiệm thời gian hàng ngày, mà vì khi incident xảy ra lúc 2 giờ sáng, on-call chạy một command thay vì 45 phút manual work.
Least privilege và audit
Mỗi service chỉ nhận secrets nó cần
Nguyên tắc least privilege áp dụng trực tiếp cho secrets: service xử lý payment chỉ cần Stripe key, không cần SendGrid key. Service gửi email chỉ cần SendGrid key, không cần database password của service khác.
Thực tế nhiều team dùng một “shared” secret store chứa tất cả secrets, mọi service đều có access. Đây là blast radius tối đa — compromise một service nghĩa là lộ tất cả secrets. IAM policy hoặc Vault policy cần scope theo service identity: service A chỉ đọc được prod/service-a/*, không đọc được prod/service-b/*.
Trong Kubernetes, dùng ServiceAccount kết hợp RBAC để giới hạn pod nào đọc được secret nào. Trong AWS, IAM role per service (hoặc per ECS task, per Lambda function) với resource-level permission trên Secrets Manager.
Audit log — ai đọc secret nào, khi nào
Audit log cho secrets không phải “nice to have” — đây là yêu cầu compliance (SOC 2, PCI DSS, HIPAA) và là công cụ điều tra incident. Khi phát hiện unauthorized access, câu hỏi đầu tiên luôn là “secret nào đã bị đọc, bởi identity nào, lúc nào?”
Cloud secret manager ghi audit log tự động qua CloudTrail (AWS) hoặc Cloud Audit Logs (GCP). Vault có audit device ghi mọi request (bao gồm cả request bị denied). Quan trọng là audit log phải được bảo vệ riêng — attacker compromise app server không nên xoá được audit log. Gửi audit log ra external system (SIEM, object storage write-only) là practice chuẩn.
Development vs production — ranh giới không được xoá
Development và production phải dùng secrets hoàn toàn khác nhau. Nghe hiển nhiên nhưng rất nhiều team dùng chung Stripe API key (hoặc tệ hơn, production database credentials) cho cả local development vì “tiện test với data thật”.
Rủi ro: developer chạy migration sai trên production database từ laptop cá nhân, hoặc test code mới gửi email thật cho customer thật từ staging environment. Mỗi môi trường cần credential set riêng biệt: development dùng sandbox/test API key, staging dùng credentials trỏ đến staging infrastructure, production dùng credentials có quyền hạn chế nhất có thể.
Access control cũng khác theo môi trường: mọi developer truy cập được development secrets, chỉ senior engineer và SRE truy cập production secrets, và production secret access phải qua approval workflow hoặc break-glass procedure cho emergency.
Sai lầm phổ biến
Hardcode secrets trong code
# KHÔNG BAO GIỜ làm thế này
STRIPE_KEY = "sk_live_abc123xyz789"
db = connect("postgres://admin:realpassword@prod-db:5432/app")
Secrets trong source code tồn tại trong git history vĩnh viễn, trong mọi bản clone, trong mọi backup. Refactor thành đọc từ environment hoặc secret manager — không có ngoại lệ, kể cả “prototype” hay “script nhỏ chạy một lần”. Script nhỏ có xu hướng trở thành script production chạy mãi mãi.
Chia sẻ secrets qua chat
“Gửi em database password qua Slack đi” — message đó nằm trong Slack search mãi mãi, accessible bởi mọi người trong workspace (hoặc bất kỳ ai compromise Slack account của bất kỳ member nào). Dùng secret manager và share access, không share giá trị. Nếu bắt buộc phải gửi, dùng tool one-time secret (Vault one-time token, hoặc service như onetimesecret.com) — link tự huỷ sau khi đọc.
Không rotate sau khi nhân viên rời đi
Nhân viên nghỉ việc mang theo kiến thức về mọi secret họ từng truy cập. Quy trình offboarding phải bao gồm: revoke personal access token, rotate shared secrets mà nhân viên đó biết giá trị, review và revoke SSH key. Nhiều team revoke account nhưng quên rằng database password mà người đó biết vẫn chưa đổi — chính nhân viên đó có thể không có ý xấu, nhưng laptop cá nhân bị mất thì credential vẫn nằm trong .bash_history.
Secret trong Docker image
# SAI — secret nằm trong image layer, ai pull image cũng thấy
ENV DATABASE_URL=postgres://admin:password@db:5432/app
COPY .env /app/.env
Docker image layer là immutable và có thể inspected. docker history --no-trunc show toàn bộ command, bao gồm ENV instruction chứa secret. Dùng runtime injection thay vì build-time: mount secret qua volume, inject qua environment variable lúc docker run, hoặc dùng Docker secrets (Swarm) / Kubernetes secrets.
Dùng cùng secret cho mọi môi trường
Staging và production dùng chung database password. Developer test migration trên staging — chạy nhầm connection string production vì biến chưa đổi. Data production bị drop. Tách biệt credentials theo môi trường là bảo vệ cơ bản nhất chống human error.
Chọn tầng phù hợp
Side project hoặc prototype — .env + .gitignore + pre-commit hook scan secret là đủ. Chi phí vận hành bằng không, rủi ro thấp vì blast radius nhỏ.
Startup hoặc team nhỏ chạy production — CI/CD secrets cho pipeline, cloud secret manager cho runtime. Setup vài giờ, chi phí vài dollar/tháng, có audit log và encryption at rest. Đây là sweet spot cho phần lớn team.
Team trung bình với compliance requirement (fintech, healthtech) — cloud secret manager kết hợp External Secrets Operator cho Kubernetes, rotation tự động cho database credentials. Đầu tư vài ngày setup, nhưng compliance audit trở nên đơn giản hơn nhiều.
Tổ chức lớn với multi-cloud hoặc yêu cầu dynamic secrets — HashiCorp Vault (self-hosted hoặc HCP). Operational overhead đáng kể, cần team có kinh nghiệm vận hành distributed system. Nhưng dynamic secrets, lease management, và encryption as a service là capabilities mà cloud secret manager đơn thuần không cung cấp.
Nguyên tắc chung: bắt đầu đơn giản, nâng tầng khi cảm thấy đau — không thiết kế Vault cluster cho 3 microservices.
Tóm tắt
Secrets khác config ở một điểm duy nhất nhưng quyết định: lộ ra ngoài là mất quyền kiểm soát hệ thống. File .env tiện cho development nhưng không có encryption, audit, hay rotation — không dùng cho production. Environment variables là cơ chế truyền phổ biến nhưng có rủi ro riêng: visible trong process listing, dễ bị log, kế thừa bởi child process.
Bốn tầng quản lý — .env, CI/CD secrets, cloud secret manager, Vault — mỗi tầng thêm capability (encryption at rest, audit log, dynamic secrets, auto-rotation) nhưng cũng thêm complexity vận hành. Chọn tầng phù hợp với quy mô team và compliance requirement, không over-engineer.
Secret rotation zero-downtime dùng dual-read pattern: cả giá trị cũ và mới đều hợp lệ trong grace period, chuyển dần rồi revoke giá trị cũ. Tự động hoá rotation — không phải vì ngày thường mà vì lúc incident xảy ra. Least privilege cho từng service, audit log gửi ra external system, và ba lớp scanning (pre-commit, CI, platform) để chặn secrets trước khi lọt vào git history. Những thứ này không phức tạp, nhưng thiếu bất kỳ mắt xích nào thì toàn bộ hệ thống secrets management đều có lỗ hổng.