单台 VPS 自托管 secrets manager 怎么选
比较 OpenBao、Infisical、SOPS 配合 age、systemd credentials 和受限环境变量文件,说明单台 VPS 上各方案的运维成本与适用场景。
自托管 secrets manager 与密码管理器的区别
自托管 secrets manager 将凭据提供给进程。密码管理器将凭据提供给人。其他差异都源于这一点。密码管理器由在场且专注的人解锁。secrets manager 则必须在凌晨 03:00、无人醒着时,将数据库密码提供给应用。
两者的故障模式不同,而且影响很大。密码管理器处于锁定状态只是带来不便:重新输入主密码即可。secrets manager 处于密封状态则会导致服务中断:任何在密封期间重启的服务都无法获得凭据,并会一直停机。将 Vaultwarden 用作自己的密码管理器可以很好地解决人的问题。但它无法解决机器的问题,而且它本来就不是为此设计的。
在一台服务器上,实际可行的方案分为两类。OpenBao 和 Infisical 是服务:包含 API、数据库、TLS(传输层安全)、登录步骤,以及一个现在必须保持运行的进程。SOPS 配合 age、systemd credentials 和 Docker secrets 则是文件:静态加密,由已经运行的组件解密,不需要额外监控任何组件。
先给出直接结论。对于只有一台服务器、供一两个人使用的环境,基于文件的方案通常更合适。没人能正确解封、也没人执行轮换的 OpenBao,比权限为 mode 600 的环境变量文件更糟糕,因为它增加了一个容易出错的活动部件和备份,而且无法提供任何你原本不能手动完成的轮换功能。
mode 600 的环境变量文件是否足够安全?
通常足够。它防范的是同一台主机上的其他用户读取数据库密码。Unix 文件权限可以实现这一点,而且在网络启动前就已生效。
sudo install -d -m 750 -o root -g myapp /etc/myapp
sudo install -m 640 -o root -g myapp /dev/null /etc/myapp/env
sudoedit /etc/myapp/env从文件所有者和其他用户两方面检查:
sudo -u myapp cat /etc/myapp/env
sudo -u nobody cat /etc/myapp/env第一条命令会输出文件内容。第二条命令输出 cat: /etc/myapp/env: Permission denied,因为 nobody 不属于 myapp 组,且文件没有设置其他用户权限位。这就是完整的安全模型,而且确实有效。
真正的泄露发生在后续步骤。包含 EnvironmentFile= 的 systemd 单元会将这些值复制到进程环境中,而进程环境是可读取的。
[Service]
User=myapp
EnvironmentFile=/etc/myapp/env
ExecStart=/usr/local/bin/myappsudo cat /proc/$(pgrep -n myapp)/environ | tr '\0' '\n'这会以明文输出您的机密,因为 /proc/<pid>/environ 可由 root 和运行该进程的用户读取。将环境变量附加到报告中的崩溃报告工具也能看到相同内容。任何以同一账户运行的工具也一样。因此,让机密远离 AI 代理首先要让机密离开环境变量。将该文件与专用的低权限服务用户配合使用,确保“运行该进程的用户”不是 root。
使用 age 的 SOPS:将加密的机密配置提交到 git
SOPS(secrets operations)会加密 YAML 或 JSON 文件中的值,并保留键名明文。age 是一种轻量级加密工具,只提供一对密钥,不需要密钥服务器。两者结合后,您可以将 secrets.enc.yaml 与代码一起提交到 git;git diff 仍会告诉您哪个设置发生了变化,但不会泄露该设置的新值。
Ubuntu 24.04 已打包 age,但没有打包 SOPS,因此请从发布页面获取 .deb。截至 2026 年 8 月,当前版本为 3.13.3。
sudo apt update && sudo apt install -y age
curl -LO https://github.com/getsops/sops/releases/download/v3.13.3/sops_3.13.3_amd64.deb
sudo apt install -y ./sops_3.13.3_amd64.deb
sops --version生成密钥对。age-keygen 会将私钥写入文件,并输出公钥,因此您会看到以 Public key: age1... 开头的一行。
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt
age-keygen -y ~/.config/sops/age/keys.txt将公钥放入仓库根目录的 .sops.yaml 中。这样,您就不必在命令行中记住接收者。
creation_rules:
- age: age1s3cqcks5genc6ru8chl0hkkd04zmxvczsvdxq99ekffe4gmvjpzsedk23csops encrypt secrets.yaml > secrets.enc.yaml
sops decrypt secrets.enc.yaml不带 path_regex 的规则会匹配所有内容,这正是初始阶段所需的行为。如果之后添加规则,请确保它匹配传递给 sops 的文件,因为规则检查的是输入路径,而不是您重定向输出到的文件。
运行时,只将这些值传递给一个进程,不要传递给其他进程:
sops exec-env secrets.enc.yaml './myapp'sops exec-env 会在内存中解密,并将值设置到子进程的环境中,因此不会将明文写入磁盘。上一节提到的环境变量注意事项同样适用于该子进程。
这里有两个常见问题。在 systemd 下,错误 Failed to get the data key required to decrypt the SOPS file 几乎总是表示 SOPS 查找了错误的主目录,因为单元不会继承您的 HOME。请在单元中使用 Environment=SOPS_AGE_KEY_FILE=/etc/sops/age.txt 显式设置路径。另一个问题是,编辑 .sops.yaml 不会重新加密已经存在的文件:添加同事的公钥只会影响新文件,因此请对每个现有文件运行 sops updatekeys secrets.enc.yaml。如果您的配置已经通过 Ansible 管理,使用 Ansible Vault 加密相同的值也能达到相同效果,无需引入第二个工具。
systemd 凭据:不进入环境变量的机密
Ubuntu 24.04 附带 systemd 255,因此无需安装。systemd-creds 会在主机上加密机密,systemd 再将其解密到只有该服务可以读取的私有目录中。
sudo systemd-creds setup
sudo install -d -m 700 /etc/myapp
echo -n 'hunter2' | sudo systemd-creds encrypt --name=db_password - /etc/myapp/db_password.cred[Service]
User=myapp
LoadCredentialEncrypted=db_password:/etc/myapp/db_password.cred
ExecStart=/usr/local/bin/myapp服务会从由 $CREDENTIALS_DIRECTORY 指定的目录中的 db_password 文件读取该值。该值不会写入环境变量,因此 /proc/<pid>/environ 不会显示有用信息,明文也不会落到根文件系统中。
在将单元指向该文件前,先确认文件可以解密:
sudo systemd-creds decrypt /etc/myapp/db_password.cred -请确认使用哪个密钥加密了该文件,因为这决定备份是否可用。默认的 --with-key=auto 在存在且可用时使用 TPM2(可信平台模块版本 2)芯片,否则使用主机密钥。大多数 VPS 实例没有 TPM2。
systemd-analyze has-tpm2no 表示使用了主机密钥,该密钥位于 /var/lib/systemd/credential.secret 中,只有 root 可以读取。将 db_password.cred 恢复到一台没有该文件的新 VPS 后,任何情况下都无法解密该文件。请将 credential.secret 一并备份,或将明文保存在仍可访问的位置。
Docker secrets:/run/secrets 下的文件
Compose 从主机读取文件,并将其挂载到容器中的 /run/secrets/<name>。
services:
app:
image: myapp:latest
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./db_password.txtdocker compose exec app cat /run/secrets/db_password
docker compose exec app env | grep -i password第一个命令会输出密钥内容。第二个命令只输出 DB_PASSWORD_FILE=/run/secrets/db_password,这正是关键:该值从未写入容器环境变量,因此不会出现在 docker inspect 的输出中。许多官方镜像已经采用这种方式,Postgres 镜像也正是这样读取 POSTGRES_PASSWORD_FILE 的。
需要明确这一点。在 Swarm 模式之外,任何层都不会进行加密:./db_password.txt 是主机上的明文文件,唯一的保护措施是文件权限和所有者。请自行设置这两项,因为 Compose 会直接挂载全局可读的文件,且不会报告错误。与直接使用 env_file 这一快捷方式相比,相关权衡请参阅Compose 环境文件和密钥指南。
运行 OpenBao 和 Vault 的实际成本
OpenBao 是 Linux Foundation 从 HashiCorp Vault 分叉出的项目。HashiCorp 在 2023 年根据 Business Source License 重新授权 Vault 后,OpenBao 随之启动。OpenBao 继续采用 MPL 2.0(Mozilla Public License)。截至 2026 年 8 月,当前版本为 2.6.2。由于该分支保留了相同的命令接口,下面几乎所有内容同样适用于 Vault。
docker pull docker.io/openbao/openbao如果希望使用 apt 管理升级,可以在 OpenBao 下载页面获取 Debian 和 Ubuntu 软件包。服务器需要一个配置文件,其中包含 listener 和 storage backend:
listener "tcp" {
address = "127.0.0.1:8200"
tls_cert_file = "/path/to/full-chain.pem"
tls_key_file = "/path/to/private-key.pem"
}
storage "raft" {
path = "/path/to/raft/data"
node_id = "raft_node_1"
}然后启动一次:
bao operator init默认情况下,系统会将 root key 拆分为 5 个 share,并要求其中 3 个 share 才能解封。这两个值分别由 -key-shares 和 -key-threshold 标志控制。系统只会显示这些 share 和初始 root token 一次,之后不会再次显示。
下面是大多数对比文章都会略过的部分。重启后的服务器处于 sealed 状态。 OpenBao 只将 root key 保存在内存中。因此,重启后必须有人提供达到阈值数量的 share,服务器才能解密自己的存储数据。内核更新或因内存不足被终止,都会导致服务器处于 sealed 状态,并使应用无法登录。
在单人 VPS 上,Shamir 拆分并不能提供实际保护,因为 5 个 share 最终都会存放在同一个人的同一个密码管理器中。自动解封会将密钥转移到受信任的设备或服务。在大型云环境中,这通常意味着使用托管密钥服务;在 VPS 上,通常意味着将密钥文件放在与受保护数据相同的磁盘上。这会真实降低安全性,但可以换来服务器重启后自动恢复运行。应明确了解这项取舍,并记录自己的选择。
Infisical:一个 UI、一个数据库,以及必须由您持有的主密钥
Infisical 是一个机密管理平台,提供 Web 界面、项目、环境和按用户划分的访问控制。使用 Compose 自托管时,步骤很短:
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml
curl -o .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example
docker compose -f docker-compose.prod.yml up -d在执行最后一条命令前编辑 .env。其中两个值必须替换为您自己的值,且其中一个之后绝不能更改:
openssl rand -hex 16
openssl rand -base64 32第一个值是 ENCRYPTION_KEY,即一个 16 字节的十六进制字符串。它是 PostgreSQL 中用于加密机密的密钥。因此,丢失它会使完整的数据库备份变成一堆密文;在运行中的实例上更改它,会导致现有机密无法解密。第二个值是 AUTH_SECRET,即用于会话的 32 字节 Base64 字符串。SITE_URL 必须是您实际访问时使用的绝对 URL,并包含协议,否则登录重定向会失败。
如果您真正需要的是面向人员的功能,例如为小型团队提供 Web 界面并隔离不同环境,那么 Infisical 比 OpenBao 更合适;OpenBao 更适合管理会自行过期的数据库凭据。使用 Infisical 需要额外维护 PostgreSQL、Redis 和 TLS 证书,包括为它们打补丁和创建备份。
密钥服务停止且应用重启时会发生什么
这个问题决定了密钥服务是否适合部署在单台主机上。网络启动前就可以读取文件,但服务不能。
重启主机后,应用和 OpenBao 同时启动。应用请求数据库密码时,OpenBao 仍处于密封状态,请求失败。然后,systemd 会循环重启应用,直到人工输入解封密钥份额。系统没有损坏,但也没有任何服务真正启动。
有两种可行的处理方式。为单元设置启动顺序,并让应用重试:After=密钥服务,再加上Restart=on-failure和足够长的RestartSec=,避免持续请求 API。或者在部署时获取密钥,而不是在启动时获取:将密钥写入权限为 600 的文件,或写入 systemd 凭据。这样,运行中的系统依赖的是文件,而不是 API。
令牌过期本质上是同一个问题,只是发生在更长的时间尺度上。OpenBao 令牌和租约都有生存时间。长期运行且从不续期的进程会失去访问权限,而此时可能与任何部署都无关。这个故障之所以难以排查,正是因为当天没有任何配置变化。
备份存储本身
这里的每种方案都有一个密钥。没有该密钥,备份就毫无价值。记录密钥的存放位置。
对于 env 文件,文件本身就是机密,因此备份必须加密。对于 SOPS,加密文件可以存放在任何公开位置,而 ~/.config/sops/age/keys.txt 中的 age 私钥绝对不能丢失。对于 systemd 凭据,将 /var/lib/systemd/credential.secret 与 .cred 文件一起备份。对于 Infisical,执行 PostgreSQL 转储,并将 ENCRYPTION_KEY 存放在与转储文件分开的其他位置。
使用 raft 存储的 OpenBao 需要创建自身的快照:
bao operator raft snapshot save backup.snap
bao operator raft snapshot restore backup.snap快照包含加密存储。因此,将快照恢复到新服务器后,仍需要 bao operator init 中的解封密钥分片。每晚将快照复制到对象存储,但将密钥分片存放在任何地方都没有,这种备份实际上什么也备份不了。在依赖备份前,先在一次性 VPS 上测试恢复。
审计日志:谁读取了哪个机密
文件不会提供审计追踪。权限模式和所有者只能说明谁可能读取该机密,无法说明谁实际读取过。对路径使用 auditd 进行监视是最接近的替代方案,但它只能报告文件被打开过,不能说明使用了哪个值。
OpenBao 会将每个请求记录到需要显式启用的审计设备:
bao audit enable file file_path=/var/log/openbao_audit.log该日志有两个特性会影响服务器的运行方式。请求和响应中的大多数字符串都会使用 HMAC-SHA256 和盐进行哈希处理,因此可以将已知值与日志进行匹配,而日志本身不会包含明文。整数和布尔值会以明文写入,因此数值机密无法获得这种哈希保护。
还要注意一个运行陷阱:如果没有已启用的审计设备能够记录请求,OpenBao 就不会响应请求。如果设备以阻塞方式失败,请求会一直挂起,直到有人修复该设备。/var/log 磁盘已满会按设计使机密 API 停止工作。第一天就为审计日志分配独立空间并配置 logrotate 规则,不要等到第一次中断后才处理。
应运行哪种自托管机密管理器?
统计机器数量和人员数量,然后再选择。
- 一台机器、一个人:使用由 root 所有且服务用户可读取的
mode 600环境变量文件。如果不希望将值放在进程环境中,可添加 systemd 凭据。 - 一台机器、2 到 5 个人,且配置已经存储在 git 中:使用 SOPS 和 age。每个人获得一对密钥,
.sops.yaml列出所有获准解密的公钥。 - 多台机器、一个配置仓库,且不需要会过期的凭据:仍使用 SOPS 和 age。为每台主机配置一个接收者密钥,这样主机密钥被窃取后,也只能解密该主机的文件。
- 多台机器、多个团队确实需要具有有效期的数据库凭据,并且有人会查看审计记录:使用 OpenBao,并在预算中每月安排 1 小时的运维时间,用于解封和恢复演练。
上述 4 种情况遵循同一条规则。运行能够满足明确需求的最小方案,因为已停止运行的机密管理器,与其中没有任何机密的机密管理器没有区别。
FAQ
单个 VPS 使用自托管 secrets manager 是否值得?
通常不值得,前提是您指的是 OpenBao 或 Infisical 等服务。在只有一台服务器、由一两个人使用的情况下,权限为 mode 600 的环境变量文件或 systemd 加密凭据可以提供同等的本地用户防护,而且无需执行解封步骤,也无需维护额外服务。拥有多台机器和多个用户后,或者确实需要让凭据自动过期而无需人工轮换时,secrets service 才开始体现价值。
password manager 和 secrets manager 有什么区别?
password manager 存储由人输入的凭据,由人在场时解锁。secrets manager 将凭据提供给进程,因此必须在 03:00、无人值守时正常工作。两者的区别就在于此:锁定的 password manager 会要求您重新输入主密码,而已密封的 secrets manager 会停止所有在密封期间重启的服务。
重启后 OpenBao 被密封,我的应用会怎样?
应用无法获取 secrets,因此会启动失败;systemd 会循环重启这些应用,直到有人提供解封所需的阈值。默认阈值为 5 份密钥中的 3 份。OpenBao 只将 root key 保存在内存中,因此每次重启后都会再次密封。您可以启用自动解封,但在单个 VPS 上,这意味着解封密钥最终会与数据保存在同一块磁盘上;也可以在部署时将 secrets 写入文件,使系统启动不再依赖 API。
我可以将 SOPS 加密文件提交到公共仓库吗?
文件中的值已加密,因此没有 age 私钥的用户无法获取这些值。密钥本身未加密:读取者可以看到您持有 STRIPE_SECRET_KEY 和 SMTP_PASSWORD,以及它们各自的变更频率。对于大多数项目而言,这些元数据可以接受;但少数项目不能接受。将 age 私钥保留在仓库之外,并在添加或删除接收者后,对所有现有文件运行 sops updatekeys。