每个生产应用都有密钥:数据库密码、API密钥、TLS证书、OAuth客户端密钥。如何管理这些密钥决定了配置错误是小事一桩还是登上头条的安全漏洞。然而,密钥管理仍然是基础设施中最常被忽视的领域之一。
我曾为从3人创业公司到50人开发团队的各种团队实施过密钥管理。合适的工具取决于你的团队规模、基础设施复杂性和合规要求。本文比较了2026年三种主流方法,并为每种方法提供了实施指南。
.env 文件的问题
让我们从大多数团队实际的做法开始:将密钥存储在 .env 文件中,提交到私有仓库或通过Slack共享。这种方法在出问题前一直有效。
我亲眼目睹的真实事件:
- 一名开发者将
.env文件提交到了公共仓库。AWS密钥在14分钟内被用来启动加密货币矿机。损失:AWS标记前已花费23,000美元。 - 一个团队通过Slack DM共享数据库凭证。六个月后,一名前员工仍然可以访问,因为没有人在他们离职后轮换凭证。
- 一个预发布环境”临时”使用了生产数据库凭证长达三年。一名初级开发者针对错误环境运行了迁移脚本。
这些并非罕见场景。它们都是周一早晨就会发生的事故。共同点是 .env 文件没有访问控制、没有审计跟踪、没有轮换机制,也没有静态加密。
HashiCorp Vault:企业级标准
Vault是现有最全面的密钥管理解决方案。它处理密钥存储、动态凭证生成、加密即服务和基于身份的访问控制。
何时选择Vault
- 你需要动态密钥(每个会话生成的数据库凭证)
- 合规要求对所有密钥访问进行审计跟踪
- 你有专门的基础设施或DevOps能力来管理它
- 多个团队需要对不同密钥有不同的访问级别
为小型团队设置 Vault
# 用于开发环境的 Vault docker-compose.yml
services:
vault:
image: hashicorp/vault:1.17
cap_add:
- IPC_LOCK
environment:
VAULT_DEV_ROOT_TOKEN_ID: "dev-only-token"
VAULT_DEV_LISTEN_ADDRESS: "0.0.0.0:8200"
ports:
- "8200:8200"
# 生产环境使用 Raft 存储后端:
# vault server -config=/vault/config/vault.hcl
# 生产环境 Vault 配置
# /vault/config/vault.hcl
storage "raft" {
path = "/vault/data"
node_id = "vault-1"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/vault/tls/server.crt"
tls_key_file = "/vault/tls/server.key"
}
api_addr = "https://vault.internal:8200"
cluster_addr = "https://vault.internal:8201"
# 启用审计日志
audit {
type = "file"
options {
file_path = "/vault/logs/audit.log"
}
}
动态数据库凭证
Vault 的杀手级功能是动态凭证。与存储静态数据库密码不同,Vault 为每个应用程序实例生成唯一的凭证,并具有自动过期功能:
# 启用数据库 secrets 引擎
vault secrets enable database
# 配置 PostgreSQL 连接
vault write database/config/myapp-db plugin_name=postgresql-database-plugin allowed_roles="app-readonly","app-readwrite" connection_url="postgresql://{{username}}:{{password}}@db.internal:5432/myapp?sslmode=require" username="vault_admin" password="initial-admin-password"
# 创建一个生成只读凭证的角色
vault write database/roles/app-readonly db_name=myapp-db creation_statements="CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{{name}}";" default_ttl="1h" max_ttl="24h"
# 应用程序请求凭证
vault read database/creds/app-readonly
# 返回: username=v-app-readonly-xyz123, password=A1B2C3..., lease_duration=1h
每个凭证都是唯一的、有时限的,并且经过审计。当员工离职时,您不需要轮换共享密码——他们的 Vault 令牌被吊销,并且它生成的所有凭证都会自动过期。
在应用程序代码中使用 Vault
// 使用 Vault 管理密钥的 Go 应用程序
package main
import (
"context"
"database/sql"
"log"
"time"
vault "github.com/hashicorp/vault/api"
_ "github.com/lib/pq"
)
type VaultDBProvider struct {
client *vault.Client
role string
db *sql.DB
lease string
}
func (v *VaultDBProvider) GetConnection(ctx context.Context) (*sql.DB, error) {
// 从 Vault 读取动态凭据
secret, err := v.client.Logical().ReadWithContext(ctx,
"database/creds/"+v.role)
if err != nil {
return nil, fmt.Errorf("读取 vault 凭据失败: %w", err)
}
username := secret.Data["username"].(string)
password := secret.Data["password"].(string)
v.lease = secret.LeaseID
dsn := fmt.Sprintf("postgres://%s:%s@db.internal:5432/myapp?sslmode=require",
username, password)
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("连接数据库失败: %w", err)
}
// 启动后台续期
go v.renewLease(ctx, secret)
v.db = db
return db, nil
}
func (v *VaultDBProvider) renewLease(ctx context.Context, secret *vault.Secret) {
renewer, err := v.client.NewLifetimeWatcher(&vault.LifetimeWatcherInput{
Secret: secret,
})
if err != nil {
log.Printf("创建租约续期器失败: %v", err)
return
}
go renewer.Start()
defer renewer.Stop()
for {
select {
case <-ctx.Done():
return
case err := <-renewer.DoneCh():
if err != nil {
log.Printf("租约续期失败,正在重新连接: %v", err)
v.GetConnection(ctx) // 获取新凭据
}
return
case <-renewer.RenewCh():
log.Printf("租约续期成功")
}
}
}
SOPS:Git 中的加密文件
Mozilla SOPS (Secrets OPerationS) 采用了不同的方法:将密钥加密后存储在与代码一起位于 Git 中的文件里。加密文件会被提交到仓库,但只有授权的用户和系统才能解密它们。
何时使用 SOPS
- 您希望密钥与代码一起进行版本控制(GitOps 工作流)
- 您的团队规模较小(少于 15 名开发者)
- 您不需要动态密钥生成
- 您已经在使用 AWS KMS、GCP KMS 或 Azure Key Vault 进行密钥管理
设置 SOPS
# .sops.yaml - 存储库根目录的配置文件
creation_rules:
# 使用 AWS KMS 加密的生产环境密钥
- path_regex: environments/production/.*.yaml$
kms: "arn:aws:kms:us-east-1:123456789:key/production-key-id"
encrypted_regex: "^(password|secret|key|token|dsn)$"
# 使用不同密钥加密的暂存环境密钥
- path_regex: environments/staging/.*.yaml$
kms: "arn:aws:kms:us-east-1:123456789:key/staging-key-id"
encrypted_regex: "^(password|secret|key|token|dsn)$"
# 使用 age 加密的开发环境密钥(无云依赖)
- path_regex: environments/dev/.*.yaml$
age: "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
encrypted_regex: "^(password|secret|key|token|dsn)$"
# 创建密钥文件
cat > environments/production/secrets.yaml << EOF
database:
password: "super-secret-production-password"
host: "db.production.internal"
api:
key: "sk_live_abc123def456"
secret: "whsec_production_webhook_secret"
redis:
password: "redis-production-password"
host: "redis.production.internal"
EOF
# 加密它
sops --encrypt --in-place environments/production/secrets.yaml
# 现在文件在 Git 中看起来像这样:
# database:
# password: ENC[AES256_GCM,data:abc123...,iv:xyz...,tag:...]
# host: db.production.internal # 非密钥值保持可读
# api:
# key: ENC[AES256_GCM,data:def456...,iv:abc...,tag:...]
注意,只有匹配 encrypted_regex 的值会被加密。文件结构保持可读,这使得代码审查有意义 — 你可以看到添加了新的密钥,而无法读取其值。
CI/CD 中的 SOPS
# 使用 SOPS 的 GitHub Actions 工作流
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # 用于 AWS OIDC 身份验证
contents: read
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/deploy-role
aws-region: us-east-1
- name: 安装 SOPS
run: |
curl -LO https://github.com/getsops/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64
chmod +x sops-v3.9.0.linux.amd64
sudo mv sops-v3.9.0.linux.amd64 /usr/local/bin/sops
- name: 解密并部署
run: |
# 将密钥解密为环境变量
eval $(sops --decrypt --output-type dotenv environments/production/secrets.yaml)
# 使用可用的解密密钥进行部署
./deploy.sh
Sealed Secrets: Kubernetes 原生方法
如果你运行 Kubernetes,Bitnami Sealed Secrets 优雅地解决了"如何在 Git 中存储 Kubernetes Secrets"的问题。
何时使用 Sealed Secrets
- 您运行 Kubernetes 并使用 GitOps(Flux 或 ArgoCD)
- 您希望机密信息在 Git 中以声明方式管理
- 您不需要在 Kubernetes 之外的机密信息
工作原理
# 在集群中安装控制器
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets --namespace kube-system
# 创建常规 Kubernetes 机密
kubectl create secret generic myapp-secrets --from-literal=database-password="production-password" --from-literal=api-key="sk_live_abc123" --dry-run=client -o yaml > myapp-secret.yaml
# 加密封装(使用集群的公钥加密)
kubeseal --format yaml < myapp-secret.yaml > myapp-sealed-secret.yaml
# 加密的机密信息可以安全地提交到 Git
cat myapp-sealed-secret.yaml
# myapp-sealed-secret.yaml - 可安全提交
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: myapp-secrets
namespace: default
spec:
encryptedData:
database-password: AgBy3i4OJSWK+PiTySYZZA9rO... # 已加密
api-key: AgCtr8YPOJSWK+QwRtGHn8B2rP... # 已加密
template:
metadata:
name: myapp-secrets
namespace: default
type: Opaque
当应用到集群时,Sealed Secrets 控制器会解密 SealedSecret 并创建一个常规的 Kubernetes Secret,Pod 可以将其作为卷或环境变量挂载。
比较矩阵
| 功能 | Vault | SOPS | Sealed Secrets |
|---|---|---|---|
| 动态机密 | 是 | 否 | 否 |
| 审计日志 | 内置 | Git 历史 | K8s 审计日志 |
| 机密轮换 | 自动 | 手动 | 手动 |
| 所需基础设施 | Vault 集群 | KMS 服务 | K8s 控制器 |
| 学习曲线 | 高 | 低 | 中等 |
| 成本(小型团队) | $50-200/月 | $0-5/月 | $0 |
| GitOps 兼容性 | 通过代理 | 原生 | 原生 |
| 非 K8s 支持 | 是 | 是 | 否 |
| 合规就绪 | SOC 2, HIPAA | 取决于 KMS | 有限 |
按团队规模的建议
1-5 名开发者,无合规要求: 从 SOPS 和 age 开始(无云依赖)。设置需要 30 分钟,能满足 90% 的机密管理需求。当您需要动态凭证或出现审计要求时,升级到 Vault。
5-20 名开发者,基本合规要求: 使用 SOPS 配合 AWS KMS 或 GCP KMS。云 KMS 提供密钥轮换和访问日志记录,无需运行额外基础设施。如果您运行 Kubernetes,可添加 Sealed Secrets。
20+ 开发人员,SOC 2 或 HIPAA: Vault。当审计员询问"谁访问了这个密钥以及何时访问的?"时,你在设置和维护上的投入就会得到回报,你可以向他们展示一个包含自动凭证轮换的完整审计跟踪。
常见错误及避免方法
- 加密整个文件而不是单个值。 使用 SOPS 的
encrypted_regex来保持文件结构可读。代码审查人员需要看到更改内容,即使他们无法看到实际的密钥值。 - 共享 Vault 根令牌。 根令牌应该只使用一次(在初始设置期间),然后撤销。为每个用户和服务创建命名认证方法和策略。
- 员工离职后不轮换密钥。 自动化此过程。Vault 的动态密钥 inherently 处理这个问题。对于 SOPS,创建一个轮换运行手册并安排执行。
- 在不同环境中使用相同的密钥。 生产环境和测试环境绝不应该共享凭证。如果测试环境被攻破,生产环境应该不受影响。
- 意外记录密钥。 审查你的日志管道。使用显式字段选择的结构化日志比记录整个请求对象更安全。
密钥管理工作并不 glamorous,但它却是将事件控制在范围内与发生灾难性泄露之间的区别。选择与你团队规模和复杂度相匹配的工具,正确实施它,然后继续构建功能。最好的密钥管理系统是团队实际持续使用的那个。
