站长的密钥正在失控
给一个站长做安全体检,最常看到的场景是这样的:数据库密码写在 wp-config.php 里,云服务商的 API Key 写在服务器的 ~/.bashrc 里,SMTP 授权码写在某个 cron 脚本里,SSH 私钥明文放在 /root/.ssh/ 并且权限是 644,部署脚本里还有一行硬编码的 GitHub Token。这些密钥散落在十几个文件里,没有集中管理,没有轮换机制,谁上过服务器谁就可能看过它们。
更麻烦的是备份。很多站长用 rsync 或者 Git 把整个站点目录备份到异地,而配置文件一并被带上去了——等于把数据库密码和 API Key 打包送进了可能不够安全的存储桶。一旦那份备份泄露,攻击者拿到的不只是一个网站,而是你所有账号的钥匙串。
密钥管理是个人站安全里最容易忽略的一环,因为它不像防火墙那样有立竿见影的效果,出事之前感觉纯粹是"形式主义"。但恰恰是密钥泄露,导致的数据事故最严重。本文介绍两套互补的方案:轻量的 SOPS 用于给配置文件做加密和版本化,专业的 HashiCorp Vault 用于集中化的密钥分发与动态凭证。个人站通常先上 SOPS 就够,规模上来后再考虑 Vault。
先分清三类密钥
在选工具之前,先把手上的密钥分分类,因为不同类别的处理方式不同。第一类是静态长期凭证,比如数据库密码、SMTP 授权码、云厂商的 API Key,特点是不设过期时间、泄露后长期有效。第二类是部署密钥,比如 SSH 私钥、Git 部署用的 Deploy Key,特点是与权限绑定、通常绑定某个具体环境。第三类是敏感配置值,比如加密盐、JWT 签名密钥,特点是必须精确不能出错,丢失或泄露都会导致服务异常。
分类的意义在于明确优先级。静态长期凭证危害最大,应该优先做集中管理和定期轮换;部署密钥可以配合 SSH CA 和强制命令限制;敏感配置值则要考虑加密存储和备份。无论哪一类,核心原则都是三条:不硬编码、集中存放、可审计可轮换。
SOPS:让密钥安全地进 Git
SOPS(Secrets OPerationS)是 Mozilla 开源的一个加密编辑器,最大的价值是能对 YAML、JSON、ENV 等结构化文件做"值加密、键明文"的处理——文件结构是可读的,方便 diff 和 review,但里面的敏感值全是密文。这样你就能把加密后的配置文件放心地提交进 Git,实现密钥的版本化。
安装很简单,从 GitHub 下载对应架构的二进制放到 /usr/local/bin 即可。然后生成一个用于加密的密钥。SOPS 支持多种后端密钥,个人站最简单的选择是 age(一个现代加密工具):
# 生成 age 密钥对
age-keygen -o ~/.config/sops/age/keys.txt
# 输出类似
# Public key: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx公钥用来加密,私钥文件 keys.txt 用来解密,必须妥善保管、绝不入库。接着写一个 SOPS 配置:
# .sops.yaml
creation_rules:
- path_regex: secrets/.*\.yaml$
age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx假设你有一个待加密的密钥文件:
# secrets/db.yaml
db_password: SuperSecret123
smtp_password: MailSecret456
jwt_secret: MyJwtSigningKey执行加密命令:
sops --encrypt --in-place secrets/db.yaml加密后的文件长这样——键明文,值密文:
db_password: ENC[AES256_GCM,data:8sG...,iv:...,tag:...,type:str]
smtp_password: ENC[AES256_GCM,data:Kd9...,iv:...,tag:...,type:str]
jwt_secret: ENC[AES256_GCM,data:Pq2...,iv:...,tag:...,type:str]
sops:
age:
- recipient: age1xxxx...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
-----END AGE ENCRYPTED FILE-----这个文件可以安全地进 Git。团队成员各自持有私钥,但只有被列入 creation_rules 的公钥对应的私钥才能解密。想改某个值,直接 sops secrets/db.yaml,它会自动解密到编辑器、保存时重新加密。要看明文,用 sops --decrypt secrets/db.yaml。在服务器上读取时,可以用 sops exec-env 或者 sops exec-file 把解密内容注入环境变量,避免明文落盘:
sops exec-env secrets/db.yaml 'mysql -u root -p"$db_password" -e "SHOW DATABASES"'SOPS 的一个实用点是它支持细粒度权限控制。你可以在 creation_rules 里对不同环境用不同密钥:生产环境的 secrets 用生产密钥加密,只允许运维人员的私钥解密;开发环境用另一套。这样即使开发者的私钥泄露,也解不开生产密钥。另外,SOPS 能配合 Git diff 工具做"解密预览",review 时能看到改动的是哪个值,但仓库里始终是密文。
Vault:集中式密钥管理
当密钥数量变多、需要轮换、或者有多个服务和多人协作时,SOPS 的"每个文件各自加密"模式就显得分散了。这时需要 HashiCorp Vault——一个专门做密钥管理的服务器。它的核心能力有三:集中存储、访问审计、动态凭证。
起步配置如下:
wget -O- https://apt.releases.hashicorp.com/gpg | \
gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" \
| tee /etc/apt/sources.list.d/hashicorp.list
apt update && apt install vault
# 开发模式启动(仅测试!)
vault server -dev -dev-root-token-id=root -dev-listen-address=127.0.0.1:8200注意 -dev 模式会使用内存存储、数据一重启就没、并且是明文不加密的,绝对不能用于生产。生产要用配置文件加持久化存储后端。第一次初始化会生成解封密钥(Unseal Key)和根 Token,务必离线保存:
vault operator init -key-shares=5 -key-threshold=3它的含义是把解封密钥拆成 5 份,任何 3 份凑齐才能解封。这是 Shamir 秘密共享,防止单点保管丢失。Vault 启动后处于"已密封"状态,必须用足够多的解封密钥解封才能提供服务。这个设计很关键:即使攻击者拿到了磁盘数据,没有解封密钥也是密文。
存取一个密钥很简单:
vault kv put secret/mysite/db password=SuperSecret123 user=siteuser
vault kv get secret/mysite/db
vault kv get -field=password secret/mysite/db真正体现 Vault 价值的是动态数据库凭证。你配好数据库的连接信息后,应用不再使用固定的数据库账号密码,而是每次启动时向 Vault 申请一对临时的用户名密码,有效期比如 1 小时,过期自动删除:
vault secrets enable database
vault write database/config/mydb \
plugin_name=mysql-database-plugin \
connection_url="{{username}}:{{password}}@tcp(127.0.0.1:3306)/" \
allowed_roles="app" \
username="vault_admin" password="AdminPass"
vault write database/roles/app \
db_name=mydb \
creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; \
GRANT SELECT,INSERT,UPDATE ON mydb.* TO '{{name}}'@'%';" \
default_ttl="1h" max_ttl="24h"之后应用向 database/creds/app 申请凭证,就会拿到一对全新的、权限受限的、一小时后自动失效的账号。这带来两个巨大好处:密钥不再长期有效,泄露的窗口被压缩到 TTL 内;每次申请都会留下审计日志,谁在什么时候拿了什么凭证清清楚楚。这是静态密码永远做不到的。
Vault 的部署红线
Vault 存着整套系统的密钥,它本身就是最高价值目标,部署必须极其谨慎。第一条红线:8200 端口绝不对公网开放,只监听 127.0.0.1,远程访问走 SSH 隧道或者服务网格。第二条:解封密钥离线保存,存在密码管理器或纸质保险柜里,不要和 Vault 服务器放一起,否则等于没加密。
第三条:用 AppRole 而不是根 Token 给应用授权。根 Token 权限太大,一旦泄露全盘皆输。AppRole 是一套给机器用的认证方式,每个应用有独立的 Role ID 和 Secret ID,配合策略(Policy)做最小权限授予:
vault policy write app-policy - <<EOF
path "secret/data/mysite/*" {
capabilities = ["read"]
}
path "database/creds/app" {
capabilities = ["read"]
}
EOF第四条:启用审计日志。vault audit enable file file_path=/var/log/vault/audit.log,它会记录每一次密钥读写,虽然日志本身包含敏感信息需要加密保护,但没有审计的密钥管理系统等于开了监控不录。
轮换:密钥管理里最被忽视的动作
无论用 SOPS 还是 Vault,密钥轮换都是必须制度化的事。很多站长觉得"密码没泄露就不用换",这是错的——你无法确定它有没有泄露,只能通过定期轮换把未知的泄露风险强制清零。轮换的原则是:数据库密码、API Key 至少每季度换一次;怀疑泄露时立即换;人员变动(比如前同事离职)后立即换。
轮换的难点不在生成新密码,而在"所有用到这个密钥的地方都要同步更新且不能中断服务"。所以轮换流程要有双写过渡:先把新密码在所有节点部署好、把旧密码设为仍可用,验证无误后再把旧密码禁用。用 Vault 的动态凭证可以把这个过程自动化,因为凭证本身就是短期的、自动切换的,这是它相对静态密钥的又一优势。
给个人站的落地建议
别一上来就追求大而全。个人站的合理路线是分阶段推进。第一阶段:把所有散落的密钥集中到几个配置文件里,用 SOPS 加密后进 Git,同时清理掉服务器上那些明文密钥和 644 权限的私钥。这一步成本最低、收益最大。第二阶段:把 SSH 改成基于 CA 的证书认证,用 command= 和 from= 限制部署密钥的用途,防止私钥被盗后为所欲为。第三阶段:如果有多个服务和自动化流程需要凭据,再引入 Vault,用 AppRole 和动态凭证替代静态密码。
工具只是手段,真正的转变是意识:密钥和代码一样,需要版本管理、需要权限控制、需要审计、需要定期更新。把密钥当成代码来治理,你才真正掌握了站点安全的主动权。反过来,如果密钥仍然是散落在十几个文件里的明文,那么再多的防火墙、再勤的补丁,都挡不住一次配置文件误传导致的彻底失守。