Secret Management (Quản lý Secret)Secret Management
Thuộc knowledge base theo DevOps Roadmap.
Tổng quan
Secret management là kỷ luật lưu trữ, phân phối, xoay vòng (rotate) và audit các thông tin nhạy cảm — API key, mật khẩu database, chứng chỉ TLS, SSH key, encryption key, OAuth client secret và access token. Bất kỳ hệ thống thực tế nào cũng phụ thuộc vào hàng chục secret như vậy, và mỗi secret là một điểm có thể bị khai thác: một AWS key bị lộ có thể bị lợi dụng đào coin gây thiệt hại hàng nghìn đô la chỉ trong vài giờ, còn một mật khẩu database bị lộ có thể phơi bày toàn bộ dữ liệu khách hàng. Secret là “chìa khóa vương quốc”, và khác với một server bị chiếm hay một website bị deface, một credential bị đánh cắp thường cấp quyền truy cập âm thầm, đã được xác thực — trông y hệt traffic hợp lệ.
Vấn đề cốt lõi là secret có xu hướng “lan tỏa” một cách tự nhiên. Developer dán chúng vào file .env, commit lên Git “tạm thời thôi”, nhúng vào Docker image, in ra log CI, hardcode trong Terraform, hoặc gửi qua Slack. Các nghiên cứu quét GitHub public liên tục phát hiện hàng triệu credential còn hiệu lực bị commit nhầm mỗi năm. Một khi secret đã lọt vào Git history, phải coi như nó đã bị lộ — rewrite history không thể “thu hồi” nó khỏi các fork, clone, CI cache và các con bot tự động quét mọi lần push public chỉ trong vài giây.
Với DevOps engineer, secret management nằm ở giao điểm giữa security và automation: pipeline, container và infrastructure-as-code đều cần credential lúc runtime, nhưng không thứ nào trong đó được phép chứa credential. Các hệ thống trưởng thành sẽ tập trung secret vào một kho chuyên dụng (Vault hoặc cloud secret manager), inject lúc runtime với thời hạn ngắn, rotate tự động, cưỡng chế least privilege theo từng workload, và liên tục quét phát hiện rò rỉ. Mô hình tư duy cần khắc cốt ghi tâm: coi mỗi secret là một thứ sống, nguy hiểm, có chủ sở hữu rõ ràng, vòng đời ngắn và audit trail đầy đủ — không phải một chuỗi tĩnh cấu hình một lần rồi quên.
Vì sao chuyện này ngày càng quan trọng hơn trước
- Bề mặt tấn công bùng nổ. Microservice, container tồn tại ngắn (ephemeral) và multi-cloud nghĩa là hàng trăm workload, mỗi cái đều cần credential, so với vài server sống lâu của một thập kỷ trước.
- Automation đọc secret liên tục. CI/CD, GitOps controller và autoscaler fetch credential hàng nghìn lần mỗi ngày. Bất kỳ đường nào trong đó cũng là một lối exfiltration.
- Supply-chain attack nhắm vào secret. Các package npm/PyPI bị chèn mã độc và GitHub Action độc hại thường xuyên đánh cắp environment variable và
~/.aws/credentials. - Compliance đòi hỏi. SOC 2, ISO 27001, PCI-DSS và HIPAA đều yêu cầu access control, rotation và audit logging cho credential — đúng những gì một secret manager cung cấp.
Kiến thức nền tảng
Vì sao secret không được nằm trong code hay file env
- Git history tồn tại mãi mãi. Secret đã commit một lần sẽ còn trong history, fork, mirror và CI cache kể cả sau khi xóa. Attacker chạy scanner tự động trên mọi lần push public chỉ trong vài giây; kể cả repo private cũng rò rỉ qua clone, backup và cấu hình phân quyền sai.
- File env lan truyền không kiểm soát. File
.envbị copy giữa các máy, đính kèm vào ticket, gửi email, nằm trong backup, mount vào container. Không có audit trail ai đã đọc và không cách nào thu hồi quyền của một người đọc riêng lẻ. - Không rotate được. Secret hardcode gần như vĩnh viễn, vì rotate đòi hỏi sửa code và deploy lại — nên thực tế chúng không bao giờ rotate, một credential lộ từ 2019 thường vẫn còn hiệu lực đến nay.
- Không có least privilege. File config là “được ăn cả”: ai đọc được repo hoặc host là đọc được mọi secret trong đó. Không có cách nào cấp “chỉ đọc mật khẩu database cho đúng một service này”.
- Blast radius lớn. Một laptop, CI runner hoặc container image bị chiếm quyền sẽ lộ mọi secret nó từng nắm giữ, trên mọi môi trường mà các secret đó chạm tới.
- Secret lọt vào log và telemetry. Env var rò rỉ qua crash dump,
/proc/<pid>/environ, công cụ error-tracking (Sentry), vàprintenvở một debug endpoint.
Mục tiêu: secret phải được tập trung hóa, mã hóa at rest và in transit, kiểm soát truy cập, audit đầy đủ, có vòng đời ngắn và inject lúc runtime — không bao giờ lưu cạnh code sử dụng nó, và không lưu ở bất cứ đâu mà con người đọc được một cách tùy tiện.
Bài toán “secret zero”
Mọi cơ chế phân phối đều đối mặt một câu hỏi “con gà quả trứng”: để fetch được secret, workload phải xác thực trước — nhưng bằng cái gì? Nếu câu trả lời là “một secret khác”, bạn chỉ đang dời vấn đề đi chỗ khác. Đây là bài toán secret zero (bootstrap). Lời giải hiện đại là workload identity: nền tảng (platform) bảo chứng danh tính của workload (Kubernetes service account token, AWS instance role, GCP metadata-server identity, OIDC token từ CI), và secret store tin vào danh tính do platform cấp thay vì một mật khẩu pre-shared. Secret zero trở thành thứ do platform quản lý và rotate hộ bạn, không phải thứ bạn phải lưu.
Vòng đời của secret
- Tạo — sinh với entropy đủ mạnh, lý tưởng nhất là do chính secret store sinh ra để plaintext không bao giờ tồn tại trên máy developer.
- Lưu trữ — mã hóa at rest trong hệ thống chuyên dụng, không phải Git/wiki/chat/hệ thống ticket.
- Phân phối — giao cho workload lúc runtime dựa trên danh tính (identity) đã xác thực, không phải một shared secret khác (bài toán secret zero).
- Sử dụng — đọc vào memory ở thời điểm muộn nhất có thể, không ghi plaintext ra disk, không log.
- Rotation — thay thế theo lịch hoặc theo yêu cầu, lý tưởng là tự động và không downtime.
- Thu hồi — vô hiệu hóa ngay khi bị lộ hoặc khi lease hết hạn.
- Audit — ghi log mọi lần đọc/ghi kèm ai/cái gì/khi nào/ở đâu, và alert được khi bất thường.
Static secret vs dynamic secret
| Static secret | Dynamic secret | |
|---|---|---|
| Vòng đời | Sống lâu tới khi rotate thủ công | Lease ngắn (vài phút tới vài giờ), tự hết hạn |
| Cách tạo | Tạo một lần, nhiều bên dùng chung | Sinh theo từng request/consumer, theo yêu cầu |
| Blast radius khi bị đánh cắp | Toàn phần, cho tới khi ai đó phát hiện và rotate | Gần bằng không — credential tự chết |
| Truy nguyên (attribution) | Dùng chung, khó biết consumer nào làm lộ | Mỗi consumer một credential riêng |
| Ví dụ | API key trong vault, KV secret | User Postgres theo phiên, IAM credential tạm thời |
Dynamic secret là đòn bẩy lớn nhất trong secret management: một credential database có lease 1 giờ biến một vụ lộ từ thảm họa thành phiền toái nhỏ, và tự động cho bạn khả năng truy nguyên theo từng consumer.
HashiCorp Vault
Vault là secret manager self-hosted / trung lập cloud tiêu chuẩn de facto. Các thành phần kiến trúc chính:
- Storage backend — nơi lưu trữ mã hóa (phổ biến hiện nay là integrated Raft storage; trước đây hay dùng Consul). Vault mã hóa mọi thứ bằng barrier key của chính nó trước khi ghi xuống backend, nên người vận hành backend không bao giờ thấy plaintext.
- Seal/unseal — Vault khởi động ở trạng thái sealed và không giải mã được gì. Master (root) key giải mã barrier lại được chia bằng Shamir’s Secret Sharing thành các key share giao cho những người vận hành khác nhau (ví dụ cần 3 trên 5 để unseal), hoặc Vault auto-unseal qua cloud KMS/HSM (khuyến nghị cho production vì bỏ được nghi thức quorum thủ công mỗi lần restart).
- Auth methods — cách client chứng minh danh tính: Kubernetes service account, AWS/Azure/GCP IAM, OIDC/JWT, AppRole (cho máy/CI), TLS cert, LDAP, GitHub, userpass. Xác thực xong sẽ nhận một token gắn với các policy và một TTL.
- Secrets engines — backend dạng plugin, mount tại các path:
kv— key-value tĩnh; v2 thêm versioning, soft-delete và check-and-set.database— credential DB động (Postgres, MySQL, MongoDB, MSSQL, v.v.).aws/azure/gcp— credential cloud động (IAM user/role/STS tạm thời).pki— một CA nội bộ cấp chứng chỉ TLS ngắn hạn.transit— encryption-as-a-service: app gửi plaintext, Vault trả ciphertext; key không bao giờ rời Vault (“mã hóa mà không lộ key”). Cũng làm signing, HMAC, rewrap.ssh— SSH certificate đã ký / one-time password để truy cập host.totp— sinh và xác thực mã TOTP.
- Policies — ACL theo path (viết bằng HCL), cấp quyền
create/read/update/delete/list, hỗ trợ templating ({{identity.entity.name}}) để một policy giới hạn mỗi caller vào đúng namespace của nó. - Dynamic secrets và lease — Vault sinh credential theo yêu cầu, mỗi cái gắn với một lease có TTL và max_ttl. Client renew lease khi còn dùng; khi lease hết hạn hoặc bị revoke, Vault chạy logic revoke của engine (ví dụ
DROP USER) và credential biến mất. Revoke lease (hoặc mọi lease dưới một prefix) là thao tác hạng nhất khi xử lý sự cố. - Namespaces (Enterprise) — cô lập multi-tenant; Performance/DR replication cho HA đa vùng.
# KV (v2) tĩnh — key-value có version
vault kv put secret/myapp/db password='s3cr3t' username='app'
vault kv get -field=password secret/myapp/db
vault kv get -version=3 secret/myapp/db # phục hồi một version cũ
# Credential database động — mỗi request một user mới, tự hết hạn
vault read database/creds/readonly-role
# Key Value
# lease_id database/creds/readonly-role/abc123
# lease_duration 1h
# username v-token-readonly-x7Kq... (vừa được tạo)
# password A1a-9nB2... (chết khi lease kết thúc)
# Encryption-as-a-service: app không bao giờ thấy key
vault write transit/encrypt/orders plaintext=$(base64 <<< "4111-1111-1111-1111")
# Xử lý sự cố: revoke mọi thứ mà một role bị compromise từng cấp
vault lease revoke -prefix database/creds/readonly-role
Mô hình tư duy về Vault: Vault là một broker, không phải tủ hồ sơ. Giá trị cao nhất của nó không phải lưu chuỗi tĩnh (cloud manager cũng làm được) mà là đúc ra credential ngắn hạn theo từng consumer và các phép crypto theo yêu cầu, để secret sống lâu ngừng tồn tại trong hệ thống của bạn.
Cloud secret managers
| Dịch vụ | Điểm mạnh | Ghi chú / giới hạn |
|---|---|---|
| AWS Secrets Manager | Rotation tự động qua Lambda, tích hợp RDS/Redshift/DocumentDB, truy cập cross-account qua resource policy, secret dạng JSON | ~0.40 USD/secret/tháng + 0.05 USD mỗi 10k API call |
| AWS SSM Parameter Store | Miễn phí (standard param), SecureString qua KMS, path phân cấp, phù hợp cho cả config lẫn secret | Không có rotation tích hợp; giới hạn 4 KB (standard) / 8 KB (advanced); throughput thấp hơn |
| Azure Key Vault | Secret, key và certificate trong một dịch vụ; key software hoặc HSM-backed; RBAC và access policy cũ; tier managed HSM | Tích hợp sâu Entra ID (Azure AD); soft-delete + purge protection |
| GCP Secret Manager | Secret có version, truy cập theo IAM, hỗ trợ CMEK, chính sách replication tự động | Rotation native chỉ là thông báo (Pub/Sub) — bạn tự viết function rotate |
Cloud manager thắng về độ đơn giản vận hành (không phải chạy cluster, không nghi thức seal/unseal), tích hợp IAM native (workload identity có sẵn) và trả theo mức dùng. Vault thắng về dynamic secret cho nhiều hệ thống, tính di động multi-cloud (một API bất kể provider) và các engine nâng cao (transit encryption, PKI nội bộ, SSH CA). Nhiều tổ chức chạy cả hai: cloud manager cho secret tĩnh cloud-native, Vault cho credential động và crypto xuyên suốt.
KMS vs secret manager — một nhầm lẫn phổ biến
Một Key Management Service (AWS KMS, Cloud KMS, key trong Azure Key Vault) quản lý encryption key và thực hiện các phép crypto; key material không bao giờ rời KMS. Một secret manager lưu và phân phối các giá trị secret tùy ý (và thường dùng KMS bên dưới để mã hóa at rest). Quy tắc: dùng KMS khi cần mã hóa/giải mã/ký dữ liệu mà không lộ key; dùng secret manager khi workload cần chính giá trị secret (mật khẩu, API token). Engine transit của Vault chính là chức năng kiểu KMS nằm trong Vault.
Khái niệm chính
Kubernetes Secrets và những hạn chế
Object Secret gốc của K8s mặc định chỉ base64-encode, không mã hóa — base64 là một cách encode chứ không phải cipher, base64 -d đảo ngược tức thì. Chúng được lưu trong etcd, dưới dạng plaintext trừ khi bật encryption at rest bằng EncryptionConfiguration (và kể cả khi đó, nên dùng KMS provider thay vì key aescbc tĩnh — vì key tĩnh nằm ngay trên disk của API server). Ngoài vấn đề lưu trữ, các bề mặt lộ thực sự là:
- Ai có role RBAC rộng (
get/listtrên Secret) đều đọc được — và nhiều role mặc định quá rộng. - Ai truy cập được node đều đọc được secret đang mount vào pod trên node đó (
/var/run/secrets/...). - Secret nằm trong manifest quản lý bằng Git chính là plaintext trong Git — anti-pattern kinh điển của GitOps.
- Secret inject dưới dạng environment variable rò rỉ qua crash dump,
/procvà tiến trình con.
Nên mount secret dưới dạng file (volume tmpfs) thay vì env var, siết chặt RBAC, và dùng một trong các pattern dưới đây để giữ plaintext ngoài Git.
| Cách tiếp cận | Cơ chế | Phù hợp khi | Cần lưu ý |
|---|---|---|---|
| External Secrets Operator (ESO) | Đồng bộ secret từ Vault/AWS/Azure/GCP thành K8s Secret qua CRD ExternalSecret | GitOps: manifest chỉ tham chiếu tên, giá trị nằm ở store bên ngoài | Giá trị vẫn nằm trong etcd; vẫn phải bật encryption etcd |
| Sealed Secrets (Bitnami) | kubeseal mã hóa secret bằng public key của controller trong cluster; CR SealedSecret commit lên Git an toàn | GitOps đơn giản, không cần chạy store bên ngoài | Private key của controller cực kỳ quan trọng — phải backup; giải mã gắn với cluster |
| SOPS (+ age/KMS) | Mã hóa giá trị bên trong file YAML/JSON nhưng giữ key đọc được; tích hợp Flux/Argo CD | Mã hóa nguyên file config trong Git, thân thiện với git-diff | Phân phối/rotate key là việc của bạn; ai giữ age/KMS key thì giải mã được |
| Vault Agent Injector / Vault Secrets Operator | Injector thêm sidecar/init container render secret vào volume dùng chung; VSO đồng bộ qua CRD | Cluster đã dùng Vault; hỗ trợ dynamic secret và auto-renew | Sidecar tốn tài nguyên pod; VSO là hướng mới, khai báo (declarative) hơn |
| Secrets Store CSI Driver | Mount secret từ store bên ngoài dưới dạng volume; có thể không tạo K8s Secret luôn | Tránh hoàn toàn việc lưu vào etcd; tiêu thụ dạng file | Tiêu thụ dạng env var lại cần sync ngược thành K8s Secret (mất ý nghĩa) |
# Ví dụ External Secrets Operator
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: myapp-db
spec:
refreshInterval: 1h # re-sync để rotation lan truyền
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: myapp-db # tên K8s Secret được tạo ra
creationPolicy: Owner
data:
- secretKey: password # key trong K8s Secret
remoteRef:
key: secret/myapp/db # path trong Vault
property: password
Hướng dẫn chọn: Không có store bên ngoài và chỉ muốn secret ra khỏi Git → Sealed Secrets hoặc SOPS. Đã chạy Vault/cloud secret manager và muốn K8s mirror lại → ESO hoặc Vault/Secrets CSI. Cần secret động (có lease) trong pod → Vault (Injector/VSO/CSI). Muốn không bao giờ ghi vào etcd → CSI driver với mount dạng file.
Ví dụ flow: Vault + Kubernetes auth
- Bật Kubernetes auth method trong Vault, cấu hình với địa chỉ API server và CA của cluster (Vault dùng chúng để xác thực token).
- Gắn một role Vault với namespace + service account cụ thể, kèm policy và TTL.
- Pod (qua Vault Agent, VSO, hoặc app trực tiếp) gửi projected service account JWT lên endpoint login của Vault.
- Vault xác thực JWT qua Kubernetes TokenReview API — chính cluster bảo chứng rằng token thuộc về service account của pod đó.
- Vault trả về một Vault token ngắn hạn, giới hạn theo policy của role.
- Workload đọc secret tĩnh của mình, hoặc yêu cầu credential DB động và nhận username/password có lease.
- Vault Agent/VSO renew token và các lease ở background; khi pod kết thúc, các lease được revoke.
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
vault write auth/kubernetes/role/myapp \
bound_service_account_names=myapp \
bound_service_account_namespaces=prod \
policies=myapp-read \
ttl=1h
Không còn secret zero: danh tính của pod (service account token do Kubernetes ký, và Kubernetes tự rotate) chính là credential. Không có gì sống lâu được pre-share cả.
Phát hiện secret bị rò rỉ
- gitleaks — scanner nhanh dùng regex + entropy cho repo, toàn bộ history và pre-commit hook. Rule và allowlist cấu hình được:
gitleaks git -v .hoặcgitleaks dir ./config. - trufflehog — quét Git history, S3 bucket, Docker image, filesystem, v.v.; tính năng “sát thủ” là verify — chủ động kiểm tra credential tìm được có còn sống hay không bằng cách gọi API của provider, giảm mạnh false positive:
trufflehog git file://. --results=verified. - detect-secrets (Yelp) — scanner theo hướng baseline, thiết kế để áp lên repo có sẵn mà không “chết chìm” trong các phát hiện lịch sử.
- GitHub secret scanning + push protection — GitHub khớp các pattern credential đã biết từ 100+ đối tác; push protection chặn push trước khi secret kịp lọt vào. Nên bật cả hai ở cấp organization; đối tác được tự động thông báo khi token của họ bị lộ có xác minh.
- Scanner của cloud — AWS cũng cảnh báo access key bị lộ, và các dịch vụ như GitGuardian giám sát liên tục toàn organization.
Chạy scanner ở ba nơi: pre-commit hook (bắt trước khi rời laptop), CI pipeline (cưỡng chế để không gì merge được), và quét full-history định kỳ (bắt rò rỉ cũ và rule detector mới). Defense in depth, vì con người sẽ lọt qua bất kỳ cổng đơn lẻ nào.
Nếu secret bị lộ — rotate trước, dọn dẹp sau. Thứ tự rất quan trọng:
- Revoke/rotate credential ngay lập tức. Chuỗi bị lộ đã compromise ngay khi công khai; một con bot có thể đã cầm nó rồi.
- Audit xem đã bị dùng độc hại chưa. Kiểm tra CloudTrail/audit log xem có truy cập từ IP lạ hay vào giờ bất thường không.
- Chỉ sau đó mới rewrite history (
git filter-repo, BFG) — và hiểu rằng đây là vệ sinh, không phải biện pháp khắc phục. Fork, clone và cache vẫn còn blob cũ, đó chính là lý do bước 1 mới là thứ thực sự bảo vệ bạn. - Hậu sự cố: tìm ra nó lộ như thế nào và bịt đường đó (thêm pre-commit hook, siết CI, sửa chỗ logging đã in nó ra).
Rotation và least privilege
- Rotation giới hạn thời gian hữu dụng của credential bị đánh cắp, là khác biệt giữa “một ngày tồi tệ” và “một vụ breach”. Ưu tiên rotation tự động (Lambda rotation của AWS Secrets Manager, dynamic secret của Vault, managed identity của cloud) thay vì nhắc lịch thủ công — rotation thủ công gần như chắc chắn không diễn ra. Thiết kế app đọc lại secret theo chu kỳ refresh hoặc theo tín hiệu, để rotation không gây sự cố thay vì phải deploy lại. Để rotate secret tĩnh mà không downtime, dùng pattern hai-secret (dual-credential): giữ credential cũ còn hiệu lực trong khi cái mới lan truyền, rồi mới thu hồi cái cũ.
- Least privilege: mỗi workload có identity riêng và chỉ đọc được path của chính nó. Trong Vault là policy theo path, theo role (lý tưởng là template theo identity của caller); trong AWS là IAM/resource policy theo từng secret. Tránh một “kho secret chung” mà mọi service đọc được — điều đó biến một vụ compromise thành compromise toàn phần.
- Ưu tiên workload identity thay vì static key ở mọi nơi: IRSA / EKS Pod Identity trên AWS, Workload Identity trên GKE, Managed Identity trên Azure, và OIDC federation cho CI (GitHub Actions / GitLab → AWS/GCP/Azure mà không lưu key cloud dài hạn nào trong hệ thống CI). Static cloud key dài hạn nên là ngoại lệ hiếm hoi và có lý do rõ ràng.
- Giới hạn scope, tách môi trường và cho hết hạn. Tách secret theo từng môi trường (không bao giờ chia sẻ secret prod sang staging). Cho mỗi credential scope hẹp nhất và TTL ngắn nhất mà vẫn chạy được.
Mã hóa in transit và at rest
Secret phải được mã hóa cả at rest (trong backend của store, trong etcd, trong backup) lẫn in transit (TLS khắp nơi — client tới Vault, Vault tới database, ESO tới cloud API). Một secret manager mã hóa at rest nhưng bị query qua HTTP plaintext thì chỉ được bảo vệ một nửa. Envelope encryption (một data key mã hóa secret, một KMS root key mã hóa data key) là pattern chuẩn cho phép rotate root key mà không phải mã hóa lại từng secret.
Best Practices
- Không bao giờ commit secret — và cưỡng chế bằng tooling, không dựa vào niềm tin. Pre-commit hook gitleaks + quét trong CI + GitHub push protection. Con người sẽ sơ suất; tooling nhiều lớp phải bắt được. Một chính sách dựa vào việc developer “cẩn thận” thì không phải là một control.
- Tập trung vào một kho chuyên dụng. Một nguồn sự thật duy nhất (Vault hoặc cloud secret manager) có mã hóa, ACL, versioning và audit log — không phải file
.env, wiki, hệ thống ticket hay chat. Phân mảnh là kẻ thù của kiểm soát. - Ưu tiên credential động, thời hạn ngắn. Credential database hoặc cloud có lease 1 giờ biến một vụ lộ từ thảm họa thành phiền toái nhỏ và cho bạn khả năng truy nguyên theo consumer miễn phí.
- Dùng workload identity, không dùng bootstrap secret dùng chung. Kubernetes service account, cloud IAM role và OIDC federation giải quyết bài toán secret zero — platform chứng minh danh tính để bạn khỏi lưu mật khẩu bootstrap.
- Tự động hóa rotation và thiết kế app chịu được nó. Rotation thủ công sẽ không diễn ra. Gắn rotation vào store và làm app đọc lại secret (hoặc dùng pattern dual-credential) để rotation không gây downtime.
- Áp dụng least privilege theo từng workload. Mỗi app một identity, một policy, một path hẹp; không bao giờ dùng một kho “tất cả trong một”. Audit policy định kỳ để phát hiện scope creep và revoke quyền khi offboarding.
- Mã hóa Kubernetes secret at rest và siết RBAC. Bật encryption etcd bằng KMS provider, mount secret dạng file thay vì env var, và giới hạn chặt ai được
get/listSecret trên toàn cluster. - Giữ secret ngoài image, log và output CI. Không bao giờ
ENV/ARGsecret trong Dockerfile (nó tồn tại trong image layer); mask secret trong CI; cẩn thận vớiset -x, error message dài dòng và công cụ error-tracking chụp lại environment. - Luôn mã hóa in transit. TLS từ client tới store và từ store tới các hệ thống backend. Mã hóa at rest vô nghĩa nếu giá trị đi qua mạng dưới dạng plaintext.
- Audit mọi thứ và alert khi bất thường. Bật Vault audit device / CloudTrail / Key Vault diagnostics, và cảnh báo khi có điều bất thường — một CI token đọc path production lúc 3 giờ sáng, một đợt tăng vọt số lần đọc, truy cập từ IP mới.
- Coi mọi secret đã lộ là đã compromise — rotate trước. Đừng tin vào “chỉ public có một phút” hay việc rewrite history. Revoke ngay, rồi mới điều tra và dọn dẹp.
- Version hóa secret và có kế hoạch rollback. KV v2 / secret version trên cloud giúp phục hồi tức thì khi rotation lỗi thay vì cuống cuồng dựng lại giá trị cũ.
- Có quy trình break-glass. Ghi rõ cách con người truy cập secret quan trọng khi automation gặp sự cố — kèm MFA, yêu cầu phê duyệt và review hậu kiểm. Test trước khi cần đến.
- Tách secret theo môi trường và theo tenant. Prod, staging và dev không được dùng chung credential; một vụ lộ ở môi trường ít tin cậy không bao giờ được cấp quyền vào prod.
- Giảm tối đa số người từng thấy plaintext. Ưu tiên secret do store sinh để plaintext không tồn tại trên laptop; dùng phê duyệt và quyền truy cập giới hạn thời gian cho vài trường hợp hiếm hoi con người phải đọc.
- Mỗi secret phải có chủ sở hữu và ngày hết hạn được ghi rõ. Secret vô chủ, không bao giờ hết hạn tích tụ thành gánh nặng không quản lý nổi. Kiểm kê chúng và loại bỏ những cái không dùng.
- Backup và DR-test cho secret store. Secret manager là một single point of failure; chạy HA, backup storage và unseal/root key một cách an toàn, và diễn tập phục hồi.
Tài liệu tham khảo
- roadmap.sh — DevOps Roadmap
- Tài liệu HashiCorp Vault
- Vault concepts: seal/unseal · leases
- Vault dynamic secrets tutorial
- Vault Kubernetes auth method
- Vault transit (encryption-as-a-service) engine
- Tài liệu AWS Secrets Manager
- AWS Secrets Manager rotation
- AWS Systems Manager Parameter Store
- Tài liệu Azure Key Vault
- Tài liệu GCP Secret Manager
- Kubernetes — Secrets · Encrypting Secret Data at Rest
- External Secrets Operator
- Bitnami Sealed Secrets
- SOPS
- Secrets Store CSI Driver
- gitleaks · trufflehog · detect-secrets
- GitHub secret scanning & push protection
- Cấu hình OpenID Connect trong cloud provider (GitHub Actions)
- OWASP Secrets Management Cheat Sheet
Part of the DevOps Roadmap knowledge base.
Overview
Secret management is the discipline of storing, distributing, rotating, and auditing sensitive credentials — API keys, database passwords, TLS certificates, SSH keys, encryption keys, OAuth client secrets, and access tokens. Every non-trivial system depends on dozens of these, and each one is a potential breach vector: a leaked AWS key can cost thousands of dollars in cryptomining within hours, and a leaked database password can expose your entire customer dataset. Secrets are the keys to the kingdom, and unlike a stolen server or a defaced website, a stolen credential often grants silent, authenticated access that looks exactly like legitimate traffic.
The core problem is that secrets naturally want to spread. Developers paste them into .env files, commit them to Git “just for now”, bake them into Docker images, print them in CI logs, hardcode them in Terraform, and share them over Slack. Studies of public GitHub repositories consistently find millions of live credentials committed by accident every year. Once a secret touches Git history, it must be considered compromised — rewriting history does not un-leak it from forks, clones, CI caches, and the automated scrapers that watch every public push within seconds.
For DevOps engineers, secret management sits at the intersection of security and automation: pipelines, containers, and infrastructure-as-code all need credentials at runtime, but none of them should ever contain credentials. Mature setups centralize secrets in a dedicated store (Vault, a cloud secret manager), inject them at runtime with short lifetimes, rotate them automatically, enforce least privilege per workload, and continuously scan for leaks. The mental model to internalize: treat every secret as a live, dangerous thing with a definite owner, a short lifetime, and a full audit trail — not as a static string you configure once and forget.
Why this matters more than it used to
- Attack surface exploded. Microservices, ephemeral containers, and multi-cloud mean hundreds of workloads each needing credentials, versus a handful of long-lived servers a decade ago.
- Automation reads secrets constantly. CI/CD, GitOps controllers, and autoscalers fetch credentials thousands of times a day. Any one of those paths is an exfiltration route.
- Supply-chain attacks target secrets. Compromised npm/PyPI packages and malicious GitHub Actions routinely exfiltrate environment variables and
~/.aws/credentials. - Compliance demands it. SOC 2, ISO 27001, PCI-DSS, and HIPAA all require access control, rotation, and audit logging for credentials — exactly what a secret manager provides.
Fundamentals
Why secrets don’t belong in code or env files
- Git history is forever. A secret committed once survives in history, forks, mirrors, and CI caches even after deletion. Attackers run automated scanners against every public push within seconds; even private repos leak through clones, backups, and misconfigured access.
- Env files spread uncontrolled.
.envfiles get copied between machines, attached to tickets, emailed, included in backups, and mounted into containers. There is no audit trail of who read them and no way to revoke a single reader. - No rotation. Hardcoded secrets are effectively permanent, because rotating them requires a code change and redeploy — so in practice they never rotate, and a credential leaked in 2019 is often still valid today.
- No least privilege. A config file is all-or-nothing: anyone who can read the repo or the host reads every secret in it. There is no way to grant “read only the database password to this one service”.
- Blast radius. One compromised laptop, CI runner, or container image exposes every secret it ever held, across every environment those secrets touch.
- Secrets end up in logs and telemetry. Env vars leak through crash dumps,
/proc/<pid>/environ, error-tracking tools (Sentry), andprintenvin a debug endpoint.
The goal: secrets should be centralized, encrypted at rest and in transit, access-controlled, audited, short-lived, and injected at runtime — never stored alongside the code that uses them, and never persisted anywhere a human can casually read them.
The secret zero problem
Every distribution scheme faces a chicken-and-egg question: to fetch a secret, a workload must first authenticate — but with what? If the answer is “another secret”, you have merely moved the problem. This is the secret zero (or bootstrap) problem. The modern answer is workload identity: the platform vouches for the workload’s identity (a Kubernetes service account token, an AWS instance role, a GCP metadata-server identity, an OIDC token from your CI), and the secret store trusts that platform-issued identity instead of a pre-shared password. Secret zero becomes something the platform manages and rotates for you, not something you store.
The secret lifecycle
- Creation — generated with sufficient entropy, ideally by the secret store itself so the plaintext never exists on a developer’s machine.
- Storage — encrypted at rest in a dedicated system, not in Git/wikis/chat/ticketing systems.
- Distribution — delivered to workloads at runtime via authenticated identity (not another shared secret — the secret zero problem).
- Usage — read into memory at the last possible moment, never written to disk in plaintext, never logged.
- Rotation — replaced on a schedule or on demand, ideally automatically and without downtime.
- Revocation — invalidated immediately on compromise or when a lease expires.
- Audit — every read and write logged with who/what/when/where, and alertable on anomalies.
Static vs dynamic secrets
| Static secret | Dynamic secret | |
|---|---|---|
| Lifetime | Long-lived until manually rotated | Short lease (minutes to hours), auto-expiring |
| Creation | Created once, reused by many | Generated per request/consumer on demand |
| Blast radius if stolen | Full, until someone notices and rotates | Near-zero — credential dies on its own |
| Attribution | Shared, hard to trace which consumer leaked it | Each consumer gets a unique credential |
| Examples | An API key in a vault, a KV secret | A per-session Postgres user, a temporary IAM credential |
Dynamic secrets are the single biggest lever in secret management: a database credential with a 1-hour lease turns a breach from a catastrophe into a nuisance, and gives you per-consumer attribution for free.
HashiCorp Vault
Vault is the de facto standard self-hosted / cloud-agnostic secret manager. Key architectural pieces:
- Storage backend — encrypted storage (integrated Raft storage is the common modern choice; Consul was traditional) holding all data. Vault encrypts everything with its own barrier key before it touches the backend, so the backend operator never sees plaintext.
- Seal/unseal — Vault starts sealed and can decrypt nothing. The master (root) key that decrypts the barrier is itself split via Shamir’s Secret Sharing into key shares given to different trusted operators (e.g., 3-of-5 needed to unseal), or Vault is auto-unsealed via a cloud KMS/HSM (recommended for production, since it removes the manual quorum ceremony on every restart).
- Auth methods — how clients prove identity: Kubernetes service accounts, AWS/Azure/GCP IAM, OIDC/JWT, AppRole (for machines/CI), TLS certs, LDAP, GitHub, userpass. Authentication yields a token with attached policies and a TTL.
- Secrets engines — pluggable backends mounted at paths:
kv— static key-value; v2 adds versioning, soft-delete, and check-and-set.database— dynamic DB credentials (Postgres, MySQL, MongoDB, MSSQL, and more).aws/azure/gcp— dynamic cloud credentials (temporary IAM users/roles/STS).pki— an internal certificate authority issuing short-lived TLS certs.transit— encryption-as-a-service: apps send plaintext, Vault returns ciphertext; the key never leaves Vault (“encryption without exposing keys”). Also does signing, HMAC, and rewrapping.ssh— signed SSH certificates / one-time passwords for host access.totp— generates and validates TOTP codes.
- Policies — path-based ACLs (HCL) granting
create/read/update/delete/liston secret paths, with support for templating ({{identity.entity.name}}) so one policy scopes each caller to its own namespace. - Dynamic secrets and leases — Vault generates credentials on demand, each attached to a lease with a TTL and max_ttl. Clients renew leases while in use; when the lease expires or is revoked, Vault runs the engine’s revoke logic (e.g.,
DROP USER) and the credential is gone. Revoking a lease (or all leases under a prefix) is a first-class operation for incident response. - Namespaces (Enterprise) — multi-tenant isolation; Performance/DR replication for HA across regions.
# Static KV (v2) — versioned key-value
vault kv put secret/myapp/db password='s3cr3t' username='app'
vault kv get -field=password secret/myapp/db
vault kv get -version=3 secret/myapp/db # recover a prior version
# Dynamic database credentials — new user per request, auto-expiring
vault read database/creds/readonly-role
# Key Value
# lease_id database/creds/readonly-role/abc123
# lease_duration 1h
# username v-token-readonly-x7Kq... (created just now)
# password A1a-9nB2... (dies when the lease ends)
# Encryption-as-a-service: the app never sees the key
vault write transit/encrypt/orders plaintext=$(base64 <<< "4111-1111-1111-1111")
# Incident response: revoke everything a compromised role ever issued
vault lease revoke -prefix database/creds/readonly-role
A HashiCorp Vault mental model: Vault is a broker, not a filing cabinet. Its highest value is not storing static strings (cloud managers do that too) but minting short-lived, per-consumer credentials and cryptographic operations on demand, so long-lived secrets stop existing in your fleet at all.
Cloud secret managers
| Service | Strengths | Notes / limits |
|---|---|---|
| AWS Secrets Manager | Native rotation via Lambda, RDS/Redshift/DocumentDB integration, cross-account access via resource policies, JSON secrets | ~$0.40/secret/month + $0.05 per 10k API calls |
| AWS SSM Parameter Store | Free tier (standard params), SecureString via KMS, hierarchical paths, good for config and secrets | No built-in rotation; 4 KB (standard) / 8 KB (advanced) size limits; lower throughput |
| Azure Key Vault | Secrets, keys, and certificates in one service; software or HSM-backed keys; RBAC and legacy access policies; managed HSM tier | Deep Entra ID (Azure AD) integration; soft-delete + purge protection |
| GCP Secret Manager | Simple versioned secrets, IAM-based access, CMEK support, automatic replication policies | Native rotation is notification-only (Pub/Sub) — you write the rotation function |
Cloud managers win on operational simplicity (no cluster to run, no seal/unseal ceremony), native IAM integration (workload identity is built in), and pay-as-you-go. Vault wins on dynamic secrets across many systems, multi-cloud portability (one API regardless of provider), and advanced engines (transit encryption, internal PKI, SSH CA). Many organizations run both: a cloud manager for cloud-native static secrets, Vault for dynamic credentials and cross-cutting crypto.
KMS vs secret managers — a common confusion
A Key Management Service (AWS KMS, Cloud KMS, Azure Key Vault keys) manages encryption keys and performs crypto operations; the key material never leaves the KMS. A secret manager stores and distributes arbitrary secret values (and usually uses a KMS under the hood to encrypt them at rest). Rule of thumb: use a KMS when you need to encrypt/decrypt/sign data and never expose the key; use a secret manager when a workload needs the actual secret value (a password, an API token). Vault’s transit engine is KMS-like functionality inside Vault.
Key Concepts
Kubernetes Secrets and their limitations
Native K8s Secret objects are only base64-encoded, not encrypted by default — base64 is an encoding, not a cipher, and base64 -d reverses it instantly. They are stored in etcd, in plaintext unless you enable encryption at rest with an EncryptionConfiguration (and even then, prefer a KMS provider over the static aescbc key, whose key otherwise sits on the API server disk). Beyond storage, the real exposure surfaces are:
- Anyone with a broad RBAC role (
get/liston Secrets) reads them — and many default roles are too broad. - Anyone with node access reads secrets mounted into pods on that node (
/var/run/secrets/...). - Secrets committed in Git-managed manifests are just plaintext in Git — the GitOps anti-pattern.
- Secrets injected as environment variables leak via crash dumps,
/proc, and child processes.
Prefer mounting secrets as files (tmpfs volumes) over env vars, scope RBAC tightly, and use one of the patterns below to keep plaintext out of Git.
| Approach | How it works | Best for | Watch out for |
|---|---|---|---|
| External Secrets Operator (ESO) | Syncs secrets from Vault/AWS/Azure/GCP into K8s Secret objects via ExternalSecret CRDs | GitOps: manifests reference secrets by name, values live in the external store | Values still land in etcd; still enable etcd encryption |
| Sealed Secrets (Bitnami) | kubeseal encrypts a secret with the cluster controller’s public key; the SealedSecret CR is safe to commit to Git | Simple GitOps without running an external store | Controller’s private key is critical — back it up; decryption is cluster-scoped |
| SOPS (+ age/KMS) | Encrypts values inside YAML/JSON while leaving keys readable; integrates with Flux/Argo CD | Encrypting whole config files in Git, git-diff-friendly | Key distribution/rotation is on you; whoever holds the age/KMS key decrypts |
| Vault Agent Injector / Vault Secrets Operator | Injector adds a sidecar/init container that renders secrets to a shared volume; VSO syncs via CRDs | Clusters already using Vault; supports dynamic secrets and auto-renew | Sidecar adds pod overhead; VSO is the newer, more declarative path |
| Secrets Store CSI Driver | Mounts secrets from external stores as a volume; can optionally not create a K8s Secret at all | Avoiding etcd storage entirely; file-based consumption | Env-var consumption requires syncing back to a K8s Secret (defeats the point) |
# External Secrets Operator example
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: myapp-db
spec:
refreshInterval: 1h # re-sync so rotations propagate
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: myapp-db # resulting K8s Secret name
creationPolicy: Owner
data:
- secretKey: password # key in the K8s Secret
remoteRef:
key: secret/myapp/db # path in Vault
property: password
Decision guide: No external store and just want secrets out of Git → Sealed Secrets or SOPS. Already run Vault/cloud secret manager and want K8s to mirror it → ESO or Vault/Secrets CSI. Need dynamic (leased) secrets in pods → Vault (Injector/VSO/CSI). Want to never persist to etcd → CSI driver with file mounts.
Example flow: Vault + Kubernetes auth
- Enable the Kubernetes auth method in Vault and configure it with the cluster’s API server address and CA (Vault uses these to validate tokens).
- Bind a Vault role to a specific namespace + service account, attaching a policy and a TTL.
- The pod (via Vault Agent, VSO, or the app directly) sends its projected service account JWT to Vault’s login endpoint.
- Vault validates the JWT against the Kubernetes TokenReview API — the cluster itself vouches that the token belongs to that pod’s service account.
- Vault returns a short-lived Vault token scoped to the role’s policy.
- The workload reads its static secrets, or requests dynamic DB credentials and receives a leased username/password.
- Vault Agent/VSO renews the token and any leases in the background; on pod termination, leases are revoked.
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
vault write auth/kubernetes/role/myapp \
bound_service_account_names=myapp \
bound_service_account_namespaces=prod \
policies=myapp-read \
ttl=1h
No secret zero: the pod’s identity (its Kubernetes-signed service account token, which Kubernetes rotates automatically) is the credential. Nothing long-lived was pre-shared.
Detecting leaked secrets
- gitleaks — fast regex + entropy scanner for repos, full history, and pre-commit hooks. Configurable rules and allowlists:
gitleaks git -v .orgitleaks dir ./config. - trufflehog — scans Git history, S3 buckets, Docker images, filesystems, and more; its killer feature is verification — it actively checks whether a found credential is live against the provider’s API, cutting false positives dramatically:
trufflehog git file://. --results=verified. - detect-secrets (Yelp) — baseline-oriented scanner designed to be adopted on existing repos without drowning you in historical findings.
- GitHub secret scanning + push protection — GitHub matches known credential patterns from 100+ partners; push protection blocks the push before the secret ever lands. Enable both org-wide; partners are auto-notified of verified leaks of their tokens.
- Cloud-native scanners — AWS also flags exposed access keys, and providers like GitGuardian offer continuous org-wide monitoring.
Run scanners in three places: a pre-commit hook (catch it before it leaves the laptop), the CI pipeline (enforce so nothing merges), and scheduled full-history scans (catch old leaks and new detector rules). Defense in depth, because humans will slip past any single gate.
If a secret leaks — rotate first, then clean up. The order matters:
- Revoke/rotate the credential immediately. The leaked string is compromised the instant it’s public; a bot may already have it.
- Audit for malicious use. Check CloudTrail/audit logs for access from unexpected IPs or at odd times.
- Only then rewrite history (
git filter-repo, BFG) — and understand this is hygiene, not remediation. Forks, clones, and caches still hold the old blob, which is exactly why step 1 is what actually protects you. - Post-incident: figure out how it leaked and close that path (add a pre-commit hook, tighten CI, fix the logging that printed it).
Rotation and least privilege
- Rotation limits the useful lifetime of a stolen credential and is the difference between “a bad day” and “a breach”. Prefer automatic rotation (AWS Secrets Manager Lambda rotation, Vault dynamic secrets, cloud-managed identities) over calendar reminders — manual rotation reliably does not happen. Design apps to re-read secrets on a refresh interval or on a signal, so rotation is a non-event rather than a redeploy. For zero-downtime rotation of static secrets, use the two-secret (dual-credential) pattern: keep the previous credential valid while the new one propagates, then retire the old one.
- Least privilege: each workload gets its own identity and can read only its own paths. In Vault this is per-path, per-role policies (ideally templated to the caller’s identity); in AWS, per-secret IAM/resource policies. Avoid a single “app secrets” bucket that every service can read — that turns any one compromise into a total compromise.
- Prefer workload identity over static keys everywhere: IRSA / EKS Pod Identity on AWS, Workload Identity on GKE, Managed Identities on Azure, and OIDC federation for CI (GitHub Actions / GitLab → AWS/GCP/Azure with no long-lived cloud keys stored in the CI system at all). Long-lived static cloud keys should be the rare, well-justified exception.
- Scope, environment-separate, and expire. Separate secrets per environment (never share prod secrets into staging). Give every credential the narrowest scope and the shortest TTL that still works.
Encryption in transit and at rest
Secrets must be encrypted both at rest (in the store’s backend, in etcd, in backups) and in transit (TLS everywhere — client to Vault, Vault to database, ESO to cloud API). A secret manager that encrypts at rest but is queried over plaintext HTTP is only half-protected. Envelope encryption (a data key encrypts the secret, a KMS root key encrypts the data key) is the standard pattern that lets you rotate the root key without re-encrypting every secret.
Best Practices
- Never commit secrets — and enforce it with tooling, not trust. Pre-commit gitleaks hooks + CI scanning + GitHub push protection. Humans will slip; layered tooling must catch it. A policy that relies on developers “being careful” is not a control.
- Centralize in a dedicated store. One system of record (Vault or a cloud secret manager) with encryption, ACLs, versioning, and audit logs — not
.envfiles, wikis, ticketing systems, or chat. Fragmentation is the enemy of control. - Prefer dynamic, short-lived credentials. A database or cloud credential with a 1-hour lease turns a breach from a catastrophe into a nuisance and gives you per-consumer attribution for free.
- Use workload identity, not shared bootstrap secrets. Kubernetes service accounts, cloud IAM roles, and OIDC federation solve the secret zero problem — the platform proves identity so you don’t store a bootstrap password.
- Automate rotation and design apps to tolerate it. Manual rotation doesn’t happen. Wire rotation into the store and make apps re-read secrets (or use the dual-credential pattern) so rotation causes no downtime.
- Apply least privilege per workload. One identity, one policy, one narrow path per app; never a shared “everything” bucket. Audit policies regularly for scope creep and revoke access on offboarding.
- Encrypt Kubernetes secrets at rest and restrict RBAC. Enable etcd encryption with a KMS provider, mount secrets as files rather than env vars, and tightly limit who can
get/listSecrets cluster-wide. - Keep secrets out of images, logs, and CI output. Never
ENV/ARGa secret in a Dockerfile (it persists in image layers); mask secrets in CI; bewareset -x, verbose error messages, and error-tracking tools that capture environment. - Encrypt in transit, always. TLS from client to store and from store to backing systems. At-rest encryption is meaningless if the value crosses the network in plaintext.
- Audit everything and alert on anomalies. Enable Vault audit devices / CloudTrail / Key Vault diagnostics, and alert on the unusual — a CI token reading production paths at 3 a.m., a spike in reads, access from a new IP.
- Treat any exposed secret as compromised — rotate first. Don’t rely on “it was only public for a minute” or a history rewrite. Revoke immediately, then investigate and clean up.
- Version secrets and plan for rollback. KV v2 / cloud secret versions let you recover instantly from a bad rotation instead of scrambling to reconstruct the old value.
- Have a break-glass procedure. Document how humans access critical secrets when the automation is down — gated by MFA, requiring approvals, and reviewed after the fact. Test it before you need it.
- Separate secrets per environment and per tenant. Prod, staging, and dev must not share credentials; a leak in a low-trust environment must never grant prod access.
- Minimize the number of humans who ever see plaintext. Prefer store-generated secrets so the plaintext never exists on a laptop; use approvals and time-boxed access for the rare cases a human must read one.
- Give every secret a documented owner and expiry. Ownerless, never-expiring secrets accumulate into an unmanageable liability. Inventory them and retire the unused.
- Back up and DR-test your secret store. A secret manager is a single point of failure; run it HA, back up its storage and unseal/root keys securely, and rehearse recovery.
References
- roadmap.sh — DevOps Roadmap
- HashiCorp Vault documentation
- Vault concepts: seal/unseal · leases
- Vault dynamic secrets tutorial
- Vault Kubernetes auth method
- Vault transit (encryption-as-a-service) engine
- AWS Secrets Manager documentation
- AWS Secrets Manager rotation
- AWS Systems Manager Parameter Store
- Azure Key Vault documentation
- GCP Secret Manager documentation
- Kubernetes — Secrets · Encrypting Secret Data at Rest
- External Secrets Operator
- Bitnami Sealed Secrets
- SOPS
- Secrets Store CSI Driver
- gitleaks · trufflehog · detect-secrets
- GitHub secret scanning & push protection
- Configuring OpenID Connect in cloud providers (GitHub Actions)
- OWASP Secrets Management Cheat Sheet